ChatViewController.swift 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. import MapKit
  2. import QuickLook
  3. import UIKit
  4. import InputBarAccessoryView
  5. import AVFoundation
  6. protocol MediaSendHandler {
  7. func onSuccess()
  8. }
  9. extension ChatViewController: MediaSendHandler {
  10. func onSuccess() {
  11. refreshMessages()
  12. }
  13. }
  14. class ChatViewController: MessagesViewController {
  15. var dcContext: DcContext
  16. weak var coordinator: ChatViewCoordinator?
  17. let outgoingAvatarOverlap: CGFloat = 17.5
  18. let loadCount = 30
  19. let chatId: Int
  20. let refreshControl = UIRefreshControl()
  21. var messageList: [DcMsg] = []
  22. var msgChangedObserver: Any?
  23. var incomingMsgObserver: Any?
  24. lazy var navBarTap: UITapGestureRecognizer = {
  25. UITapGestureRecognizer(target: self, action: #selector(chatProfilePressed))
  26. }()
  27. /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and udpate audio cell UI accordingly.
  28. open lazy var audioController = BasicAudioController(messageCollectionView: messagesCollectionView)
  29. private var disableWriting: Bool
  30. var showCustomNavBar = true
  31. var previewView: UIView?
  32. var previewController: PreviewController?
  33. override var inputAccessoryView: UIView? {
  34. if disableWriting {
  35. return nil
  36. }
  37. return messageInputBar
  38. }
  39. init(dcContext: DcContext, chatId: Int) {
  40. self.dcContext = dcContext
  41. self.chatId = chatId
  42. self.disableWriting = !DcChat(id: chatId).canSend
  43. super.init(nibName: nil, bundle: nil)
  44. hidesBottomBarWhenPushed = true
  45. }
  46. required init?(coder _: NSCoder) {
  47. fatalError("init(coder:) has not been implemented")
  48. }
  49. override func viewDidLoad() {
  50. messagesCollectionView.register(CustomMessageCell.self)
  51. super.viewDidLoad()
  52. if !DcConfig.configured {
  53. // TODO: display message about nothing being configured
  54. return
  55. }
  56. configureMessageCollectionView()
  57. if !disableWriting {
  58. configureMessageInputBar()
  59. messageInputBar.inputTextView.text = textDraft
  60. messageInputBar.inputTextView.becomeFirstResponder()
  61. }
  62. loadFirstMessages()
  63. }
  64. override func viewWillAppear(_ animated: Bool) {
  65. super.viewWillAppear(animated)
  66. // this will be removed in viewWillDisappear
  67. navigationController?.navigationBar.addGestureRecognizer(navBarTap)
  68. let chat = DcChat(id: chatId)
  69. if showCustomNavBar {
  70. let titleView = ChatTitleView()
  71. var subtitle = "ErrSubtitle"
  72. let chatContactIds = chat.contactIds
  73. if chat.isGroup {
  74. subtitle = String.localizedStringWithFormat(NSLocalizedString("n_members", comment: ""), chatContactIds.count)
  75. } else if chatContactIds.count >= 1 {
  76. if chat.isDeviceTalk {
  77. subtitle = String.localized("device_talk_subtitle")
  78. } else if chat.isSelfTalk {
  79. subtitle = String.localized("chat_self_talk_subtitle")
  80. } else {
  81. subtitle = DcContact(id: chatContactIds[0]).email
  82. }
  83. }
  84. titleView.updateTitleView(title: chat.name, subtitle: subtitle)
  85. navigationItem.titleView = titleView
  86. let badge: InitialsBadge
  87. if let image = chat.profileImage {
  88. badge = InitialsBadge(image: image, size: 28)
  89. } else {
  90. badge = InitialsBadge(name: chat.name, color: chat.color, size: 28)
  91. }
  92. badge.setVerified(chat.isVerified)
  93. navigationItem.rightBarButtonItem = UIBarButtonItem(customView: badge)
  94. }
  95. configureMessageMenu()
  96. let nc = NotificationCenter.default
  97. msgChangedObserver = nc.addObserver(
  98. forName: dcNotificationChanged,
  99. object: nil,
  100. queue: OperationQueue.main
  101. ) { notification in
  102. if let ui = notification.userInfo {
  103. if self.disableWriting {
  104. // always refresh, as we can't check currently
  105. self.refreshMessages()
  106. } else if let id = ui["message_id"] as? Int {
  107. if id > 0 {
  108. self.updateMessage(id)
  109. } else {
  110. // change might be a deletion
  111. self.refreshMessages()
  112. }
  113. }
  114. }
  115. }
  116. incomingMsgObserver = nc.addObserver(
  117. forName: dcNotificationIncoming,
  118. object: nil, queue: OperationQueue.main
  119. ) { notification in
  120. if let ui = notification.userInfo {
  121. if self.chatId == ui["chat_id"] as? Int {
  122. if let id = ui["message_id"] as? Int {
  123. if id > 0 {
  124. self.insertMessage(DcMsg(id: id))
  125. }
  126. }
  127. }
  128. }
  129. }
  130. }
  131. override func viewDidAppear(_ animated: Bool) {
  132. super.viewDidAppear(animated)
  133. // things that do not affect the chatview
  134. // and are delayed after the view is displayed
  135. dcContext.marknoticedChat(chatId: chatId)
  136. let array = DcArray(arrayPointer: dc_get_fresh_msgs(mailboxPointer))
  137. UIApplication.shared.applicationIconBadgeNumber = array.count
  138. }
  139. override func viewWillDisappear(_ animated: Bool) {
  140. super.viewWillDisappear(animated)
  141. // the navigationController will be used when chatDetail is pushed, so we have to remove that gestureRecognizer
  142. navigationController?.navigationBar.removeGestureRecognizer(navBarTap)
  143. }
  144. override func viewDidDisappear(_ animated: Bool) {
  145. super.viewDidDisappear(animated)
  146. setTextDraft()
  147. let nc = NotificationCenter.default
  148. if let msgChangedObserver = self.msgChangedObserver {
  149. nc.removeObserver(msgChangedObserver)
  150. }
  151. if let incomingMsgObserver = self.incomingMsgObserver {
  152. nc.removeObserver(incomingMsgObserver)
  153. }
  154. audioController.stopAnyOngoingPlaying()
  155. }
  156. @objc
  157. private func loadMoreMessages() {
  158. DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
  159. DispatchQueue.main.async {
  160. self.messageList = self.getMessageIds(self.loadCount, from: self.messageList.count) + self.messageList
  161. self.messagesCollectionView.reloadDataAndKeepOffset()
  162. self.refreshControl.endRefreshing()
  163. }
  164. }
  165. }
  166. @objc
  167. private func refreshMessages() {
  168. DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
  169. DispatchQueue.main.async {
  170. self.messageList = self.getMessageIds(self.messageList.count)
  171. self.messagesCollectionView.reloadDataAndKeepOffset()
  172. self.refreshControl.endRefreshing()
  173. if self.isLastSectionVisible() {
  174. self.messagesCollectionView.scrollToBottom(animated: true)
  175. }
  176. }
  177. }
  178. }
  179. private func loadFirstMessages() {
  180. DispatchQueue.global(qos: .userInitiated).async {
  181. DispatchQueue.main.async {
  182. self.messageList = self.getMessageIds(self.loadCount)
  183. self.messagesCollectionView.reloadData()
  184. self.refreshControl.endRefreshing()
  185. self.messagesCollectionView.scrollToBottom(animated: false)
  186. }
  187. }
  188. }
  189. private var textDraft: String? {
  190. if let draft = dc_get_draft(mailboxPointer, UInt32(chatId)) {
  191. if let cString = dc_msg_get_text(draft) {
  192. let swiftString = String(cString: cString)
  193. dc_str_unref(cString)
  194. dc_msg_unref(draft)
  195. return swiftString
  196. }
  197. dc_msg_unref(draft)
  198. return nil
  199. }
  200. return nil
  201. }
  202. private func getMessageIds(_ count: Int, from: Int? = nil) -> [DcMsg] {
  203. let cMessageIds = dc_get_chat_msgs(mailboxPointer, UInt32(chatId), 0, 0)
  204. let ids: [Int]
  205. if let from = from {
  206. ids = Utils.copyAndFreeArrayWithOffset(inputArray: cMessageIds, len: count, skipEnd: from)
  207. } else {
  208. ids = Utils.copyAndFreeArrayWithLen(inputArray: cMessageIds, len: count)
  209. }
  210. let markIds: [UInt32] = ids.map { UInt32($0) }
  211. dc_markseen_msgs(mailboxPointer, UnsafePointer(markIds), Int32(ids.count))
  212. return ids.map {
  213. DcMsg(id: $0)
  214. }
  215. }
  216. private func setTextDraft() {
  217. if let text = self.messageInputBar.inputTextView.text {
  218. let draft = dc_msg_new(mailboxPointer, DC_MSG_TEXT)
  219. dc_msg_set_text(draft, text.cString(using: .utf8))
  220. dc_set_draft(mailboxPointer, UInt32(chatId), draft)
  221. // cleanup
  222. dc_msg_unref(draft)
  223. }
  224. }
  225. private func configureMessageMenu() {
  226. var menuItems: [UIMenuItem]
  227. if disableWriting {
  228. menuItems = [
  229. UIMenuItem(title: String.localized("start_chat"), action: #selector(MessageCollectionViewCell.messageStartChat(_:))),
  230. UIMenuItem(title: String.localized("delete"), action: #selector(MessageCollectionViewCell.messageDelete(_:))),
  231. UIMenuItem(title: String.localized("menu_block_contact"), action: #selector(MessageCollectionViewCell.messageBlock(_:))),
  232. ]
  233. } else {
  234. // Configures the UIMenu which is shown when selecting a message
  235. menuItems = [
  236. UIMenuItem(title: String.localized("info"), action: #selector(MessageCollectionViewCell.messageInfo(_:))),
  237. UIMenuItem(title: String.localized("delete"), action: #selector(MessageCollectionViewCell.messageDelete(_:)))
  238. ]
  239. }
  240. UIMenuController.shared.menuItems = menuItems
  241. }
  242. private func configureMessageCollectionView() {
  243. messagesCollectionView.messagesDataSource = self
  244. messagesCollectionView.messageCellDelegate = self
  245. scrollsToBottomOnKeyboardBeginsEditing = true // default false
  246. maintainPositionOnKeyboardFrameChanged = true // default false
  247. messagesCollectionView.backgroundColor = DcColors.chatBackgroundColor
  248. messagesCollectionView.addSubview(refreshControl)
  249. refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged)
  250. let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
  251. layout?.sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 2, right: 8)
  252. // Hide the outgoing avatar and adjust the label alignment to line up with the messages
  253. layout?.setMessageOutgoingAvatarSize(.zero)
  254. layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right,
  255. textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
  256. layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right,
  257. textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
  258. // Set outgoing avatar to overlap with the message bubble
  259. layout?.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left,
  260. textInsets: UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 0)))
  261. layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30))
  262. layout?.setMessageIncomingMessagePadding(UIEdgeInsets(
  263. top: 0, left: -18, bottom: 0, right: 0))
  264. layout?.setMessageIncomingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .left,
  265. textInsets: UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 0)))
  266. layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30))
  267. layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0))
  268. layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30))
  269. layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8))
  270. messagesCollectionView.messagesLayoutDelegate = self
  271. messagesCollectionView.messagesDisplayDelegate = self
  272. }
  273. private func configureMessageInputBar() {
  274. messageInputBar.delegate = self
  275. messageInputBar.inputTextView.tintColor = DcColors.primary
  276. messageInputBar.inputTextView.placeholder = String.localized("chat_input_placeholder")
  277. messageInputBar.separatorLine.isHidden = true
  278. messageInputBar.inputTextView.tintColor = DcColors.primary
  279. messageInputBar.inputTextView.textColor = DcColors.defaultTextColor
  280. messageInputBar.backgroundView.backgroundColor = DcColors.chatBackgroundColor
  281. scrollsToBottomOnKeyboardBeginsEditing = true
  282. messageInputBar.inputTextView.backgroundColor = DcColors.inputFieldColor
  283. messageInputBar.inputTextView.placeholderTextColor = DcColors.placeholderColor
  284. messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 38)
  285. messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 38)
  286. messageInputBar.inputTextView.layer.borderColor = UIColor.themeColor(light: UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1),
  287. dark: UIColor(red: 55 / 255, green: 55/255, blue: 55/255, alpha: 1)).cgColor
  288. messageInputBar.inputTextView.layer.borderWidth = 1.0
  289. messageInputBar.inputTextView.layer.cornerRadius = 16.0
  290. messageInputBar.inputTextView.layer.masksToBounds = true
  291. messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
  292. configureInputBarItems()
  293. }
  294. private func configureInputBarItems() {
  295. messageInputBar.setLeftStackViewWidthConstant(to: 30, animated: false)
  296. messageInputBar.setRightStackViewWidthConstant(to: 30, animated: false)
  297. let sendButtonImage = UIImage(named: "paper_plane")?.withRenderingMode(.alwaysTemplate)
  298. messageInputBar.sendButton.image = sendButtonImage
  299. messageInputBar.sendButton.title = nil
  300. messageInputBar.sendButton.tintColor = UIColor(white: 1, alpha: 1)
  301. messageInputBar.sendButton.layer.cornerRadius = 15
  302. messageInputBar.middleContentViewPadding = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 10)
  303. // this adds a padding between textinputfield and send button
  304. messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
  305. messageInputBar.sendButton.setSize(CGSize(width: 30, height: 30), animated: false)
  306. let leftItems = [
  307. InputBarButtonItem()
  308. .configure {
  309. $0.spacing = .fixed(0)
  310. let clipperIcon = #imageLiteral(resourceName: "ic_attach_file_36pt").withRenderingMode(.alwaysTemplate)
  311. $0.image = clipperIcon
  312. $0.tintColor = DcColors.colorDisabled
  313. $0.setSize(CGSize(width: 30, height: 30), animated: false)
  314. }.onSelected {
  315. $0.tintColor = DcColors.primary
  316. }.onDeselected {
  317. $0.tintColor = DcColors.colorDisabled
  318. }.onTouchUpInside { _ in
  319. self.clipperButtonPressed()
  320. }
  321. ]
  322. messageInputBar.setStackViewItems(leftItems, forStack: .left, animated: false)
  323. // This just adds some more flare
  324. messageInputBar.sendButton
  325. .onEnabled { item in
  326. UIView.animate(withDuration: 0.3, animations: {
  327. item.backgroundColor = DcColors.primary
  328. })
  329. }.onDisabled { item in
  330. UIView.animate(withDuration: 0.3, animations: {
  331. item.backgroundColor = DcColors.colorDisabled
  332. })
  333. }
  334. }
  335. @objc private func chatProfilePressed() {
  336. coordinator?.showChatDetail(chatId: chatId)
  337. }
  338. // MARK: - UICollectionViewDataSource
  339. public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  340. guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
  341. fatalError("notMessagesCollectionView")
  342. }
  343. guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
  344. fatalError("nilMessagesDataSource")
  345. }
  346. let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
  347. switch message.kind {
  348. case .text, .attributedText, .emoji:
  349. let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
  350. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  351. return cell
  352. case .photo, .video:
  353. let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
  354. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  355. return cell
  356. case .photoText, .videoText, .fileText:
  357. let cell = messagesCollectionView.dequeueReusableCell(TextMediaMessageCell.self, for: indexPath)
  358. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  359. return cell
  360. case .location:
  361. let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
  362. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  363. return cell
  364. case .contact:
  365. let cell = messagesCollectionView.dequeueReusableCell(ContactMessageCell.self, for: indexPath)
  366. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  367. return cell
  368. case .custom:
  369. let cell = messagesCollectionView.dequeueReusableCell(CustomMessageCell.self, for: indexPath)
  370. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  371. return cell
  372. case .audio:
  373. let cell = messagesCollectionView.dequeueReusableCell(AudioMessageCell.self, for: indexPath)
  374. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  375. return cell
  376. }
  377. }
  378. override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
  379. if action == NSSelectorFromString("messageInfo:") ||
  380. action == NSSelectorFromString("messageDelete:") ||
  381. action == NSSelectorFromString("messageBlock:") ||
  382. action == NSSelectorFromString("messageStartChat:") {
  383. return true
  384. } else {
  385. return super.collectionView(collectionView, canPerformAction: action, forItemAt: indexPath, withSender: sender)
  386. }
  387. }
  388. override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
  389. switch action {
  390. case NSSelectorFromString("messageInfo:"):
  391. let msg = messageList[indexPath.section]
  392. logger.info("message: View info \(msg.messageId)")
  393. let msgViewController = MessageInfoViewController(dcContext: dcContext, message: msg)
  394. if let ctrl = navigationController {
  395. ctrl.pushViewController(msgViewController, animated: true)
  396. }
  397. case NSSelectorFromString("messageDelete:"):
  398. let msg = messageList[indexPath.section]
  399. logger.info("message: delete \(msg.messageId)")
  400. askToDeleteMessage(id: msg.id)
  401. case NSSelectorFromString("messageStartChat:"):
  402. let msg = messageList[indexPath.section]
  403. logger.info("message: Start Chat \(msg.messageId)")
  404. let chat = msg.createChat()
  405. // TODO: figure out how to properly show the chat after creation
  406. refreshMessages()
  407. coordinator?.showChat(chatId: chat.id)
  408. case NSSelectorFromString("messageBlock:"):
  409. let msg = messageList[indexPath.section]
  410. logger.info("message: Block \(msg.messageId)")
  411. msg.fromContact.block()
  412. refreshMessages()
  413. default:
  414. super.collectionView(collectionView, performAction: action, forItemAt: indexPath, withSender: sender)
  415. }
  416. }
  417. private func askToChatWith(email: String) {
  418. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), email),
  419. message: nil,
  420. preferredStyle: .actionSheet)
  421. alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
  422. self.dismiss(animated: true, completion: nil)
  423. let contactId = self.dcContext.createContact(name: "", email: email)
  424. let chatId = self.dcContext.createChat(contactId: contactId)
  425. self.coordinator?.showChat(chatId: chatId)
  426. }))
  427. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
  428. self.dismiss(animated: true, completion: nil)
  429. }))
  430. present(alert, animated: true, completion: nil)
  431. }
  432. private func askToDeleteMessage(id: Int) {
  433. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("ask_delete_messages"), 1),
  434. message: nil,
  435. preferredStyle: .actionSheet)
  436. alert.addAction(UIAlertAction(title: String.localized("delete"), style: .destructive, handler: { _ in
  437. self.dcContext.deleteMessage(msgId: id)
  438. self.dismiss(animated: true, completion: nil)
  439. }))
  440. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
  441. self.dismiss(animated: true, completion: nil)
  442. }))
  443. present(alert, animated: true, completion: nil)
  444. }
  445. }
  446. // MARK: - MessagesDataSource
  447. extension ChatViewController: MessagesDataSource {
  448. func numberOfSections(in _: MessagesCollectionView) -> Int {
  449. return messageList.count
  450. }
  451. func currentSender() -> SenderType {
  452. let currentSender = Sender(senderId: "1", displayName: "Alice")
  453. return currentSender
  454. }
  455. func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType {
  456. return messageList[indexPath.section]
  457. }
  458. func avatar(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> Avatar {
  459. let message = messageList[indexPath.section]
  460. let contact = message.fromContact
  461. return Avatar(image: contact.profileImage, initials: Utils.getInitials(inputName: contact.displayName))
  462. }
  463. func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
  464. if isInfoMessage(at: indexPath) {
  465. return nil
  466. }
  467. if isTimeLabelVisible(at: indexPath) {
  468. return NSAttributedString(
  469. string: MessageKitDateFormatter.shared.string(from: message.sentDate),
  470. attributes: [
  471. NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
  472. NSAttributedString.Key.foregroundColor: DcColors.grayTextColor,
  473. ]
  474. )
  475. }
  476. return nil
  477. }
  478. func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
  479. var attributedString: NSMutableAttributedString?
  480. if !isPreviousMessageSameSender(at: indexPath) {
  481. let name = message.sender.displayName
  482. let m = messageList[indexPath.section]
  483. attributedString = NSMutableAttributedString(string: name, attributes: [
  484. .font: UIFont.systemFont(ofSize: 14),
  485. .foregroundColor: m.fromContact.color,
  486. ])
  487. }
  488. if isMessageForwarded(at: indexPath) {
  489. let forwardedString = NSMutableAttributedString(string: String.localized("forwarded_message"), attributes: [
  490. .font: UIFont.systemFont(ofSize: 14),
  491. .foregroundColor: DcColors.grayTextColor,
  492. ])
  493. if attributedString == nil {
  494. attributedString = forwardedString
  495. } else {
  496. attributedString?.append(NSAttributedString(string: "\n", attributes: nil))
  497. attributedString?.append(forwardedString)
  498. }
  499. }
  500. return attributedString
  501. }
  502. func isMessageForwarded(at indexPath: IndexPath) -> Bool {
  503. let m = messageList[indexPath.section]
  504. return m.isForwarded
  505. }
  506. func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
  507. guard indexPath.section + 1 < messageList.count else { return false }
  508. let messageA = messageList[indexPath.section]
  509. let messageB = messageList[indexPath.section + 1]
  510. if messageA.fromContactId == messageB.fromContactId {
  511. return false
  512. }
  513. let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)
  514. let dateA = messageA.sentDate
  515. let dateB = messageB.sentDate
  516. let dayA = (calendar?.component(.day, from: dateA))
  517. let dayB = (calendar?.component(.day, from: dateB))
  518. return dayA != dayB
  519. }
  520. func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
  521. guard indexPath.section - 1 >= 0 else { return false }
  522. let messageA = messageList[indexPath.section - 1]
  523. let messageB = messageList[indexPath.section]
  524. if messageA.isInfo {
  525. return false
  526. }
  527. return messageA.fromContactId == messageB.fromContactId
  528. }
  529. func isInfoMessage(at indexPath: IndexPath) -> Bool {
  530. return messageList[indexPath.section].isInfo
  531. }
  532. func isImmediateNextMessageSameSender(at indexPath: IndexPath) -> Bool {
  533. guard indexPath.section + 1 < messageList.count else { return false }
  534. let messageA = messageList[indexPath.section]
  535. let messageB = messageList[indexPath.section + 1]
  536. if messageA.isInfo {
  537. return false
  538. }
  539. let dateA = messageA.sentDate
  540. let dateB = messageB.sentDate
  541. let timeinterval = dateB.timeIntervalSince(dateA)
  542. let minute = 60.0
  543. return messageA.fromContactId == messageB.fromContactId && timeinterval.isLessThanOrEqualTo(minute)
  544. }
  545. func isAvatarHidden(at indexPath: IndexPath) -> Bool {
  546. let message = messageList[indexPath.section]
  547. return isNextMessageSameSender(at: indexPath) || message.isInfo
  548. }
  549. func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
  550. guard indexPath.section + 1 < messageList.count else { return false }
  551. let messageA = messageList[indexPath.section]
  552. let messageB = messageList[indexPath.section + 1]
  553. if messageA.isInfo {
  554. return false
  555. }
  556. return messageA.fromContactId == messageB.fromContactId
  557. }
  558. func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
  559. guard indexPath.section < messageList.count else { return nil }
  560. let m = messageList[indexPath.section]
  561. if m.isInfo || isImmediateNextMessageSameSender(at: indexPath) {
  562. return nil
  563. }
  564. let timestampAttributes: [NSAttributedString.Key: Any] = [
  565. .font: UIFont.systemFont(ofSize: 12),
  566. .foregroundColor: UIColor.lightGray,
  567. ]
  568. if isFromCurrentSender(message: message) {
  569. let text = NSMutableAttributedString()
  570. text.append(NSAttributedString(string: m.formattedSentDate(), attributes: timestampAttributes))
  571. // TODO: this should be replaced by the respective icons,
  572. // for accessibility, the a11y strings should be added
  573. var stateDescription: String
  574. switch Int32(m.state) {
  575. case DC_STATE_OUT_PENDING:
  576. stateDescription = "Pending"
  577. case DC_STATE_OUT_DELIVERED:
  578. stateDescription = "Sent"
  579. case DC_STATE_OUT_MDN_RCVD:
  580. stateDescription = "Read"
  581. case DC_STATE_OUT_FAILED:
  582. stateDescription = "Failed"
  583. default:
  584. stateDescription = "Unknown"
  585. }
  586. text.append(NSAttributedString(
  587. string: " - " + stateDescription,
  588. attributes: [
  589. .font: UIFont.systemFont(ofSize: 12),
  590. .foregroundColor: DcColors.defaultTextColor,
  591. ]
  592. ))
  593. return text
  594. }
  595. if !isAvatarHidden(at: indexPath) {
  596. let text = NSMutableAttributedString()
  597. text.append(NSAttributedString(string: " "))
  598. text.append(NSAttributedString(string: m.formattedSentDate(), attributes: timestampAttributes))
  599. return text
  600. }
  601. return NSAttributedString(string: m.formattedSentDate(), attributes: timestampAttributes)
  602. }
  603. func updateMessage(_ messageId: Int) {
  604. if let index = messageList.firstIndex(where: { $0.id == messageId }) {
  605. dc_markseen_msgs(mailboxPointer, UnsafePointer([UInt32(messageId)]), 1)
  606. messageList[index] = DcMsg(id: messageId)
  607. // Reload section to update header/footer labels
  608. messagesCollectionView.performBatchUpdates({
  609. messagesCollectionView.reloadSections([index])
  610. if index > 0 {
  611. messagesCollectionView.reloadSections([index - 1])
  612. }
  613. if index < messageList.count - 1 {
  614. messagesCollectionView.reloadSections([index + 1])
  615. }
  616. }, completion: { [weak self] _ in
  617. if self?.isLastSectionVisible() == true {
  618. self?.messagesCollectionView.scrollToBottom(animated: true)
  619. }
  620. })
  621. } else {
  622. let msg = DcMsg(id: messageId)
  623. if msg.chatId == chatId {
  624. insertMessage(msg)
  625. }
  626. }
  627. }
  628. func insertMessage(_ message: DcMsg) {
  629. dc_markseen_msgs(mailboxPointer, UnsafePointer([UInt32(message.id)]), 1)
  630. messageList.append(message)
  631. // Reload last section to update header/footer labels and insert a new one
  632. messagesCollectionView.performBatchUpdates({
  633. messagesCollectionView.insertSections([messageList.count - 1])
  634. if messageList.count >= 2 {
  635. messagesCollectionView.reloadSections([messageList.count - 2])
  636. }
  637. }, completion: { [weak self] _ in
  638. if self?.isLastSectionVisible() == true {
  639. self?.messagesCollectionView.scrollToBottom(animated: true)
  640. }
  641. })
  642. }
  643. func isLastSectionVisible() -> Bool {
  644. guard !messageList.isEmpty else { return false }
  645. let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1)
  646. return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath)
  647. }
  648. }
  649. // MARK: - MessagesDisplayDelegate
  650. extension ChatViewController: MessagesDisplayDelegate {
  651. // MARK: - Text Messages
  652. func textColor(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
  653. return DcColors.defaultTextColor
  654. }
  655. // MARK: - All Messages
  656. func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
  657. return isFromCurrentSender(message: message) ? DcColors.messagePrimaryColor : DcColors.messageSecondaryColor
  658. }
  659. func messageStyle(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
  660. if isInfoMessage(at: indexPath) {
  661. return .custom { view in
  662. view.style = .none
  663. view.backgroundColor = DcColors.systemMessageBackgroundColor
  664. let radius: CGFloat = 16
  665. let path = UIBezierPath(roundedRect: view.bounds,
  666. byRoundingCorners: UIRectCorner.allCorners,
  667. cornerRadii: CGSize(width: radius, height: radius))
  668. let mask = CAShapeLayer()
  669. mask.path = path.cgPath
  670. view.layer.mask = mask
  671. view.center.x = self.view.center.x
  672. }
  673. }
  674. var corners: UIRectCorner = []
  675. if isFromCurrentSender(message: message) {
  676. corners.formUnion(.topLeft)
  677. corners.formUnion(.bottomLeft)
  678. if !isPreviousMessageSameSender(at: indexPath) {
  679. corners.formUnion(.topRight)
  680. }
  681. if !isNextMessageSameSender(at: indexPath) {
  682. corners.formUnion(.bottomRight)
  683. }
  684. } else {
  685. corners.formUnion(.topRight)
  686. corners.formUnion(.bottomRight)
  687. if !isPreviousMessageSameSender(at: indexPath) {
  688. corners.formUnion(.topLeft)
  689. }
  690. if !isNextMessageSameSender(at: indexPath) {
  691. corners.formUnion(.bottomLeft)
  692. }
  693. }
  694. return .custom { view in
  695. let radius: CGFloat = 16
  696. let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
  697. let mask = CAShapeLayer()
  698. mask.path = path.cgPath
  699. view.layer.mask = mask
  700. }
  701. }
  702. func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) {
  703. let message = messageList[indexPath.section]
  704. let contact = message.fromContact
  705. let avatar = Avatar(image: contact.profileImage, initials: Utils.getInitials(inputName: contact.displayName))
  706. avatarView.set(avatar: avatar)
  707. avatarView.isHidden = isAvatarHidden(at: indexPath)
  708. avatarView.backgroundColor = contact.color
  709. }
  710. func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
  711. return [.url, .phoneNumber]
  712. }
  713. func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
  714. return [ NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor,
  715. NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
  716. NSAttributedString.Key.underlineColor: DcColors.defaultTextColor ]
  717. }
  718. }
  719. // MARK: - MessagesLayoutDelegate
  720. extension ChatViewController: MessagesLayoutDelegate {
  721. func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
  722. if isTimeLabelVisible(at: indexPath) {
  723. return 18
  724. }
  725. return 0
  726. }
  727. func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
  728. if isInfoMessage(at: indexPath) {
  729. return 0
  730. }
  731. if !isPreviousMessageSameSender(at: indexPath) {
  732. return 40
  733. } else if isMessageForwarded(at: indexPath) {
  734. return 20
  735. }
  736. return 0
  737. }
  738. func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
  739. if isInfoMessage(at: indexPath) {
  740. return 0
  741. }
  742. if !isImmediateNextMessageSameSender(at: indexPath) {
  743. return 16
  744. }
  745. return 0
  746. }
  747. func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat {
  748. return 40
  749. }
  750. func footerViewSize(for _: MessageType, at _: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
  751. return CGSize(width: messagesCollectionView.bounds.width, height: 20)
  752. }
  753. @objc private func clipperButtonPressed() {
  754. showClipperOptions()
  755. }
  756. private func showClipperOptions() {
  757. let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
  758. let photoAction = PhotoPickerAlertAction(title: String.localized("camera"), style: .default, handler: photoButtonPressed(_:))
  759. let videoAction = PhotoPickerAlertAction(title: String.localized("video"), style: .default, handler: videoButtonPressed(_:))
  760. alert.addAction(photoAction)
  761. alert.addAction(videoAction)
  762. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  763. self.present(alert, animated: true, completion: nil)
  764. }
  765. private func photoButtonPressed(_ action: UIAlertAction) {
  766. coordinator?.showCameraViewController()
  767. }
  768. private func videoButtonPressed(_ action: UIAlertAction) {
  769. coordinator?.showVideoLibrary()
  770. }
  771. }
  772. // MARK: - MessageCellDelegate
  773. extension ChatViewController: MessageCellDelegate {
  774. @objc func didTapMessage(in cell: MessageCollectionViewCell) {
  775. if let indexPath = messagesCollectionView.indexPath(for: cell) {
  776. let message = messageList[indexPath.section]
  777. if message.isSetupMessage {
  778. didTapAsm(msg: message, orgText: "")
  779. } else if let url = message.fileURL {
  780. // find all other messages with same message type
  781. var previousUrls: [URL] = []
  782. var nextUrls: [URL] = []
  783. var prev: Int = Int(dc_get_next_media(mailboxPointer, UInt32(message.id), -1, Int32(message.type), 0, 0))
  784. while prev != 0 {
  785. let prevMessage = DcMsg(id: prev)
  786. if let url = prevMessage.fileURL {
  787. previousUrls.insert(url, at: 0)
  788. }
  789. prev = Int(dc_get_next_media(mailboxPointer, UInt32(prevMessage.id), -1, Int32(prevMessage.type), 0, 0))
  790. }
  791. var next: Int = Int(dc_get_next_media(mailboxPointer, UInt32(message.id), 1, Int32(message.type), 0, 0))
  792. while next != 0 {
  793. let nextMessage = DcMsg(id: next)
  794. if let url = nextMessage.fileURL {
  795. nextUrls.insert(url, at: 0)
  796. }
  797. next = Int(dc_get_next_media(mailboxPointer, UInt32(nextMessage.id), 1, Int32(nextMessage.type), 0, 0))
  798. }
  799. // these are the files user will be able to swipe trough
  800. let mediaUrls: [URL] = previousUrls + [url] + nextUrls
  801. previewController = PreviewController(currentIndex: previousUrls.count, urls: mediaUrls)
  802. present(previewController!.qlController, animated: true)
  803. }
  804. }
  805. }
  806. private func didTapAsm(msg: DcMsg, orgText: String) {
  807. let inputDlg = UIAlertController(
  808. title: String.localized("autocrypt_continue_transfer_title"),
  809. message: String.localized("autocrypt_continue_transfer_please_enter_code"),
  810. preferredStyle: .alert)
  811. inputDlg.addTextField(configurationHandler: { (textField) in
  812. textField.placeholder = msg.setupCodeBegin + ".."
  813. textField.text = orgText
  814. textField.keyboardType = UIKeyboardType.numbersAndPunctuation // allows entering spaces; decimalPad would require a mask to keep things readable
  815. })
  816. inputDlg.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  817. let okAction = UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  818. let textField = inputDlg.textFields![0]
  819. let modText = textField.text ?? ""
  820. let success = self.dcContext.continueKeyTransfer(msgId: msg.id, setupCode: modText)
  821. let alert = UIAlertController(
  822. title: String.localized("autocrypt_continue_transfer_title"),
  823. message: String.localized(success ? "autocrypt_continue_transfer_succeeded" : "autocrypt_bad_setup_code"),
  824. preferredStyle: .alert)
  825. if success {
  826. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  827. } else {
  828. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  829. let retryAction = UIAlertAction(title: String.localized("autocrypt_continue_transfer_retry"), style: .default, handler: { _ in
  830. self.didTapAsm(msg: msg, orgText: modText)
  831. })
  832. alert.addAction(retryAction)
  833. alert.preferredAction = retryAction
  834. }
  835. self.navigationController?.present(alert, animated: true, completion: nil)
  836. })
  837. inputDlg.addAction(okAction)
  838. inputDlg.preferredAction = okAction // without setting preferredAction, cancel become shown *bold* as the preferred action
  839. navigationController?.present(inputDlg, animated: true, completion: nil)
  840. }
  841. @objc func didTapAvatar(in cell: MessageCollectionViewCell) {
  842. if let indexPath = messagesCollectionView.indexPath(for: cell) {
  843. let message = messageList[indexPath.section]
  844. let chat = DcChat(id: chatId)
  845. coordinator?.showContactDetail(of: message.fromContact.id, in: chat.chatType)
  846. }
  847. }
  848. @objc(didTapCellTopLabelIn:) func didTapCellTopLabel(in _: MessageCollectionViewCell) {
  849. logger.info("Top label tapped")
  850. }
  851. @objc(didTapCellBottomLabelIn:) func didTapCellBottomLabel(in _: MessageCollectionViewCell) {
  852. print("Bottom label tapped")
  853. }
  854. func didTapPlayButton(in cell: AudioMessageCell) {
  855. guard let indexPath = messagesCollectionView.indexPath(for: cell),
  856. let message = messagesCollectionView.messagesDataSource?.messageForItem(at: indexPath, in: messagesCollectionView) else {
  857. print("Failed to identify message when audio cell receive tap gesture")
  858. return
  859. }
  860. guard audioController.state != .stopped else {
  861. // There is no audio sound playing - prepare to start playing for given audio message
  862. audioController.playSound(for: message, in: cell)
  863. return
  864. }
  865. if audioController.playingMessage?.messageId == message.messageId {
  866. // tap occur in the current cell that is playing audio sound
  867. if audioController.state == .playing {
  868. audioController.pauseSound(for: message, in: cell)
  869. } else {
  870. audioController.resumeSound()
  871. }
  872. } else {
  873. // tap occur in a difference cell that the one is currently playing sound. First stop currently playing and start the sound for given message
  874. audioController.stopAnyOngoingPlaying()
  875. audioController.playSound(for: message, in: cell)
  876. }
  877. }
  878. func didStartAudio(in cell: AudioMessageCell) {
  879. print("audio started")
  880. }
  881. func didStopAudio(in cell: AudioMessageCell) {
  882. print("audio stopped")
  883. }
  884. func didPauseAudio(in cell: AudioMessageCell) {
  885. print("audio paused")
  886. }
  887. @objc func didTapBackground(in cell: MessageCollectionViewCell) {
  888. print("background of message tapped")
  889. }
  890. }
  891. // MARK: - MessageLabelDelegate
  892. extension ChatViewController: MessageLabelDelegate {
  893. func didSelectPhoneNumber(_ phoneNumber: String) {
  894. logger.info("phone open", phoneNumber)
  895. if let escapedPhoneNumber = phoneNumber.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
  896. if let url = NSURL(string: "tel:\(escapedPhoneNumber)") {
  897. UIApplication.shared.open(url as URL)
  898. }
  899. }
  900. }
  901. func didSelectURL(_ url: URL) {
  902. if Utils.isEmail(url: url) {
  903. print("tapped on contact")
  904. let email = Utils.getEmailFrom(url)
  905. self.askToChatWith(email: email)
  906. ///TODO: implement handling
  907. } else {
  908. UIApplication.shared.open(url)
  909. }
  910. }
  911. }
  912. // MARK: - LocationMessageDisplayDelegate
  913. /*
  914. extension ChatViewController: LocationMessageDisplayDelegate {
  915. func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
  916. let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
  917. let pinImage = #imageLiteral(resourceName: "ic_block_36pt").withRenderingMode(.alwaysTemplate)
  918. annotationView.image = pinImage
  919. annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
  920. return annotationView
  921. }
  922. func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
  923. return { view in
  924. view.layer.transform = CATransform3DMakeScale(0, 0, 0)
  925. view.alpha = 0.0
  926. UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: {
  927. view.layer.transform = CATransform3DIdentity
  928. view.alpha = 1.0
  929. }, completion: nil)
  930. }
  931. }
  932. }
  933. */
  934. // MARK: - MessageInputBarDelegate
  935. extension ChatViewController: InputBarAccessoryViewDelegate {
  936. func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
  937. DispatchQueue.global().async {
  938. dc_send_text_msg(mailboxPointer, UInt32(self.chatId), text)
  939. }
  940. inputBar.inputTextView.text = String()
  941. }
  942. }
  943. /*
  944. extension ChatViewController: MessageInputBarDelegate {
  945. }
  946. */
  947. // MARK: - MessageCollectionViewCell
  948. extension MessageCollectionViewCell {
  949. @objc func messageDelete(_ sender: Any?) {
  950. // Get the collectionView
  951. if let collectionView = self.superview as? UICollectionView {
  952. // Get indexPath
  953. if let indexPath = collectionView.indexPath(for: self) {
  954. // Trigger action
  955. collectionView.delegate?.collectionView?(collectionView,
  956. performAction: #selector(MessageCollectionViewCell.messageDelete(_:)),
  957. forItemAt: indexPath, withSender: sender)
  958. }
  959. }
  960. }
  961. @objc func messageInfo(_ sender: Any?) {
  962. // Get the collectionView
  963. if let collectionView = self.superview as? UICollectionView {
  964. // Get indexPath
  965. if let indexPath = collectionView.indexPath(for: self) {
  966. // Trigger action
  967. collectionView.delegate?.collectionView?(collectionView,
  968. performAction: #selector(MessageCollectionViewCell.messageInfo(_:)),
  969. forItemAt: indexPath, withSender: sender)
  970. }
  971. }
  972. }
  973. @objc func messageBlock(_ sender: Any?) {
  974. // Get the collectionView
  975. if let collectionView = self.superview as? UICollectionView {
  976. // Get indexPath
  977. if let indexPath = collectionView.indexPath(for: self) {
  978. // Trigger action
  979. collectionView.delegate?.collectionView?(collectionView,
  980. performAction: #selector(MessageCollectionViewCell.messageBlock(_:)),
  981. forItemAt: indexPath, withSender: sender)
  982. }
  983. }
  984. }
  985. @objc func messageStartChat(_ sender: Any?) {
  986. // Get the collectionView
  987. if let collectionView = self.superview as? UICollectionView {
  988. // Get indexPath
  989. if let indexPath = collectionView.indexPath(for: self) {
  990. // Trigger action
  991. collectionView.delegate?.collectionView?(collectionView,
  992. performAction: #selector(MessageCollectionViewCell.messageStartChat(_:)),
  993. forItemAt: indexPath, withSender: sender)
  994. }
  995. }
  996. }
  997. }