ShareViewController.swift 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import UIKit
  2. import Social
  3. import DcCore
  4. import MobileCoreServices
  5. import Intents
  6. import SDWebImageWebPCoder
  7. import SDWebImage
  8. class ShareViewController: SLComposeServiceViewController {
  9. let logger = SimpleLogger()
  10. let dcAccounts: DcAccounts = DcAccounts()
  11. lazy var dcContext: DcContext = {
  12. return dcAccounts.getSelected()
  13. }()
  14. var selectedChatId: Int?
  15. var selectedChat: DcChat?
  16. var shareAttachment: ShareAttachment?
  17. var isAccountConfigured: Bool = true
  18. var isLoading: Bool = false
  19. var previewImageHeightConstraint: NSLayoutConstraint?
  20. var previewImageWidthConstraint: NSLayoutConstraint?
  21. lazy var preview: SDAnimatedImageView? = {
  22. UIGraphicsBeginImageContext(CGSize(width: 96, height: 96))
  23. let image = UIGraphicsGetImageFromCurrentImageContext()
  24. UIGraphicsEndImageContext()
  25. let imageView = SDAnimatedImageView(image: image)
  26. imageView.clipsToBounds = true
  27. imageView.shouldGroupAccessibilityChildren = true
  28. imageView.isAccessibilityElement = false
  29. imageView.contentMode = .scaleAspectFit
  30. imageView.translatesAutoresizingMaskIntoConstraints = false
  31. previewImageHeightConstraint = imageView.constraintHeightTo(96)
  32. previewImageWidthConstraint = imageView.constraintWidthTo(96)
  33. previewImageHeightConstraint?.isActive = true
  34. previewImageWidthConstraint?.isActive = true
  35. return imageView
  36. }()
  37. lazy var initialsBadge: InitialsBadge = {
  38. let view = InitialsBadge(name: "", color: UIColor.clear, size: 28)
  39. view.translatesAutoresizingMaskIntoConstraints = false
  40. view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  41. return view
  42. }()
  43. override func viewDidLoad() {
  44. super.viewDidLoad()
  45. setupNavigationBar()
  46. // workaround for iOS13 bug
  47. if #available(iOS 13.0, *) {
  48. _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: .main) { (_) in
  49. if let layoutContainerView = self.view.subviews.last {
  50. layoutContainerView.frame.size.height += 10
  51. }
  52. }
  53. }
  54. placeholder = String.localized("chat_input_placeholder")
  55. let webPCoder = SDImageWebPCoder.shared
  56. SDImageCodersManager.shared.addCoder(webPCoder)
  57. dcAccounts.logger = logger
  58. dcAccounts.openDatabase()
  59. let accountIds = dcAccounts.getAll()
  60. for accountId in accountIds {
  61. let dcContext = dcAccounts.get(id: accountId)
  62. if !dcContext.isOpen() {
  63. do {
  64. let secret = try KeychainManager.getAccountSecret(accountID: dcContext.id)
  65. if !dcContext.open(passphrase: secret) {
  66. logger.error("Failed to open database.")
  67. }
  68. } catch KeychainError.unhandledError(let message, let status), KeychainError.accessError(let message, let status) {
  69. logger.error("KeychainError. \(message). Error status: \(status)")
  70. } catch {
  71. logger.error("\(error)")
  72. }
  73. }
  74. }
  75. if #available(iOSApplicationExtension 13.0, *) {
  76. if let intent = self.extensionContext?.intent as? INSendMessageIntent,
  77. let identifiers = intent.conversationIdentifier?.split(separator: "."),
  78. let contextId = Int(identifiers[0]),
  79. let chatId = Int(identifiers[1]) {
  80. if accountIds.contains(contextId) {
  81. dcContext = dcAccounts.get(id: contextId)
  82. selectedChatId = chatId
  83. } else {
  84. logger.error("invalid INSendMessageIntent \(contextId) doesn't exist")
  85. cancel()
  86. return
  87. }
  88. }
  89. }
  90. isAccountConfigured = dcContext.isOpen() && dcContext.isConfigured()
  91. if !isAccountConfigured {
  92. logger.error("selected context \(dcContext.id) is not configured")
  93. cancel()
  94. return
  95. }
  96. if selectedChatId == nil {
  97. selectedChatId = dcContext.getChatIdByContactId(contactId: Int(DC_CONTACT_ID_SELF))
  98. logger.debug("selected chatID: \(String(describing: selectedChatId))")
  99. }
  100. let contact = dcContext.getContact(id: Int(DC_CONTACT_ID_SELF))
  101. let title = dcContext.displayname ?? dcContext.addr ?? ""
  102. initialsBadge.setName(title)
  103. initialsBadge.setColor(contact.color)
  104. if let image = contact.profileImage {
  105. initialsBadge.setImage(image)
  106. }
  107. guard let chatId = selectedChatId else {
  108. cancel()
  109. return
  110. }
  111. selectedChat = dcContext.getChat(chatId: chatId)
  112. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  113. guard let self = self else { return }
  114. self.shareAttachment = ShareAttachment(dcContext: self.dcContext, inputItems: self.extensionContext?.inputItems, delegate: self)
  115. }
  116. }
  117. override func loadPreviewView() -> UIView! {
  118. return preview
  119. }
  120. override func isContentValid() -> Bool {
  121. // Do validation of contentText and/or NSExtensionContext attachments here
  122. return isAccountConfigured && !isLoading && (!(contentText?.isEmpty ?? true) || !(self.shareAttachment?.isEmpty ?? true))
  123. }
  124. private func setupNavigationBar() {
  125. guard let item = navigationController?.navigationBar.items?.first else { return }
  126. let button = UIBarButtonItem(
  127. title: String.localized("menu_send"),
  128. style: .done,
  129. target: self,
  130. action: #selector(appendPostTapped))
  131. item.rightBarButtonItem? = button
  132. let cancelButton = UIBarButtonItem(
  133. title: String.localized("cancel"),
  134. style: .done,
  135. target: self,
  136. action: #selector(onCancelPressed))
  137. let avatarItem = UIBarButtonItem(customView: initialsBadge)
  138. item.leftBarButtonItems = [avatarItem, cancelButton]
  139. }
  140. /// Invoked when the user wants to post.
  141. @objc
  142. private func appendPostTapped() {
  143. if let chatId = self.selectedChatId {
  144. guard var messages = shareAttachment?.messages else { return }
  145. if !self.contentText.isEmpty {
  146. if messages.count == 1 {
  147. messages[0].text?.append(self.contentText)
  148. } else {
  149. let message = dcContext.newMessage(viewType: DC_MSG_TEXT)
  150. message.text = self.contentText
  151. messages.insert(message, at: 0)
  152. }
  153. }
  154. let chatListController = SendingController(chatId: chatId, dcMsgs: messages, dcContext: dcContext)
  155. chatListController.delegate = self
  156. self.pushConfigurationViewController(chatListController)
  157. }
  158. }
  159. func quit() {
  160. dcAccounts.closeDatabase()
  161. // Inform the host that we're done, so it un-blocks its UI.
  162. self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
  163. }
  164. override func configurationItems() -> [Any]! {
  165. let item = SLComposeSheetConfigurationItem()
  166. if isAccountConfigured {
  167. // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
  168. // TODO: discuss if we want to add an item for the account selection.
  169. item?.title = String.localized("forward_to")
  170. item?.value = selectedChat?.name
  171. logger.debug("configurationItems chat name: \(String(describing: selectedChat?.name))")
  172. item?.tapHandler = {
  173. let chatListController = ChatListController(dcContext: self.dcContext, chatListDelegate: self)
  174. self.pushConfigurationViewController(chatListController)
  175. }
  176. } else {
  177. item?.title = String.localized("share_account_not_configured")
  178. }
  179. return [item as Any]
  180. }
  181. override func didSelectCancel() {
  182. quit()
  183. }
  184. @objc func onCancelPressed() {
  185. cancel()
  186. }
  187. }
  188. extension ShareViewController: ChatListDelegate {
  189. func onChatSelected(chatId: Int) {
  190. selectedChatId = chatId
  191. selectedChat = dcContext.getChat(chatId: chatId)
  192. reloadConfigurationItems()
  193. popConfigurationViewController()
  194. }
  195. }
  196. extension ShareViewController: SendingControllerDelegate {
  197. func onSendingAttemptFinished() {
  198. DispatchQueue.main.async {
  199. self.popConfigurationViewController()
  200. UserDefaults.shared?.set(true, forKey: UserDefaults.hasExtensionAttemptedToSend)
  201. self.quit()
  202. }
  203. }
  204. }
  205. extension ShareViewController: ShareAttachmentDelegate {
  206. func onUrlShared(url: URL) {
  207. DispatchQueue.main.async {
  208. if var contentText = self.contentText, !contentText.isEmpty {
  209. contentText.append("\n\(url.absoluteString)")
  210. self.textView.text = contentText
  211. } else {
  212. self.textView.text = "\(url.absoluteString)"
  213. }
  214. self.validateContent()
  215. }
  216. }
  217. func onAttachmentChanged() {
  218. DispatchQueue.main.async {
  219. self.validateContent()
  220. }
  221. }
  222. func onThumbnailChanged() {
  223. DispatchQueue.main.async { [weak self] in
  224. guard let self = self else { return }
  225. if let preview = self.preview {
  226. preview.image = self.shareAttachment?.thumbnail ?? nil
  227. }
  228. }
  229. }
  230. func onLoadingStarted() {
  231. isLoading = true
  232. }
  233. func onLoadingFinished() {
  234. isLoading = false
  235. self.validateContent()
  236. }
  237. }