ChatViewController.swift 32 KB

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