ChatViewController.swift 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942
  1. //
  2. // ChatViewController.swift
  3. // deltachat-ios
  4. //
  5. // Created by Bastian van de Wetering on 08.11.17.
  6. // Copyright © 2017 Jonas Reinsch. All rights reserved.
  7. //
  8. import ALCameraViewController
  9. import MapKit
  10. import MessageInputBar
  11. import MessageKit
  12. import QuickLook
  13. import UIKit
  14. class ChatViewController: MessagesViewController {
  15. weak var coordinator: ChatViewCoordinator?
  16. let outgoingAvatarOverlap: CGFloat = 17.5
  17. let loadCount = 30
  18. let chatId: Int
  19. let refreshControl = UIRefreshControl()
  20. var messageList: [MRMessage] = []
  21. var msgChangedObserver: Any?
  22. var incomingMsgObserver: Any?
  23. lazy var navBarTap: UITapGestureRecognizer = {
  24. return UITapGestureRecognizer(target: self, action: #selector(chatProfilePressed))
  25. }()
  26. var disableWriting = false
  27. var previewView: UIView?
  28. var previewController: PreviewController?
  29. init(chatId: Int, title: String? = nil) {
  30. self.chatId = chatId
  31. super.init(nibName: nil, bundle: nil)
  32. if let title = title {
  33. updateTitleView(title: title, subtitle: nil)
  34. }
  35. }
  36. required init?(coder _: NSCoder) {
  37. fatalError("init(coder:) has not been implemented")
  38. }
  39. override func viewDidLoad() {
  40. messagesCollectionView.register(CustomCell.self)
  41. super.viewDidLoad()
  42. view.backgroundColor = DCColors.chatBackgroundColor
  43. if !MRConfig.configured {
  44. // TODO: display message about nothing being configured
  45. return
  46. }
  47. configureMessageCollectionView()
  48. if !disableWriting {
  49. configureMessageInputBar()
  50. messageInputBar.inputTextView.text = textDraft
  51. messageInputBar.inputTextView.becomeFirstResponder()
  52. }
  53. loadFirstMessages()
  54. }
  55. override func viewWillAppear(_ animated: Bool) {
  56. super.viewWillAppear(animated)
  57. // this will be removed in viewWillDisappear
  58. navigationController?.navigationBar.addGestureRecognizer(navBarTap)
  59. let chat = MRChat(id: chatId)
  60. updateTitleView(title: chat.name, subtitle: chat.subtitle)
  61. if let image = chat.profileImage {
  62. navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, style: .done, target: self, action: #selector(chatProfilePressed))
  63. } else {
  64. let initialsLabel = InitialsLabel(name: chat.name, color: chat.color, size: 28)
  65. navigationItem.rightBarButtonItem = UIBarButtonItem(customView: initialsLabel)
  66. }
  67. configureMessageMenu()
  68. if #available(iOS 11.0, *) {
  69. if disableWriting {
  70. navigationController?.navigationBar.prefersLargeTitles = true
  71. }
  72. }
  73. let nc = NotificationCenter.default
  74. msgChangedObserver = nc.addObserver(
  75. forName: dcNotificationChanged,
  76. object: nil,
  77. queue: OperationQueue.main
  78. ) { notification in
  79. if let ui = notification.userInfo {
  80. if self.disableWriting {
  81. // always refresh, as we can't check currently
  82. self.refreshMessages()
  83. } else if let id = ui["message_id"] as? Int {
  84. if id > 0 {
  85. self.updateMessage(id)
  86. }
  87. }
  88. }
  89. }
  90. incomingMsgObserver = nc.addObserver(
  91. forName: dcNotificationIncoming,
  92. object: nil, queue: OperationQueue.main
  93. ) { notification in
  94. if let ui = notification.userInfo {
  95. if self.chatId == ui["chat_id"] as! Int {
  96. let id = ui["message_id"] as! Int
  97. if id > 0 {
  98. self.insertMessage(MRMessage(id: id))
  99. }
  100. }
  101. }
  102. }
  103. }
  104. override func viewWillDisappear(_ animated: Bool) {
  105. super.viewWillDisappear(animated)
  106. // the navigationController will be used when chatDetail is pushed, so we have to remove that gestureRecognizer
  107. navigationController?.navigationBar.removeGestureRecognizer(navBarTap)
  108. let cnt = Int(dc_get_fresh_msg_cnt(mailboxPointer, UInt32(chatId)))
  109. logger.info("updating count for chat \(cnt)")
  110. UIApplication.shared.applicationIconBadgeNumber = cnt
  111. if #available(iOS 11.0, *) {
  112. if disableWriting {
  113. navigationController?.navigationBar.prefersLargeTitles = false
  114. }
  115. }
  116. }
  117. override func viewDidDisappear(_ animated: Bool) {
  118. super.viewDidDisappear(animated)
  119. setTextDraft()
  120. let nc = NotificationCenter.default
  121. if let msgChangedObserver = self.msgChangedObserver {
  122. nc.removeObserver(msgChangedObserver)
  123. }
  124. if let incomingMsgObserver = self.incomingMsgObserver {
  125. nc.removeObserver(incomingMsgObserver)
  126. }
  127. }
  128. @objc
  129. private func loadMoreMessages() {
  130. DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
  131. DispatchQueue.main.async {
  132. self.messageList = self.getMessageIds(self.loadCount, from: self.messageList.count) + self.messageList
  133. self.messagesCollectionView.reloadDataAndKeepOffset()
  134. self.refreshControl.endRefreshing()
  135. }
  136. }
  137. }
  138. @objc
  139. private func refreshMessages() {
  140. DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
  141. DispatchQueue.main.async {
  142. self.messageList = self.getMessageIds(self.messageList.count)
  143. self.messagesCollectionView.reloadDataAndKeepOffset()
  144. self.refreshControl.endRefreshing()
  145. if self.isLastSectionVisible() {
  146. self.messagesCollectionView.scrollToBottom(animated: true)
  147. }
  148. }
  149. }
  150. }
  151. private func loadFirstMessages() {
  152. DispatchQueue.global(qos: .userInitiated).async {
  153. DispatchQueue.main.async {
  154. self.messageList = self.getMessageIds(self.loadCount)
  155. self.messagesCollectionView.reloadData()
  156. self.refreshControl.endRefreshing()
  157. self.messagesCollectionView.scrollToBottom(animated: false)
  158. }
  159. }
  160. }
  161. private var textDraft: String? {
  162. // FIXME: need to free pointer
  163. if let draft = dc_get_draft(mailboxPointer, UInt32(chatId)) {
  164. if let text = dc_msg_get_text(draft) {
  165. let s = String(validatingUTF8: text)!
  166. return s
  167. }
  168. return nil
  169. }
  170. return nil
  171. }
  172. private func getMessageIds(_ count: Int, from: Int? = nil) -> [MRMessage] {
  173. let cMessageIds = dc_get_chat_msgs(mailboxPointer, UInt32(chatId), 0, 0)
  174. let ids: [Int]
  175. if let from = from {
  176. ids = Utils.copyAndFreeArrayWithOffset(inputArray: cMessageIds, len: count, skipEnd: from)
  177. } else {
  178. ids = Utils.copyAndFreeArrayWithLen(inputArray: cMessageIds, len: count)
  179. }
  180. let markIds: [UInt32] = ids.map { UInt32($0) }
  181. dc_markseen_msgs(mailboxPointer, UnsafePointer(markIds), Int32(ids.count))
  182. return ids.map {
  183. MRMessage(id: $0)
  184. }
  185. }
  186. private func setTextDraft() {
  187. if let text = self.messageInputBar.inputTextView.text {
  188. let draft = dc_msg_new(mailboxPointer, DC_MSG_TEXT)
  189. dc_msg_set_text(draft, text.cString(using: .utf8))
  190. dc_set_draft(mailboxPointer, UInt32(chatId), draft)
  191. // cleanup
  192. dc_msg_unref(draft)
  193. }
  194. }
  195. override var inputAccessoryView: UIView? {
  196. if disableWriting {
  197. return nil
  198. }
  199. return messageInputBar
  200. }
  201. private func configureMessageMenu() {
  202. var menuItems: [UIMenuItem]
  203. if disableWriting {
  204. menuItems = [
  205. UIMenuItem(title: "Start Chat", action: #selector(MessageCollectionViewCell.messageStartChat(_:))),
  206. UIMenuItem(title: "Dismiss", action: #selector(MessageCollectionViewCell.messageDismiss(_:))),
  207. UIMenuItem(title: "Block", action: #selector(MessageCollectionViewCell.messageBlock(_:))),
  208. ]
  209. } else {
  210. // Configures the UIMenu which is shown when selecting a message
  211. menuItems = [
  212. UIMenuItem(title: "Info", action: #selector(MessageCollectionViewCell.messageInfo(_:))),
  213. ]
  214. }
  215. UIMenuController.shared.menuItems = menuItems
  216. }
  217. private func configureMessageCollectionView() {
  218. messagesCollectionView.messagesDataSource = self
  219. messagesCollectionView.messageCellDelegate = self
  220. scrollsToBottomOnKeyboardBeginsEditing = true // default false
  221. maintainPositionOnKeyboardFrameChanged = true // default false
  222. messagesCollectionView.addSubview(refreshControl)
  223. refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged)
  224. let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
  225. layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8)
  226. // Hide the outgoing avatar and adjust the label alignment to line up with the messages
  227. layout?.setMessageOutgoingAvatarSize(.zero)
  228. layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
  229. layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
  230. // Set outgoing avatar to overlap with the message bubble
  231. layout?.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0)))
  232. layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30))
  233. layout?.setMessageIncomingMessagePadding(UIEdgeInsets(top: -outgoingAvatarOverlap, left: -18, bottom: outgoingAvatarOverlap / 2, right: 18))
  234. layout?.setMessageIncomingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(top: -7, left: 38, bottom: 0, right: 0)))
  235. layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30))
  236. layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0))
  237. layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30))
  238. layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8))
  239. messagesCollectionView.messagesLayoutDelegate = self
  240. messagesCollectionView.messagesDisplayDelegate = self
  241. }
  242. private func configureMessageInputBar() {
  243. messageInputBar.delegate = self
  244. messageInputBar.inputTextView.tintColor = DCColors.primary
  245. messageInputBar.sendButton.tintColor = DCColors.primary
  246. messageInputBar.isTranslucent = true
  247. messageInputBar.separatorLine.isHidden = true
  248. messageInputBar.inputTextView.tintColor = DCColors.primary
  249. messageInputBar.delegate = self
  250. scrollsToBottomOnKeyboardBeginsEditing = true
  251. messageInputBar.inputTextView.backgroundColor = UIColor(red: 245 / 255, green: 245 / 255, blue: 245 / 255, alpha: 1)
  252. messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1)
  253. messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 38)
  254. messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 38)
  255. messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1).cgColor
  256. messageInputBar.inputTextView.layer.borderWidth = 1.0
  257. messageInputBar.inputTextView.layer.cornerRadius = 16.0
  258. messageInputBar.inputTextView.layer.masksToBounds = true
  259. messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
  260. configureInputBarItems()
  261. }
  262. private func configureInputBarItems() {
  263. messageInputBar.setLeftStackViewWidthConstant(to: 44, animated: false)
  264. messageInputBar.setRightStackViewWidthConstant(to: 30, animated: false)
  265. let sendButtonImage = UIImage(named: "paper_plane")?.withRenderingMode(.alwaysTemplate)
  266. messageInputBar.sendButton.image = sendButtonImage
  267. messageInputBar.sendButton.tintColor = UIColor(white: 1, alpha: 1)
  268. messageInputBar.sendButton.backgroundColor = UIColor(white: 0.9, alpha: 1)
  269. messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 0, bottom: 6, right: 0)
  270. messageInputBar.sendButton.setSize(CGSize(width: 30, height: 30), animated: false)
  271. messageInputBar.sendButton.title = nil
  272. messageInputBar.sendButton.layer.cornerRadius = 15
  273. messageInputBar.textViewPadding.right = -40
  274. let leftItems = [
  275. InputBarButtonItem()
  276. .configure {
  277. $0.spacing = .fixed(0)
  278. $0.image = UIImage(named: "camera")?.withRenderingMode(.alwaysTemplate)
  279. $0.setSize(CGSize(width: 36, height: 36), animated: false)
  280. $0.tintColor = UIColor(white: 0.8, alpha: 1)
  281. }.onSelected {
  282. $0.tintColor = DCColors.primary
  283. }.onDeselected {
  284. $0.tintColor = UIColor(white: 0.8, alpha: 1)
  285. }.onTouchUpInside { _ in
  286. self.didPressPhotoButton()
  287. },
  288. ]
  289. messageInputBar.setStackViewItems(leftItems, forStack: .left, animated: false)
  290. // This just adds some more flare
  291. messageInputBar.sendButton
  292. .onEnabled { item in
  293. UIView.animate(withDuration: 0.3, animations: {
  294. item.backgroundColor = DCColors.primary
  295. })
  296. }.onDisabled { item in
  297. UIView.animate(withDuration: 0.3, animations: {
  298. item.backgroundColor = UIColor(white: 0.9, alpha: 1)
  299. })
  300. }
  301. }
  302. @objc private func chatProfilePressed() {
  303. coordinator?.showChatDetail(chatId: chatId)
  304. }
  305. // MARK: - UICollectionViewDataSource
  306. public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  307. guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
  308. fatalError("notMessagesCollectionView")
  309. }
  310. guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
  311. fatalError("nilMessagesDataSource")
  312. }
  313. let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
  314. switch message.kind {
  315. case .text, .attributedText, .emoji:
  316. let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
  317. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  318. return cell
  319. case .photo, .video:
  320. let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
  321. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  322. return cell
  323. case .location:
  324. let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
  325. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  326. return cell
  327. case .custom:
  328. let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath)
  329. cell.configure(with: message, at: indexPath, and: messagesCollectionView)
  330. return cell
  331. }
  332. }
  333. override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
  334. if action == NSSelectorFromString("messageInfo:") ||
  335. action == NSSelectorFromString("messageBlock:") ||
  336. action == NSSelectorFromString("messageDismiss:") ||
  337. action == NSSelectorFromString("messageStartChat:") {
  338. return true
  339. } else {
  340. return super.collectionView(collectionView, canPerformAction: action, forItemAt: indexPath, withSender: sender)
  341. }
  342. }
  343. override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
  344. switch action {
  345. case NSSelectorFromString("messageInfo:"):
  346. let msg = messageList[indexPath.section]
  347. logger.info("message: View info \(msg.messageId)")
  348. let msgViewController = MessageInfoViewController(message: msg)
  349. if let ctrl = navigationController {
  350. ctrl.pushViewController(msgViewController, animated: true)
  351. }
  352. case NSSelectorFromString("messageStartChat:"):
  353. let msg = messageList[indexPath.section]
  354. logger.info("message: Start Chat \(msg.messageId)")
  355. _ = msg.createChat()
  356. // TODO: figure out how to properly show the chat after creation
  357. refreshMessages()
  358. case NSSelectorFromString("messageBlock:"):
  359. let msg = messageList[indexPath.section]
  360. logger.info("message: Block \(msg.messageId)")
  361. msg.fromContact.block()
  362. refreshMessages()
  363. case NSSelectorFromString("messageDismiss:"):
  364. let msg = messageList[indexPath.section]
  365. logger.info("message: Dismiss \(msg.messageId)")
  366. msg.fromContact.marknoticed()
  367. refreshMessages()
  368. default:
  369. super.collectionView(collectionView, performAction: action, forItemAt: indexPath, withSender: sender)
  370. }
  371. }
  372. }
  373. // MARK: - MessagesDataSource
  374. extension ChatViewController: MessagesDataSource {
  375. func numberOfSections(in _: MessagesCollectionView) -> Int {
  376. return messageList.count
  377. }
  378. func currentSender() -> Sender {
  379. let currentSender = Sender(id: "1", displayName: "Alice")
  380. return currentSender
  381. }
  382. func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType {
  383. return messageList[indexPath.section]
  384. }
  385. func avatar(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> Avatar {
  386. let message = messageList[indexPath.section]
  387. let contact = message.fromContact
  388. return Avatar(image: contact.profileImage, initials: Utils.getInitials(inputName: contact.name))
  389. }
  390. func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
  391. if isInfoMessage(at: indexPath) {
  392. return nil
  393. }
  394. if isTimeLabelVisible(at: indexPath) {
  395. return NSAttributedString(
  396. string: MessageKitDateFormatter.shared.string(from: message.sentDate),
  397. attributes: [
  398. NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
  399. NSAttributedString.Key.foregroundColor: UIColor.darkGray,
  400. ]
  401. )
  402. }
  403. return nil
  404. }
  405. func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
  406. if !isPreviousMessageSameSender(at: indexPath) {
  407. let name = message.sender.displayName
  408. let m = messageList[indexPath.section]
  409. return NSAttributedString(string: name, attributes: [
  410. .font: UIFont.systemFont(ofSize: 14),
  411. .foregroundColor: m.fromContact.color,
  412. ])
  413. }
  414. return nil
  415. }
  416. func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
  417. guard indexPath.section + 1 < messageList.count else { return false }
  418. let messageA = messageList[indexPath.section]
  419. let messageB = messageList[indexPath.section + 1]
  420. if messageA.fromContactId == messageB.fromContactId {
  421. return false
  422. }
  423. let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)
  424. let dateA = messageA.sentDate
  425. let dateB = messageB.sentDate
  426. let dayA = (calendar?.component(.day, from: dateA))
  427. let dayB = (calendar?.component(.day, from: dateB))
  428. return dayA != dayB
  429. }
  430. func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
  431. guard indexPath.section - 1 >= 0 else { return false }
  432. let messageA = messageList[indexPath.section - 1]
  433. let messageB = messageList[indexPath.section]
  434. if messageA.isInfo {
  435. return false
  436. }
  437. return messageA.fromContactId == messageB.fromContactId
  438. }
  439. func isInfoMessage(at indexPath: IndexPath) -> Bool {
  440. return messageList[indexPath.section].isInfo
  441. }
  442. func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
  443. guard indexPath.section + 1 < messageList.count else { return false }
  444. let messageA = messageList[indexPath.section]
  445. let messageB = messageList[indexPath.section + 1]
  446. if messageA.isInfo {
  447. return false
  448. }
  449. return messageA.fromContactId == messageB.fromContactId
  450. }
  451. func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
  452. guard indexPath.section < messageList.count else { return nil }
  453. let m = messageList[indexPath.section]
  454. if m.isInfo || isNextMessageSameSender(at: indexPath) {
  455. return nil
  456. }
  457. let timestampAttributes: [NSAttributedString.Key: Any] = [
  458. .font: UIFont.systemFont(ofSize: 12),
  459. .foregroundColor: UIColor.lightGray,
  460. ]
  461. if isFromCurrentSender(message: message) {
  462. let text = NSMutableAttributedString()
  463. text.append(NSAttributedString(string: m.formattedSentDate(), attributes: timestampAttributes))
  464. text.append(NSAttributedString(
  465. string: " - " + m.stateDescription(),
  466. attributes: [
  467. .font: UIFont.systemFont(ofSize: 12),
  468. .foregroundColor: UIColor.darkText,
  469. ]
  470. ))
  471. return text
  472. }
  473. return NSAttributedString(string: m.formattedSentDate(), attributes: timestampAttributes)
  474. }
  475. func updateMessage(_ messageId: Int) {
  476. if let index = messageList.firstIndex(where: { $0.id == messageId }) {
  477. dc_markseen_msgs(mailboxPointer, UnsafePointer([UInt32(messageId)]), 1)
  478. messageList[index] = MRMessage(id: messageId)
  479. // Reload section to update header/footer labels
  480. messagesCollectionView.performBatchUpdates({
  481. messagesCollectionView.reloadSections([index])
  482. if index > 0 {
  483. messagesCollectionView.reloadSections([index - 1])
  484. }
  485. if index < messageList.count - 1 {
  486. messagesCollectionView.reloadSections([index + 1])
  487. }
  488. }, completion: { [weak self] _ in
  489. if self?.isLastSectionVisible() == true {
  490. self?.messagesCollectionView.scrollToBottom(animated: true)
  491. }
  492. })
  493. } else {
  494. let msg = MRMessage(id: messageId)
  495. if msg.chatId == chatId {
  496. insertMessage(msg)
  497. }
  498. }
  499. }
  500. func insertMessage(_ message: MRMessage) {
  501. dc_markseen_msgs(mailboxPointer, UnsafePointer([UInt32(message.id)]), 1)
  502. messageList.append(message)
  503. // Reload last section to update header/footer labels and insert a new one
  504. messagesCollectionView.performBatchUpdates({
  505. messagesCollectionView.insertSections([messageList.count - 1])
  506. if messageList.count >= 2 {
  507. messagesCollectionView.reloadSections([messageList.count - 2])
  508. }
  509. }, completion: { [weak self] _ in
  510. if self?.isLastSectionVisible() == true {
  511. self?.messagesCollectionView.scrollToBottom(animated: true)
  512. }
  513. })
  514. }
  515. func isLastSectionVisible() -> Bool {
  516. guard !messageList.isEmpty else { return false }
  517. let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1)
  518. return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath)
  519. }
  520. }
  521. // MARK: - MessagesDisplayDelegate
  522. extension ChatViewController: MessagesDisplayDelegate {
  523. // MARK: - Text Messages
  524. func textColor(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
  525. return .darkText
  526. }
  527. // MARK: - All Messages
  528. func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
  529. return isFromCurrentSender(message: message) ? DCColors.messagePrimaryColor : DCColors.messageSecondaryColor
  530. }
  531. func messageStyle(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
  532. if isInfoMessage(at: indexPath) {
  533. return .custom { view in
  534. view.style = .none
  535. view.backgroundColor = UIColor(alpha: 10, red: 0, green: 0, blue: 0)
  536. let radius: CGFloat = 16
  537. let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: UIRectCorner.allCorners, cornerRadii: CGSize(width: radius, height: radius))
  538. let mask = CAShapeLayer()
  539. mask.path = path.cgPath
  540. view.layer.mask = mask
  541. view.center.x = self.view.center.x
  542. }
  543. }
  544. var corners: UIRectCorner = []
  545. if isFromCurrentSender(message: message) {
  546. corners.formUnion(.topLeft)
  547. corners.formUnion(.bottomLeft)
  548. if !isPreviousMessageSameSender(at: indexPath) {
  549. corners.formUnion(.topRight)
  550. }
  551. if !isNextMessageSameSender(at: indexPath) {
  552. corners.formUnion(.bottomRight)
  553. }
  554. } else {
  555. corners.formUnion(.topRight)
  556. corners.formUnion(.bottomRight)
  557. if !isPreviousMessageSameSender(at: indexPath) {
  558. corners.formUnion(.topLeft)
  559. }
  560. if !isNextMessageSameSender(at: indexPath) {
  561. corners.formUnion(.bottomLeft)
  562. }
  563. }
  564. return .custom { view in
  565. let radius: CGFloat = 16
  566. let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
  567. let mask = CAShapeLayer()
  568. mask.path = path.cgPath
  569. view.layer.mask = mask
  570. }
  571. }
  572. func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) {
  573. let message = messageList[indexPath.section]
  574. let contact = message.fromContact
  575. let avatar = Avatar(image: contact.profileImage, initials: Utils.getInitials(inputName: contact.name))
  576. avatarView.set(avatar: avatar)
  577. avatarView.isHidden = isNextMessageSameSender(at: indexPath) || message.isInfo
  578. avatarView.backgroundColor = contact.color
  579. }
  580. func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
  581. return [.url, .date, .phoneNumber, .address]
  582. }
  583. }
  584. // MARK: - MessagesLayoutDelegate
  585. extension ChatViewController: MessagesLayoutDelegate {
  586. func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
  587. if isTimeLabelVisible(at: indexPath) {
  588. return 18
  589. }
  590. return 0
  591. }
  592. func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
  593. if isInfoMessage(at: indexPath) {
  594. return 0
  595. }
  596. if isFromCurrentSender(message: message) {
  597. return !isPreviousMessageSameSender(at: indexPath) ? 40 : 0
  598. } else {
  599. return !isPreviousMessageSameSender(at: indexPath) ? (40 + outgoingAvatarOverlap) : 0
  600. }
  601. }
  602. func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
  603. if isInfoMessage(at: indexPath) {
  604. return 0
  605. }
  606. if !isNextMessageSameSender(at: indexPath) {
  607. return 16
  608. }
  609. if isFromCurrentSender(message: message) {
  610. return 0
  611. }
  612. return 9
  613. }
  614. func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat {
  615. return 40
  616. }
  617. func footerViewSize(for _: MessageType, at _: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
  618. return CGSize(width: messagesCollectionView.bounds.width, height: 20)
  619. }
  620. @objc func didPressPhotoButton() {
  621. if UIImagePickerController.isSourceTypeAvailable(.camera) {
  622. let cameraViewController = CameraViewController { [weak self] image, _ in
  623. self?.dismiss(animated: true, completion: nil)
  624. DispatchQueue.global().async {
  625. if let pickedImage = image {
  626. let width = Int32(exactly: pickedImage.size.width)!
  627. let height = Int32(exactly: pickedImage.size.height)!
  628. let path = Utils.saveImage(image: pickedImage)
  629. let msg = dc_msg_new(mailboxPointer, DC_MSG_IMAGE)
  630. dc_msg_set_file(msg, path, "image/jpeg")
  631. dc_msg_set_dimension(msg, width, height)
  632. dc_send_msg(mailboxPointer, UInt32(self!.chatId), msg)
  633. // cleanup
  634. dc_msg_unref(msg)
  635. }
  636. }
  637. }
  638. present(cameraViewController, animated: true, completion: nil)
  639. } else {
  640. let alert = UIAlertController(title: "Camera is not available", message: nil, preferredStyle: .alert)
  641. alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { _ in
  642. self.dismiss(animated: true, completion: nil)
  643. }))
  644. present(alert, animated: true, completion: nil)
  645. }
  646. }
  647. }
  648. // MARK: - MessageCellDelegate
  649. extension ChatViewController: MessageCellDelegate {
  650. func didTapMessage(in cell: MessageCollectionViewCell) {
  651. if let indexPath = messagesCollectionView.indexPath(for: cell) {
  652. let message = messageList[indexPath.section]
  653. if let url = message.fileURL {
  654. previewController = PreviewController(urls: [url])
  655. present(previewController!.qlController, animated: true)
  656. }
  657. }
  658. }
  659. func didTapAvatar(in _: MessageCollectionViewCell) {
  660. logger.info("Avatar tapped")
  661. }
  662. @objc(didTapCellTopLabelIn:) func didTapCellTopLabel(in _: MessageCollectionViewCell) {
  663. logger.info("Top label tapped")
  664. }
  665. func didTapBottomLabel(in _: MessageCollectionViewCell) {
  666. print("Bottom label tapped")
  667. }
  668. }
  669. class PreviewController: QLPreviewControllerDataSource {
  670. var urls: [URL]
  671. var qlController: QLPreviewController
  672. init(urls: [URL]) {
  673. self.urls = urls
  674. qlController = QLPreviewController()
  675. qlController.dataSource = self
  676. }
  677. func numberOfPreviewItems(in _: QLPreviewController) -> Int {
  678. return urls.count
  679. }
  680. func previewController(_: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  681. return urls[index] as QLPreviewItem
  682. }
  683. }
  684. // MARK: - MessageLabelDelegate
  685. extension ChatViewController: MessageLabelDelegate {
  686. func didSelectAddress(_ addressComponents: [String: String]) {
  687. let mapAddress = Utils.formatAddressForQuery(address: addressComponents)
  688. if let escapedMapAddress = mapAddress.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
  689. // Use query, to handle malformed addresses
  690. if let url = URL(string: "http://maps.apple.com/?q=\(escapedMapAddress)") {
  691. UIApplication.shared.open(url as URL)
  692. }
  693. }
  694. }
  695. func didSelectDate(_ date: Date) {
  696. let interval = date.timeIntervalSinceReferenceDate
  697. if let url = NSURL(string: "calshow:\(interval)") {
  698. UIApplication.shared.open(url as URL)
  699. }
  700. }
  701. func didSelectPhoneNumber(_ phoneNumber: String) {
  702. logger.info("phone open", phoneNumber)
  703. if let escapedPhoneNumber = phoneNumber.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
  704. if let url = NSURL(string: "tel:\(escapedPhoneNumber)") {
  705. UIApplication.shared.open(url as URL)
  706. }
  707. }
  708. }
  709. func didSelectURL(_ url: URL) {
  710. UIApplication.shared.open(url)
  711. }
  712. }
  713. // MARK: - LocationMessageDisplayDelegate
  714. /*
  715. extension ChatViewController: LocationMessageDisplayDelegate {
  716. func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
  717. let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
  718. let pinImage = #imageLiteral(resourceName: "ic_block_36pt").withRenderingMode(.alwaysTemplate)
  719. annotationView.image = pinImage
  720. annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
  721. return annotationView
  722. }
  723. func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
  724. return { view in
  725. view.layer.transform = CATransform3DMakeScale(0, 0, 0)
  726. view.alpha = 0.0
  727. UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: {
  728. view.layer.transform = CATransform3DIdentity
  729. view.alpha = 1.0
  730. }, completion: nil)
  731. }
  732. }
  733. }
  734. */
  735. // MARK: - MessageInputBarDelegate
  736. extension ChatViewController: MessageInputBarDelegate {
  737. func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
  738. DispatchQueue.global().async {
  739. dc_send_text_msg(mailboxPointer, UInt32(self.chatId), text)
  740. }
  741. inputBar.inputTextView.text = String()
  742. }
  743. }
  744. // MARK: - MessageCollectionViewCell
  745. extension MessageCollectionViewCell {
  746. @objc func messageInfo(_ sender: Any?) {
  747. // Get the collectionView
  748. if let collectionView = self.superview as? UICollectionView {
  749. // Get indexPath
  750. if let indexPath = collectionView.indexPath(for: self) {
  751. // Trigger action
  752. collectionView.delegate?.collectionView?(collectionView, performAction: #selector(MessageCollectionViewCell.messageInfo(_:)), forItemAt: indexPath, withSender: sender)
  753. }
  754. }
  755. }
  756. @objc func messageBlock(_ sender: Any?) {
  757. // Get the collectionView
  758. if let collectionView = self.superview as? UICollectionView {
  759. // Get indexPath
  760. if let indexPath = collectionView.indexPath(for: self) {
  761. // Trigger action
  762. collectionView.delegate?.collectionView?(collectionView, performAction: #selector(MessageCollectionViewCell.messageBlock(_:)), forItemAt: indexPath, withSender: sender)
  763. }
  764. }
  765. }
  766. @objc func messageDismiss(_ sender: Any?) {
  767. // Get the collectionView
  768. if let collectionView = self.superview as? UICollectionView {
  769. // Get indexPath
  770. if let indexPath = collectionView.indexPath(for: self) {
  771. // Trigger action
  772. collectionView.delegate?.collectionView?(collectionView, performAction: #selector(MessageCollectionViewCell.messageDismiss(_:)), forItemAt: indexPath, withSender: sender)
  773. }
  774. }
  775. }
  776. @objc func messageStartChat(_ sender: Any?) {
  777. // Get the collectionView
  778. if let collectionView = self.superview as? UICollectionView {
  779. // Get indexPath
  780. if let indexPath = collectionView.indexPath(for: self) {
  781. // Trigger action
  782. collectionView.delegate?.collectionView?(collectionView, performAction: #selector(MessageCollectionViewCell.messageStartChat(_:)), forItemAt: indexPath, withSender: sender)
  783. }
  784. }
  785. }
  786. }