ChatViewController.swift 45 KB

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