ShareViewController.swift 10 KB

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