ContactDetailViewController.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. import UIKit
  2. import DcCore
  3. // this is also used as ChatDetail for SingleChats
  4. class ContactDetailViewController: UITableViewController {
  5. private let viewModel: ContactDetailViewModel
  6. private lazy var headerCell: ContactDetailHeader = {
  7. return ContactDetailHeader()
  8. }()
  9. private lazy var startChatCell: ActionCell = {
  10. let cell = ActionCell()
  11. cell.actionColor = SystemColor.blue.uiColor
  12. cell.actionTitle = String.localized("send_message")
  13. return cell
  14. }()
  15. private lazy var ephemeralMessagesCell: UITableViewCell = {
  16. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  17. cell.textLabel?.text = String.localized("ephemeral_messages")
  18. cell.accessoryType = .disclosureIndicator
  19. return cell
  20. }()
  21. private lazy var showEncrInfoCell: ActionCell = {
  22. let cell = ActionCell()
  23. cell.actionTitle = String.localized("encryption_info_title_desktop")
  24. cell.actionColor = SystemColor.blue.uiColor
  25. return cell
  26. }()
  27. private lazy var blockContactCell: ActionCell = {
  28. let cell = ActionCell()
  29. cell.actionTitle = viewModel.contact.isBlocked ? String.localized("menu_unblock_contact") : String.localized("menu_block_contact")
  30. cell.actionColor = viewModel.contact.isBlocked ? SystemColor.blue.uiColor : UIColor.red
  31. return cell
  32. }()
  33. private lazy var muteChatCell: ActionCell = {
  34. let cell = ActionCell()
  35. cell.actionTitle = viewModel.chatIsMuted ? String.localized("menu_unmute") : String.localized("menu_mute")
  36. cell.actionColor = SystemColor.blue.uiColor
  37. return cell
  38. }()
  39. private lazy var archiveChatCell: ActionCell = {
  40. let cell = ActionCell()
  41. cell.actionTitle = viewModel.chatIsArchived ? String.localized("menu_unarchive_chat") : String.localized("menu_archive_chat")
  42. cell.actionColor = SystemColor.blue.uiColor
  43. return cell
  44. }()
  45. private lazy var deleteChatCell: ActionCell = {
  46. let cell = ActionCell()
  47. cell.actionTitle = String.localized("menu_delete_chat")
  48. cell.actionColor = UIColor.red
  49. return cell
  50. }()
  51. private lazy var galleryCell: UITableViewCell = {
  52. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  53. cell.textLabel?.text = String.localized("images_and_videos")
  54. cell.accessoryType = .disclosureIndicator
  55. if viewModel.chatId == 0 {
  56. cell.isUserInteractionEnabled = false
  57. cell.textLabel?.isEnabled = false
  58. }
  59. return cell
  60. }()
  61. private lazy var documentsCell: UITableViewCell = {
  62. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  63. cell.textLabel?.text = String.localized("files")
  64. cell.accessoryType = .disclosureIndicator
  65. if viewModel.chatId == 0 {
  66. cell.isUserInteractionEnabled = false
  67. cell.textLabel?.isEnabled = false
  68. }
  69. return cell
  70. }()
  71. private lazy var statusCell: MultilineLabelCell = {
  72. let cell = MultilineLabelCell()
  73. cell.multilineDelegate = self
  74. return cell
  75. }()
  76. init(dcContext: DcContext, contactId: Int) {
  77. self.viewModel = ContactDetailViewModel(dcContext: dcContext, contactId: contactId)
  78. super.init(style: .grouped)
  79. }
  80. required init?(coder _: NSCoder) {
  81. fatalError("init(coder:) has not been implemented")
  82. }
  83. // MARK: - lifecycle
  84. override func viewDidLoad() {
  85. super.viewDidLoad()
  86. configureTableView()
  87. if !self.viewModel.isSavedMessages && !self.viewModel.isDeviceTalk {
  88. navigationItem.rightBarButtonItem = UIBarButtonItem(
  89. title: String.localized("global_menu_edit_desktop"),
  90. style: .plain, target: self, action: #selector(editButtonPressed))
  91. self.title = String.localized("tab_contact")
  92. } else {
  93. self.title = String.localized("profile")
  94. }
  95. }
  96. override func viewWillAppear(_ animated: Bool) {
  97. super.viewWillAppear(animated)
  98. updateHeader() // maybe contact name has been edited
  99. updateCellValues()
  100. tableView.reloadData()
  101. }
  102. // MARK: - setup and configuration
  103. private func configureTableView() {
  104. tableView.register(ActionCell.self, forCellReuseIdentifier: ActionCell.reuseIdentifier)
  105. tableView.register(ContactCell.self, forCellReuseIdentifier: ContactCell.reuseIdentifier)
  106. headerCell.frame = CGRect(0, 0, tableView.frame.width, ContactCell.cellHeight)
  107. tableView.tableHeaderView = headerCell
  108. tableView.sectionHeaderHeight = UITableView.automaticDimension
  109. tableView.rowHeight = UITableView.automaticDimension
  110. }
  111. // MARK: - UITableViewDatasource, UITableViewDelegate
  112. override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  113. if previousTraitCollection?.preferredContentSizeCategory !=
  114. traitCollection.preferredContentSizeCategory {
  115. headerCell.frame = CGRect(0, 0, tableView.frame.width, ContactCell.cellHeight)
  116. }
  117. }
  118. override func numberOfSections(in tableView: UITableView) -> Int {
  119. return viewModel.numberOfSections
  120. }
  121. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  122. return viewModel.numberOfRowsInSection(section)
  123. }
  124. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  125. let row = indexPath.row
  126. let cellType = viewModel.typeFor(section: indexPath.section)
  127. switch cellType {
  128. case .chatOptions:
  129. switch viewModel.chatOptionFor(row: row) {
  130. case .documents:
  131. return documentsCell
  132. case .gallery:
  133. return galleryCell
  134. case .ephemeralMessages:
  135. return ephemeralMessagesCell
  136. case .muteChat:
  137. return muteChatCell
  138. case .startChat:
  139. return startChatCell
  140. }
  141. case .statusArea:
  142. return statusCell
  143. case .chatActions:
  144. switch viewModel.chatActionFor(row: row) {
  145. case .archiveChat:
  146. return archiveChatCell
  147. case .showEncrInfo:
  148. return showEncrInfoCell
  149. case .blockContact:
  150. return blockContactCell
  151. case .deleteChat:
  152. return deleteChatCell
  153. }
  154. case .sharedChats:
  155. if let cell = tableView.dequeueReusableCell(withIdentifier: ContactCell.reuseIdentifier, for: indexPath) as? ContactCell {
  156. viewModel.update(sharedChatCell: cell, row: row)
  157. cell.backgroundColor = DcColors.sharedChatCellBackgroundColor
  158. return cell
  159. }
  160. }
  161. return UITableViewCell() // should never get here
  162. }
  163. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  164. let type = viewModel.typeFor(section: indexPath.section)
  165. switch type {
  166. case .chatOptions:
  167. handleChatOption(indexPath: indexPath)
  168. case .statusArea:
  169. break
  170. case .chatActions:
  171. handleChatAction(indexPath: indexPath)
  172. case .sharedChats:
  173. let chatId = viewModel.getSharedChatIdAt(indexPath: indexPath)
  174. showChat(chatId: chatId)
  175. }
  176. }
  177. override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  178. let type = viewModel.typeFor(section: indexPath.section)
  179. switch type {
  180. case .sharedChats:
  181. return ContactCell.cellHeight
  182. default:
  183. return UITableView.automaticDimension
  184. }
  185. }
  186. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  187. return viewModel.titleFor(section: section)
  188. }
  189. // MARK: - updates
  190. private func updateHeader() {
  191. if viewModel.isSavedMessages {
  192. let chat = viewModel.context.getChat(chatId: viewModel.chatId)
  193. headerCell.updateDetails(title: chat.name, subtitle: String.localized("chat_self_talk_subtitle"))
  194. if let img = chat.profileImage {
  195. headerCell.setImage(img)
  196. } else {
  197. headerCell.setBackupImage(name: chat.name, color: chat.color)
  198. }
  199. headerCell.setVerified(isVerified: false)
  200. } else {
  201. headerCell.updateDetails(title: viewModel.contact.displayName,
  202. subtitle: viewModel.isDeviceTalk ? String.localized("device_talk_subtitle") : viewModel.contact.email)
  203. if let img = viewModel.contact.profileImage {
  204. headerCell.setImage(img)
  205. } else {
  206. headerCell.setBackupImage(name: viewModel.contact.displayName, color: viewModel.contact.color)
  207. }
  208. headerCell.setVerified(isVerified: viewModel.contact.isVerified)
  209. }
  210. headerCell.onAvatarTap = showContactAvatarIfNeeded
  211. }
  212. private func updateCellValues() {
  213. ephemeralMessagesCell.detailTextLabel?.text = String.localized(viewModel.chatIsEphemeral ? "on" : "off")
  214. galleryCell.detailTextLabel?.text = String.numberOrNone(viewModel.galleryItemMessageIds.count)
  215. documentsCell.detailTextLabel?.text = String.numberOrNone(viewModel.documentItemMessageIds.count)
  216. statusCell.setText(text: viewModel.contact.status)
  217. }
  218. // MARK: - actions
  219. private func handleChatAction(indexPath: IndexPath) {
  220. let action = viewModel.chatActionFor(row: indexPath.row)
  221. switch action {
  222. case .archiveChat:
  223. tableView.deselectRow(at: indexPath, animated: true) // animated as no other elements pop up
  224. toggleArchiveChat()
  225. case .showEncrInfo:
  226. tableView.deselectRow(at: indexPath, animated: false)
  227. showEncrInfoAlert()
  228. case .blockContact:
  229. tableView.deselectRow(at: indexPath, animated: false)
  230. toggleBlockContact()
  231. case .deleteChat:
  232. tableView.deselectRow(at: indexPath, animated: false)
  233. showDeleteChatConfirmationAlert()
  234. }
  235. }
  236. private func handleChatOption(indexPath: IndexPath) {
  237. let action = viewModel.chatOptionFor(row: indexPath.row)
  238. switch action {
  239. case .documents:
  240. showDocuments()
  241. case .gallery:
  242. showGallery()
  243. case .ephemeralMessages:
  244. showEphemeralMessagesController()
  245. case .muteChat:
  246. tableView.deselectRow(at: indexPath, animated: false)
  247. if viewModel.chatIsMuted {
  248. self.viewModel.context.setChatMuteDuration(chatId: self.viewModel.chatId, duration: 0)
  249. muteChatCell.actionTitle = String.localized("menu_mute")
  250. self.navigationController?.popViewController(animated: true)
  251. } else {
  252. showMuteAlert()
  253. }
  254. case .startChat:
  255. tableView.deselectRow(at: indexPath, animated: false)
  256. let contactId = viewModel.contactId
  257. chatWith(contactId: contactId)
  258. }
  259. }
  260. private func toggleArchiveChat() {
  261. let archived = viewModel.toggleArchiveChat()
  262. if archived {
  263. self.navigationController?.popToRootViewController(animated: false)
  264. } else {
  265. archiveChatCell.actionTitle = String.localized("menu_archive_chat")
  266. }
  267. }
  268. private func updateBlockContactCell() {
  269. blockContactCell.actionTitle = viewModel.contact.isBlocked ? String.localized("menu_unblock_contact") : String.localized("menu_block_contact")
  270. blockContactCell.actionColor = viewModel.contact.isBlocked ? SystemColor.blue.uiColor : UIColor.red
  271. }
  272. @objc private func editButtonPressed() {
  273. showEditContact(contactId: viewModel.contactId)
  274. }
  275. // MARK: alerts
  276. private func showDeleteChatConfirmationAlert() {
  277. let alert = UIAlertController(
  278. title: nil,
  279. message: String.localized("ask_delete_chat_desktop"),
  280. preferredStyle: .safeActionSheet
  281. )
  282. alert.addAction(UIAlertAction(title: String.localized("menu_delete_chat"), style: .destructive, handler: { _ in
  283. self.deleteChat()
  284. }))
  285. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  286. self.present(alert, animated: true, completion: nil)
  287. }
  288. private func showEncrInfoAlert() {
  289. let alert = UIAlertController(
  290. title: nil,
  291. message: self.viewModel.context.getContactEncrInfo(contactId: self.viewModel.contactId),
  292. preferredStyle: .alert
  293. )
  294. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  295. self.present(alert, animated: true, completion: nil)
  296. }
  297. private func showEphemeralMessagesController() {
  298. let ephemeralMessagesController = SettingsEphemeralMessageController(dcContext: viewModel.context, chatId: viewModel.chatId)
  299. navigationController?.pushViewController(ephemeralMessagesController, animated: true)
  300. }
  301. private func showMuteAlert() {
  302. let alert = UIAlertController(title: String.localized("mute"), message: nil, preferredStyle: .safeActionSheet)
  303. let forever = -1
  304. addDurationSelectionAction(to: alert, key: "mute_for_one_hour", duration: Time.oneHour)
  305. addDurationSelectionAction(to: alert, key: "mute_for_two_hours", duration: Time.twoHours)
  306. addDurationSelectionAction(to: alert, key: "mute_for_one_day", duration: Time.oneDay)
  307. addDurationSelectionAction(to: alert, key: "mute_for_seven_days", duration: Time.oneWeek)
  308. addDurationSelectionAction(to: alert, key: "mute_forever", duration: forever)
  309. let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
  310. alert.addAction(cancelAction)
  311. present(alert, animated: true, completion: nil)
  312. }
  313. private func addDurationSelectionAction(to alert: UIAlertController, key: String, duration: Int) {
  314. let action = UIAlertAction(title: String.localized(key), style: .default, handler: { _ in
  315. self.viewModel.context.setChatMuteDuration(chatId: self.viewModel.chatId, duration: duration)
  316. self.muteChatCell.actionTitle = String.localized("menu_unmute")
  317. self.navigationController?.popViewController(animated: true)
  318. })
  319. alert.addAction(action)
  320. }
  321. private func toggleBlockContact() {
  322. if viewModel.contact.isBlocked {
  323. let alert = UIAlertController(title: String.localized("ask_unblock_contact"), message: nil, preferredStyle: .safeActionSheet)
  324. alert.addAction(UIAlertAction(title: String.localized("menu_unblock_contact"), style: .default, handler: { _ in
  325. self.viewModel.contact.unblock()
  326. self.updateBlockContactCell()
  327. }))
  328. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  329. present(alert, animated: true, completion: nil)
  330. } else {
  331. let alert = UIAlertController(title: String.localized("ask_block_contact"), message: nil, preferredStyle: .safeActionSheet)
  332. alert.addAction(UIAlertAction(title: String.localized("menu_block_contact"), style: .destructive, handler: { _ in
  333. self.viewModel.contact.block()
  334. self.updateBlockContactCell()
  335. }))
  336. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  337. present(alert, animated: true, completion: nil)
  338. }
  339. }
  340. private func chatWith(contactId: Int) {
  341. let chatId = self.viewModel.context.createChatByContactId(contactId: contactId)
  342. self.showChat(chatId: chatId)
  343. }
  344. // MARK: - coordinator
  345. private func showChat(chatId: Int) {
  346. if let chatlistViewController = navigationController?.viewControllers[0] {
  347. let chatViewController = ChatViewController(dcContext: viewModel.context, chatId: chatId)
  348. navigationController?.setViewControllers([chatlistViewController, chatViewController], animated: true)
  349. }
  350. }
  351. private func showEditContact(contactId: Int) {
  352. let editContactController = EditContactController(dcContext: viewModel.context, contactIdForUpdate: contactId)
  353. navigationController?.pushViewController(editContactController, animated: true)
  354. }
  355. private func showDocuments() {
  356. let messageIds: [Int] = viewModel.documentItemMessageIds.reversed()
  357. let fileGalleryController = DocumentGalleryController(context: viewModel.context, fileMessageIds: messageIds)
  358. navigationController?.pushViewController(fileGalleryController, animated: true)
  359. }
  360. private func showGallery() {
  361. let messageIds: [Int] = viewModel.galleryItemMessageIds.reversed()
  362. let galleryController = GalleryViewController(context: viewModel.context, mediaMessageIds: messageIds)
  363. navigationController?.pushViewController(galleryController, animated: true)
  364. }
  365. private func showContactAvatarIfNeeded() {
  366. if viewModel.isSavedMessages {
  367. let chat = viewModel.context.getChat(chatId: viewModel.chatId)
  368. if let url = chat.profileImageURL {
  369. let previewController = PreviewController(type: .single(url))
  370. previewController.customTitle = chat.name
  371. present(previewController, animated: true, completion: nil)
  372. }
  373. } else if let url = viewModel.contact.profileImageURL {
  374. let previewController = PreviewController(type: .single(url))
  375. previewController.customTitle = viewModel.contact.displayName
  376. present(previewController, animated: true, completion: nil)
  377. }
  378. }
  379. private func deleteChat() {
  380. if viewModel.chatId == 0 {
  381. return
  382. }
  383. viewModel.context.deleteChat(chatId: viewModel.chatId)
  384. NotificationManager.removeNotificationsForChat(chatId: viewModel.chatId)
  385. // just pop to viewControllers - we've in chatlist or archive then
  386. // (no not use `navigationController?` here: popping self will make the reference becoming nil)
  387. if let navigationController = navigationController {
  388. navigationController.popViewController(animated: false)
  389. navigationController.popViewController(animated: true)
  390. }
  391. }
  392. }
  393. extension ContactDetailViewController: MultilineLabelCellDelegate {
  394. func phoneNumberTapped(number: String) {
  395. let sanitizedNumber = number.filter("0123456789".contains)
  396. if let phoneURL = URL(string: "tel://\(sanitizedNumber)") {
  397. UIApplication.shared.open(phoneURL, options: [:], completionHandler: nil)
  398. }
  399. }
  400. func urlTapped(url: URL) {
  401. if Utils.isEmail(url: url) {
  402. let email = Utils.getEmailFrom(url)
  403. let contactId = viewModel.context.createContact(name: "", email: email)
  404. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), email),
  405. message: nil, preferredStyle: .safeActionSheet)
  406. alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { [weak self] _ in
  407. guard let self = self else { return }
  408. let chatId = self.viewModel.context.createChatByContactId(contactId: contactId)
  409. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  410. appDelegate.appCoordinator.showChat(chatId: chatId, clearViewControllerStack: true)
  411. }
  412. }))
  413. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  414. present(alert, animated: true, completion: nil)
  415. } else {
  416. UIApplication.shared.open(url)
  417. }
  418. }
  419. }