GroupChatDetailViewController.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import UIKit
  2. import DcCore
  3. class GroupChatDetailViewController: UIViewController {
  4. enum ProfileSections {
  5. case attachments
  6. case members
  7. case chatActions
  8. }
  9. enum ChatAction {
  10. case ephemeralMessages
  11. case muteChat
  12. case archiveChat
  13. case leaveGroup
  14. case deleteChat
  15. }
  16. private lazy var chatActions: [ChatAction] = {
  17. var actions: [ChatAction] = [.muteChat, .archiveChat, .leaveGroup, .deleteChat]
  18. if UserDefaults.standard.bool(forKey: "ephemeral_messages") || dcContext.getChatEphemeralTimer(chatId: chatId) > 0 {
  19. actions.insert(.ephemeralMessages, at: 0)
  20. }
  21. return actions
  22. }()
  23. private let attachmentsRowGallery = 0
  24. private let attachmentsRowDocuments = 1
  25. private let membersRowAddMembers = 0
  26. private let membersRowQrInvite = 1
  27. private let memberManagementRows = 2
  28. private let dcContext: DcContext
  29. private let sections: [ProfileSections] = [.attachments, .members, .chatActions]
  30. private var currentUser: DcContact? {
  31. let myId = groupMemberIds.filter { DcContact(id: $0).email == dcContext.addr }.first
  32. guard let currentUserId = myId else {
  33. return nil
  34. }
  35. return DcContact(id: currentUserId)
  36. }
  37. private var chatId: Int
  38. private var chat: DcChat {
  39. return dcContext.getChat(chatId: chatId)
  40. }
  41. // stores contactIds
  42. private var groupMemberIds: [Int] = []
  43. // MARK: - subviews
  44. private lazy var editBarButtonItem: UIBarButtonItem = {
  45. UIBarButtonItem(title: String.localized("global_menu_edit_desktop"), style: .plain, target: self, action: #selector(editButtonPressed))
  46. }()
  47. lazy var tableView: UITableView = {
  48. let table = UITableView(frame: .zero, style: .grouped)
  49. table.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
  50. table.register(ActionCell.self, forCellReuseIdentifier: "actionCell")
  51. table.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
  52. table.delegate = self
  53. table.dataSource = self
  54. table.tableHeaderView = groupHeader
  55. return table
  56. }()
  57. private lazy var groupHeader: ContactDetailHeader = {
  58. let header = ContactDetailHeader()
  59. header.updateDetails(
  60. title: chat.name,
  61. subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
  62. )
  63. if let img = chat.profileImage {
  64. header.setImage(img)
  65. } else {
  66. header.setBackupImage(name: chat.name, color: chat.color)
  67. }
  68. header.setVerified(isVerified: chat.isVerified)
  69. return header
  70. }()
  71. private lazy var ephemeralMessagesCell: UITableViewCell = {
  72. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  73. cell.textLabel?.text = String.localized("pref_ephemeral_messages")
  74. cell.selectionStyle = .none
  75. cell.accessoryType = .disclosureIndicator
  76. return cell
  77. }()
  78. private lazy var muteChatCell: ActionCell = {
  79. let cell = ActionCell()
  80. cell.actionTitle = self.chat.isMuted ? String.localized("menu_unmute") : String.localized("menu_mute")
  81. cell.actionColor = SystemColor.blue.uiColor
  82. cell.selectionStyle = .none
  83. return cell
  84. }()
  85. private lazy var archiveChatCell: ActionCell = {
  86. let cell = ActionCell()
  87. cell.actionTitle = chat.isArchived ? String.localized("menu_unarchive_chat") : String.localized("menu_archive_chat")
  88. cell.actionColor = UIColor.systemBlue
  89. cell.selectionStyle = .none
  90. return cell
  91. }()
  92. private lazy var leaveGroupCell: ActionCell = {
  93. let cell = ActionCell()
  94. cell.actionTitle = String.localized("menu_leave_group")
  95. cell.actionColor = UIColor.red
  96. return cell
  97. }()
  98. private lazy var deleteChatCell: ActionCell = {
  99. let cell = ActionCell()
  100. cell.actionTitle = String.localized("menu_delete_chat")
  101. cell.actionColor = UIColor.red
  102. cell.selectionStyle = .none
  103. return cell
  104. }()
  105. private lazy var galleryCell: UITableViewCell = {
  106. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  107. cell.textLabel?.text = String.localized("gallery")
  108. cell.accessoryType = .disclosureIndicator
  109. return cell
  110. }()
  111. private lazy var documentsCell: UITableViewCell = {
  112. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  113. cell.textLabel?.text = String.localized("documents")
  114. cell.accessoryType = .disclosureIndicator
  115. return cell
  116. }()
  117. init(chatId: Int, dcContext: DcContext) {
  118. self.dcContext = dcContext
  119. self.chatId = chatId
  120. super.init(nibName: nil, bundle: nil)
  121. setupSubviews()
  122. }
  123. required init?(coder _: NSCoder) {
  124. fatalError("init(coder:) has not been implemented")
  125. }
  126. private func setupSubviews() {
  127. view.addSubview(tableView)
  128. tableView.translatesAutoresizingMaskIntoConstraints = false
  129. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
  130. tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  131. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
  132. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  133. }
  134. // MARK: - lifecycle
  135. override func viewDidLoad() {
  136. super.viewDidLoad()
  137. title = String.localized("tab_group")
  138. navigationItem.rightBarButtonItem = editBarButtonItem
  139. groupHeader.frame = CGRect(0, 0, tableView.frame.width, ContactCell.cellHeight)
  140. }
  141. override func viewWillAppear(_ animated: Bool) {
  142. super.viewWillAppear(animated)
  143. //update chat object, maybe chat name was edited
  144. updateGroupMembers()
  145. tableView.reloadData() // to display updates
  146. editBarButtonItem.isEnabled = currentUser != nil
  147. updateHeader()
  148. }
  149. // MARK: - update
  150. private func updateGroupMembers() {
  151. groupMemberIds = chat.contactIds
  152. tableView.reloadData()
  153. }
  154. private func updateHeader() {
  155. groupHeader.updateDetails(
  156. title: chat.name,
  157. subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
  158. )
  159. if let img = chat.profileImage {
  160. groupHeader.setImage(img)
  161. } else {
  162. groupHeader.setBackupImage(name: chat.name, color: chat.color)
  163. }
  164. groupHeader.setVerified(isVerified: chat.isVerified)
  165. }
  166. // MARK: - actions
  167. @objc func editButtonPressed() {
  168. showGroupChatEdit(chat: chat)
  169. }
  170. private func toggleArchiveChat() {
  171. let archivedBefore = chat.isArchived
  172. dcContext.archiveChat(chatId: chat.id, archive: !archivedBefore)
  173. if archivedBefore {
  174. archiveChatCell.actionTitle = String.localized("menu_archive_chat")
  175. } else {
  176. self.navigationController?.popToRootViewController(animated: false)
  177. }
  178. }
  179. private func getGroupMemberIdFor(_ row: Int) -> Int {
  180. return groupMemberIds[row - memberManagementRows]
  181. }
  182. private func isMemberManagementRow(row: Int) -> Bool {
  183. return row < memberManagementRows
  184. }
  185. // MARK: - coordinator
  186. private func showSingleChatEdit(contactId: Int) {
  187. let editContactController = EditContactController(dcContext: dcContext, contactIdForUpdate: contactId)
  188. navigationController?.pushViewController(editContactController, animated: true)
  189. }
  190. private func showAddGroupMember(chatId: Int) {
  191. let groupMemberViewController = AddGroupMembersViewController(chatId: chatId)
  192. navigationController?.pushViewController(groupMemberViewController, animated: true)
  193. }
  194. private func showQrCodeInvite(chatId: Int) {
  195. let qrInviteCodeController = QrInviteViewController(dcContext: dcContext, chatId: chatId)
  196. navigationController?.pushViewController(qrInviteCodeController, animated: true)
  197. }
  198. private func showGroupChatEdit(chat: DcChat) {
  199. let editGroupViewController = EditGroupViewController(dcContext: dcContext, chat: chat)
  200. navigationController?.pushViewController(editGroupViewController, animated: true)
  201. }
  202. private func showContactDetail(of contactId: Int) {
  203. let contactDetailController = ContactDetailViewController(dcContext: dcContext, contactId: contactId)
  204. navigationController?.pushViewController(contactDetailController, animated: true)
  205. }
  206. private func showDocuments() {
  207. let messageIds = dcContext.getChatMedia(
  208. chatId: chatId,
  209. messageType: DC_MSG_FILE,
  210. messageType2: DC_MSG_AUDIO,
  211. messageType3: 0
  212. )
  213. let fileGalleryController = DocumentGalleryController(fileMessageIds: messageIds)
  214. navigationController?.pushViewController(fileGalleryController, animated: true) }
  215. private func showGallery() {
  216. let messageIds = dcContext.getChatMedia(
  217. chatId: chatId,
  218. messageType: DC_MSG_IMAGE,
  219. messageType2: DC_MSG_GIF,
  220. messageType3: DC_MSG_VIDEO
  221. )
  222. let galleryController = GalleryViewController(mediaMessageIds: messageIds)
  223. navigationController?.pushViewController(galleryController, animated: true)
  224. }
  225. private func deleteChat() {
  226. dcContext.deleteChat(chatId: chatId)
  227. // just pop to viewControllers - we've in chatlist or archive then
  228. // (no not use `navigationController?` here: popping self will make the reference becoming nil)
  229. if let navigationController = navigationController {
  230. navigationController.popViewController(animated: false)
  231. navigationController.popViewController(animated: true)
  232. }
  233. }
  234. }
  235. // MARK: - UITableViewDelegate, UITableViewDataSource
  236. extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSource {
  237. func numberOfSections(in _: UITableView) -> Int {
  238. return sections.count
  239. }
  240. func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  241. let sectionType = sections[section]
  242. switch sectionType {
  243. case .attachments:
  244. return 2
  245. case .members:
  246. return groupMemberIds.count + memberManagementRows
  247. case .chatActions:
  248. return chatActions.count
  249. }
  250. }
  251. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  252. let sectionType = sections[indexPath.section]
  253. let row = indexPath.row
  254. switch sectionType {
  255. case .attachments, .chatActions:
  256. return Constants.defaultCellHeight
  257. case .members:
  258. switch row {
  259. case membersRowAddMembers, membersRowQrInvite:
  260. return Constants.defaultCellHeight
  261. default:
  262. return ContactCell.cellHeight
  263. }
  264. }
  265. }
  266. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  267. let row = indexPath.row
  268. let sectionType = sections[indexPath.section]
  269. switch sectionType {
  270. case .attachments:
  271. if row == attachmentsRowGallery {
  272. return galleryCell
  273. } else if row == attachmentsRowDocuments {
  274. return documentsCell
  275. }
  276. case .members:
  277. if row == membersRowAddMembers || row == membersRowQrInvite {
  278. guard let actionCell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath) as? ActionCell else {
  279. safe_fatalError("could not dequeue action cell")
  280. break
  281. }
  282. if row == membersRowAddMembers {
  283. actionCell.actionTitle = String.localized("group_add_members")
  284. actionCell.actionColor = UIColor.systemBlue
  285. } else if row == membersRowQrInvite {
  286. actionCell.actionTitle = String.localized("qrshow_join_group_title")
  287. actionCell.actionColor = UIColor.systemBlue
  288. }
  289. return actionCell
  290. }
  291. guard let contactCell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as? ContactCell else {
  292. safe_fatalError("could not dequeue contactCell cell")
  293. break
  294. }
  295. let contactId: Int = getGroupMemberIdFor(row)
  296. let cellData = ContactCellData(
  297. contactId: contactId,
  298. chatId: dcContext.getChatIdByContactIdOld(contactId)
  299. )
  300. let cellViewModel = ContactCellViewModel(contactData: cellData)
  301. contactCell.updateCell(cellViewModel: cellViewModel)
  302. return contactCell
  303. case .chatActions:
  304. switch chatActions[row] {
  305. case .ephemeralMessages:
  306. return ephemeralMessagesCell
  307. case .muteChat:
  308. return muteChatCell
  309. case .archiveChat:
  310. return archiveChatCell
  311. case .leaveGroup:
  312. return leaveGroupCell
  313. case .deleteChat:
  314. return deleteChatCell
  315. }
  316. }
  317. // should never get here
  318. return UITableViewCell(frame: .zero)
  319. }
  320. func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
  321. let sectionType = sections[indexPath.section]
  322. let row = indexPath.row
  323. switch sectionType {
  324. case .attachments:
  325. if row == attachmentsRowGallery {
  326. showGallery()
  327. } else if row == attachmentsRowDocuments {
  328. showDocuments()
  329. }
  330. case .members:
  331. if row == membersRowAddMembers {
  332. showAddGroupMember(chatId: chat.id)
  333. } else if row == membersRowQrInvite {
  334. showQrCodeInvite(chatId: chat.id)
  335. } else {
  336. let member = getGroupMember(at: row)
  337. showContactDetail(of: member.id)
  338. }
  339. case .chatActions:
  340. switch chatActions[row] {
  341. case .ephemeralMessages:
  342. showEphemeralMessagesController()
  343. case .muteChat:
  344. if chat.isMuted {
  345. dcContext.setChatMuteDuration(chatId: chatId, duration: 0)
  346. muteChatCell.actionTitle = String.localized("menu_mute")
  347. } else {
  348. showMuteAlert()
  349. }
  350. case .archiveChat:
  351. toggleArchiveChat()
  352. case .leaveGroup:
  353. showLeaveGroupConfirmationAlert()
  354. case .deleteChat:
  355. showDeleteChatConfirmationAlert()
  356. }
  357. }
  358. }
  359. func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  360. if sections[section] == .members {
  361. return String.localized("tab_members")
  362. }
  363. return nil
  364. }
  365. func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  366. return Constants.defaultHeaderHeight
  367. }
  368. func tableView(_: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  369. guard let currentUser = self.currentUser else {
  370. return false
  371. }
  372. let row = indexPath.row
  373. let sectionType = sections[indexPath.section]
  374. if sectionType == .members &&
  375. !isMemberManagementRow(row: row) &&
  376. getGroupMemberIdFor(row) != currentUser.id {
  377. return true
  378. }
  379. return false
  380. }
  381. func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
  382. guard let currentUser = self.currentUser else {
  383. return nil
  384. }
  385. let row = indexPath.row
  386. let sectionType = sections[indexPath.section]
  387. if sectionType == .members &&
  388. !isMemberManagementRow(row: row) &&
  389. getGroupMemberIdFor(row) != currentUser.id {
  390. // action set for members except for current user
  391. let delete = UITableViewRowAction(style: .destructive, title: String.localized("remove_desktop")) { [weak self] _, indexPath in
  392. guard let self = self else { return }
  393. let contact = self.getGroupMember(at: row)
  394. let title = String.localizedStringWithFormat(String.localized("ask_remove_members"), contact.nameNAddr)
  395. let alert = UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
  396. alert.addAction(UIAlertAction(title: String.localized("remove_desktop"), style: .destructive, handler: { _ in
  397. let success = self.dcContext.removeContactFromChat(chatId: self.chat.id, contactId: contact.id)
  398. if success {
  399. self.removeGroupMemberFromTableAt(indexPath)
  400. }
  401. }))
  402. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  403. self.present(alert, animated: true, completion: nil)
  404. }
  405. delete.backgroundColor = UIColor.red
  406. return [delete]
  407. }
  408. return nil
  409. }
  410. private func getGroupMember(at row: Int) -> DcContact {
  411. return DcContact(id: getGroupMemberIdFor(row))
  412. }
  413. private func removeGroupMemberFromTableAt(_ indexPath: IndexPath) {
  414. self.groupMemberIds.remove(at: indexPath.row - memberManagementRows)
  415. self.tableView.deleteRows(at: [indexPath], with: .automatic)
  416. updateHeader() // to display correct group size
  417. }
  418. private func showEphemeralMessagesController() {
  419. let ephemeralMessagesController = SettingsEphemeralMessageController(dcContext: dcContext, chatId: chatId)
  420. navigationController?.pushViewController(ephemeralMessagesController, animated: true)
  421. }
  422. }
  423. // MARK: - alerts
  424. extension GroupChatDetailViewController {
  425. private func showMuteAlert() {
  426. let alert = UIAlertController(title: String.localized("mute"), message: nil, preferredStyle: .safeActionSheet)
  427. let forever = -1
  428. addDurationSelectionAction(to: alert, key: "mute_for_one_hour", duration: Time.oneHour)
  429. addDurationSelectionAction(to: alert, key: "mute_for_two_hours", duration: Time.twoHours)
  430. addDurationSelectionAction(to: alert, key: "mute_for_one_day", duration: Time.oneDay)
  431. addDurationSelectionAction(to: alert, key: "mute_for_seven_days", duration: Time.oneWeek)
  432. addDurationSelectionAction(to: alert, key: "mute_forever", duration: forever)
  433. let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
  434. alert.addAction(cancelAction)
  435. present(alert, animated: true, completion: nil)
  436. }
  437. private func addDurationSelectionAction(to alert: UIAlertController, key: String, duration: Int) {
  438. let action = UIAlertAction(title: String.localized(key), style: .default, handler: { _ in
  439. self.dcContext.setChatMuteDuration(chatId: self.chatId, duration: duration)
  440. self.muteChatCell.actionTitle = String.localized("menu_unmute")
  441. })
  442. alert.addAction(action)
  443. }
  444. private func showDeleteChatConfirmationAlert() {
  445. let alert = UIAlertController(
  446. title: nil,
  447. message: String.localized("ask_delete_chat_desktop"),
  448. preferredStyle: .safeActionSheet
  449. )
  450. alert.addAction(UIAlertAction(title: String.localized("menu_delete_chat"), style: .destructive, handler: { _ in
  451. self.deleteChat()
  452. }))
  453. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  454. self.present(alert, animated: true, completion: nil)
  455. }
  456. private func showLeaveGroupConfirmationAlert() {
  457. if let userId = currentUser?.id {
  458. let alert = UIAlertController(title: String.localized("ask_leave_group"), message: nil, preferredStyle: .safeActionSheet)
  459. alert.addAction(UIAlertAction(title: String.localized("menu_leave_group"), style: .destructive, handler: { _ in
  460. _ = self.dcContext.removeContactFromChat(chatId: self.chat.id, contactId: userId)
  461. self.editBarButtonItem.isEnabled = false
  462. self.updateGroupMembers()
  463. }))
  464. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  465. present(alert, animated: true, completion: nil)
  466. }
  467. }
  468. }