GroupChatDetailViewController.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import UIKit
  2. class GroupChatDetailViewController: UIViewController {
  3. enum ProfileSections {
  4. case attachments
  5. //case memberManagement // add member, qr invideCode
  6. case members // contactCells
  7. case chatActions // archive, leave, delete
  8. }
  9. private let attachmentsRowGallery = 0
  10. private let attachmentsRowDocuments = 1
  11. private let membersRowAddMembers = 0
  12. private let membersRowQrInvite = 1
  13. private let memberManagementRows = 2
  14. private let chatActionsRowArchiveChat = 0
  15. private let chatActionsRowLeaveGroup = 1
  16. private let chatActionsRowDeleteChat = 2
  17. private let context: DcContext
  18. weak var coordinator: GroupChatDetailCoordinator?
  19. private let sections: [ProfileSections] = [.attachments, .members, .chatActions]
  20. private var currentUser: DcContact? {
  21. let myId = groupMemberIds.filter { DcContact(id: $0).email == DcConfig.addr }.first
  22. guard let currentUserId = myId else {
  23. return nil
  24. }
  25. return DcContact(id: currentUserId)
  26. }
  27. fileprivate var chat: DcChat
  28. // stores contactIds
  29. private var groupMemberIds: [Int] = []
  30. // MARK: - subviews
  31. private lazy var editBarButtonItem: UIBarButtonItem = {
  32. UIBarButtonItem(title: String.localized("global_menu_edit_desktop"), style: .plain, target: self, action: #selector(editButtonPressed))
  33. }()
  34. lazy var tableView: UITableView = {
  35. let table = UITableView(frame: .zero, style: .grouped)
  36. table.bounces = false
  37. table.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
  38. table.register(ActionCell.self, forCellReuseIdentifier: "actionCell")
  39. table.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
  40. table.delegate = self
  41. table.dataSource = self
  42. table.tableHeaderView = groupHeader
  43. return table
  44. }()
  45. private lazy var groupHeader: ContactDetailHeader = {
  46. let header = ContactDetailHeader()
  47. header.updateDetails(
  48. title: chat.name,
  49. subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
  50. )
  51. if let img = chat.profileImage {
  52. header.setImage(img)
  53. } else {
  54. header.setBackupImage(name: chat.name, color: chat.color)
  55. }
  56. header.setVerified(isVerified: chat.isVerified)
  57. return header
  58. }()
  59. private lazy var archiveChatCell: ActionCell = {
  60. let cell = ActionCell()
  61. cell.actionTitle = chat.isArchived ? String.localized("menu_unarchive_chat") : String.localized("menu_archive_chat")
  62. cell.actionColor = UIColor.systemBlue
  63. cell.selectionStyle = .none
  64. return cell
  65. }()
  66. private lazy var leaveGroupCell: ActionCell = {
  67. let cell = ActionCell()
  68. cell.actionTitle = String.localized("menu_leave_group")
  69. cell.actionColor = UIColor.red
  70. return cell
  71. }()
  72. private lazy var deleteChatCell: ActionCell = {
  73. let cell = ActionCell()
  74. cell.actionTitle = String.localized("menu_delete_chat")
  75. cell.actionColor = UIColor.red
  76. cell.selectionStyle = .none
  77. return cell
  78. }()
  79. private lazy var galleryCell: UITableViewCell = {
  80. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  81. cell.textLabel?.text = String.localized("gallery")
  82. cell.accessoryType = .disclosureIndicator
  83. return cell
  84. }()
  85. private lazy var documentsCell: UITableViewCell = {
  86. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  87. cell.textLabel?.text = String.localized("documents")
  88. cell.accessoryType = .disclosureIndicator
  89. return cell
  90. }()
  91. init(chatId: Int, context: DcContext) {
  92. self.context = context
  93. chat = DcChat(id: chatId)
  94. super.init(nibName: nil, bundle: nil)
  95. setupSubviews()
  96. }
  97. required init?(coder _: NSCoder) {
  98. fatalError("init(coder:) has not been implemented")
  99. }
  100. private func setupSubviews() {
  101. view.addSubview(tableView)
  102. tableView.translatesAutoresizingMaskIntoConstraints = false
  103. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
  104. tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  105. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
  106. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  107. }
  108. // MARK: - lifecycle
  109. override func viewDidLoad() {
  110. super.viewDidLoad()
  111. title = String.localized("tab_group")
  112. navigationItem.rightBarButtonItem = editBarButtonItem
  113. groupHeader.frame = CGRect(0, 0, tableView.frame.width, ContactCell.cellHeight)
  114. }
  115. override func viewWillAppear(_ animated: Bool) {
  116. super.viewWillAppear(animated)
  117. updateGroupMembers()
  118. tableView.reloadData() // to display updates
  119. editBarButtonItem.isEnabled = currentUser != nil
  120. updateHeader()
  121. //update chat object, maybe chat name was edited
  122. chat = DcChat(id: chat.id)
  123. }
  124. // MARK: - update
  125. private func updateGroupMembers() {
  126. groupMemberIds = chat.contactIds
  127. tableView.reloadData()
  128. }
  129. private func updateHeader() {
  130. groupHeader.updateDetails(
  131. title: chat.name,
  132. subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
  133. )
  134. }
  135. // MARK: - actions
  136. @objc func editButtonPressed() {
  137. coordinator?.showGroupChatEdit(chat: chat)
  138. }
  139. private func toggleArchiveChat() {
  140. let archivedBefore = chat.isArchived
  141. context.archiveChat(chatId: chat.id, archive: !archivedBefore)
  142. if archivedBefore {
  143. archiveChatCell.actionTitle = String.localized("menu_archive_chat")
  144. } else {
  145. self.navigationController?.popToRootViewController(animated: false)
  146. }
  147. self.chat = DcChat(id: chat.id)
  148. }
  149. private func getGroupMemberIdFor(_ row: Int) -> Int {
  150. return groupMemberIds[row - memberManagementRows]
  151. }
  152. private func isMemberManagementRow(row: Int) -> Bool {
  153. return row < memberManagementRows
  154. }
  155. }
  156. // MARK: - UITableViewDelegate, UITableViewDataSource
  157. extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSource {
  158. func numberOfSections(in _: UITableView) -> Int {
  159. return sections.count
  160. }
  161. func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  162. let sectionType = sections[section]
  163. switch sectionType {
  164. case .attachments:
  165. return 2
  166. case .members:
  167. return groupMemberIds.count + memberManagementRows
  168. case .chatActions:
  169. return 3
  170. }
  171. }
  172. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  173. let sectionType = sections[indexPath.section]
  174. let row = indexPath.row
  175. switch sectionType {
  176. case .attachments, .chatActions:
  177. return Constants.defaultCellHeight
  178. case .members:
  179. switch row {
  180. case membersRowAddMembers, membersRowQrInvite:
  181. return Constants.defaultCellHeight
  182. default:
  183. return ContactCell.cellHeight
  184. }
  185. }
  186. }
  187. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  188. let row = indexPath.row
  189. let sectionType = sections[indexPath.section]
  190. switch sectionType {
  191. case .attachments:
  192. if row == attachmentsRowGallery {
  193. return galleryCell
  194. } else if row == attachmentsRowDocuments {
  195. return documentsCell
  196. }
  197. case .members:
  198. if row == membersRowAddMembers || row == membersRowQrInvite {
  199. guard let actionCell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath) as? ActionCell else {
  200. safe_fatalError("could not dequeue action cell")
  201. break
  202. }
  203. if row == membersRowAddMembers {
  204. actionCell.actionTitle = String.localized("group_add_members")
  205. actionCell.actionColor = UIColor.systemBlue
  206. } else if row == membersRowQrInvite {
  207. actionCell.actionTitle = String.localized("qrshow_join_group_title")
  208. actionCell.actionColor = UIColor.systemBlue
  209. }
  210. return actionCell
  211. }
  212. guard let contactCell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as? ContactCell else {
  213. safe_fatalError("could not dequeue contactCell cell")
  214. break
  215. }
  216. let cellData = ContactCellData(contactId: getGroupMemberIdFor(row))
  217. let cellViewModel = ContactCellViewModel(contactData: cellData)
  218. contactCell.updateCell(cellViewModel: cellViewModel)
  219. return contactCell
  220. case .chatActions:
  221. if row == chatActionsRowArchiveChat {
  222. return archiveChatCell
  223. } else if row == chatActionsRowLeaveGroup {
  224. return leaveGroupCell
  225. } else if row == chatActionsRowDeleteChat {
  226. return deleteChatCell
  227. }
  228. }
  229. // should never get here
  230. return UITableViewCell(frame: .zero)
  231. }
  232. func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
  233. let sectionType = sections[indexPath.section]
  234. let row = indexPath.row
  235. switch sectionType {
  236. case .attachments:
  237. if row == attachmentsRowGallery {
  238. coordinator?.showGallery()
  239. } else if row == attachmentsRowDocuments {
  240. coordinator?.showDocuments()
  241. }
  242. case .members:
  243. if row == membersRowAddMembers {
  244. coordinator?.showAddGroupMember(chatId: chat.id)
  245. } else if row == membersRowQrInvite {
  246. coordinator?.showQrCodeInvite(chatId: chat.id)
  247. } else {
  248. let member = getGroupMember(at: row)
  249. coordinator?.showContactDetail(of: member.id)
  250. }
  251. case .chatActions:
  252. if row == chatActionsRowArchiveChat {
  253. toggleArchiveChat()
  254. } else if row == chatActionsRowLeaveGroup {
  255. showLeaveGroupConfirmationAlert()
  256. } else if row == chatActionsRowDeleteChat {
  257. showDeleteChatConfirmationAlert()
  258. }
  259. }
  260. }
  261. func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  262. if sections[section] == .members {
  263. return String.localized("tab_members")
  264. }
  265. return nil
  266. }
  267. func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  268. return Constants.defaultHeaderHeight
  269. }
  270. func tableView(_: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  271. guard let currentUser = self.currentUser else {
  272. return false
  273. }
  274. let row = indexPath.row
  275. let sectionType = sections[indexPath.section]
  276. if sectionType == .members &&
  277. !isMemberManagementRow(row: row) &&
  278. getGroupMemberIdFor(row) != currentUser.id {
  279. return true
  280. }
  281. return false
  282. }
  283. func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
  284. guard let currentUser = self.currentUser else {
  285. return nil
  286. }
  287. let row = indexPath.row
  288. let sectionType = sections[indexPath.section]
  289. if sectionType == .members &&
  290. !isMemberManagementRow(row: row) &&
  291. getGroupMemberIdFor(row) != currentUser.id {
  292. // action set for members except for current user
  293. let delete = UITableViewRowAction(style: .destructive, title: String.localized("remove_desktop")) { [unowned self] _, indexPath in
  294. let contact = self.getGroupMember(at: row)
  295. let title = String.localizedStringWithFormat(String.localized("ask_remove_members"), contact.nameNAddr)
  296. let alert = UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
  297. alert.addAction(UIAlertAction(title: String.localized("remove_desktop"), style: .destructive, handler: { _ in
  298. let success = dc_remove_contact_from_chat(mailboxPointer, UInt32(self.chat.id), UInt32(contact.id))
  299. if success == 1 {
  300. self.removeGroupMemberFromTableAt(indexPath)
  301. }
  302. }))
  303. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  304. self.present(alert, animated: true, completion: nil)
  305. }
  306. delete.backgroundColor = UIColor.red
  307. return [delete]
  308. }
  309. return nil
  310. }
  311. private func getGroupMember(at row: Int) -> DcContact {
  312. return DcContact(id: getGroupMemberIdFor(row))
  313. }
  314. private func removeGroupMemberFromTableAt(_ indexPath: IndexPath) {
  315. self.groupMemberIds.remove(at: indexPath.row - memberManagementRows)
  316. self.tableView.deleteRows(at: [indexPath], with: .automatic)
  317. updateHeader() // to display correct group size
  318. }
  319. }
  320. // MARK: - alerts
  321. extension GroupChatDetailViewController {
  322. private func showDeleteChatConfirmationAlert() {
  323. let alert = UIAlertController(
  324. title: nil,
  325. message: String.localized("ask_delete_chat_desktop"),
  326. preferredStyle: .safeActionSheet
  327. )
  328. alert.addAction(UIAlertAction(title: String.localized("menu_delete_chat"), style: .destructive, handler: { _ in
  329. self.coordinator?.deleteChat()
  330. }))
  331. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  332. self.present(alert, animated: true, completion: nil)
  333. }
  334. private func showLeaveGroupConfirmationAlert() {
  335. if let userId = currentUser?.id {
  336. let alert = UIAlertController(title: String.localized("ask_leave_group"), message: nil, preferredStyle: .safeActionSheet)
  337. alert.addAction(UIAlertAction(title: String.localized("menu_leave_group"), style: .destructive, handler: { _ in
  338. dc_remove_contact_from_chat(mailboxPointer, UInt32(self.chat.id), UInt32(userId))
  339. self.editBarButtonItem.isEnabled = false
  340. self.updateGroupMembers()
  341. }))
  342. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  343. present(alert, animated: true, completion: nil)
  344. }
  345. }
  346. }