GroupChatDetailViewController.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. import UIKit
  2. import DcCore
  3. class GroupChatDetailViewController: UIViewController {
  4. enum ProfileSections {
  5. case attachments
  6. case members
  7. case chatActions
  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 dcContext: DcContext
  18. private let sections: [ProfileSections] = [.attachments, .members, .chatActions]
  19. private var currentUser: DcContact? {
  20. let myId = groupMemberIds.filter { DcContact(id: $0).email == dcContext.addr }.first
  21. guard let currentUserId = myId else {
  22. return nil
  23. }
  24. return DcContact(id: currentUserId)
  25. }
  26. private var chatId: Int
  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.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
  37. table.register(ActionCell.self, forCellReuseIdentifier: "actionCell")
  38. table.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
  39. table.delegate = self
  40. table.dataSource = self
  41. table.tableHeaderView = groupHeader
  42. return table
  43. }()
  44. private lazy var groupHeader: ContactDetailHeader = {
  45. let header = ContactDetailHeader()
  46. header.updateDetails(
  47. title: chat.name,
  48. subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
  49. )
  50. if let img = chat.profileImage {
  51. header.setImage(img)
  52. } else {
  53. header.setBackupImage(name: chat.name, color: chat.color)
  54. }
  55. header.setVerified(isVerified: chat.isVerified)
  56. return header
  57. }()
  58. private lazy var archiveChatCell: ActionCell = {
  59. let cell = ActionCell()
  60. cell.actionTitle = chat.isArchived ? String.localized("menu_unarchive_chat") : String.localized("menu_archive_chat")
  61. cell.actionColor = UIColor.systemBlue
  62. cell.selectionStyle = .none
  63. return cell
  64. }()
  65. private lazy var leaveGroupCell: ActionCell = {
  66. let cell = ActionCell()
  67. cell.actionTitle = String.localized("menu_leave_group")
  68. cell.actionColor = UIColor.red
  69. return cell
  70. }()
  71. private lazy var deleteChatCell: ActionCell = {
  72. let cell = ActionCell()
  73. cell.actionTitle = String.localized("menu_delete_chat")
  74. cell.actionColor = UIColor.red
  75. cell.selectionStyle = .none
  76. return cell
  77. }()
  78. private lazy var galleryCell: UITableViewCell = {
  79. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  80. cell.textLabel?.text = String.localized("gallery")
  81. cell.accessoryType = .disclosureIndicator
  82. return cell
  83. }()
  84. private lazy var documentsCell: UITableViewCell = {
  85. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  86. cell.textLabel?.text = String.localized("documents")
  87. cell.accessoryType = .disclosureIndicator
  88. return cell
  89. }()
  90. init(chatId: Int, dcContext: DcContext) {
  91. self.dcContext = dcContext
  92. self.chatId = chatId
  93. chat = dcContext.getChat(chatId: 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. //update chat object, maybe chat name was edited
  118. chat = dcContext.getChat(chatId: chat.id)
  119. updateGroupMembers()
  120. tableView.reloadData() // to display updates
  121. editBarButtonItem.isEnabled = currentUser != nil
  122. updateHeader()
  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. if let img = chat.profileImage {
  135. groupHeader.setImage(img)
  136. } else {
  137. groupHeader.setBackupImage(name: chat.name, color: chat.color)
  138. }
  139. groupHeader.setVerified(isVerified: chat.isVerified)
  140. }
  141. // MARK: - actions
  142. @objc func editButtonPressed() {
  143. showGroupChatEdit(chat: chat)
  144. }
  145. private func toggleArchiveChat() {
  146. let archivedBefore = chat.isArchived
  147. dcContext.archiveChat(chatId: chat.id, archive: !archivedBefore)
  148. if archivedBefore {
  149. archiveChatCell.actionTitle = String.localized("menu_archive_chat")
  150. } else {
  151. self.navigationController?.popToRootViewController(animated: false)
  152. }
  153. self.chat = dcContext.getChat(chatId: chat.id)
  154. }
  155. private func getGroupMemberIdFor(_ row: Int) -> Int {
  156. return groupMemberIds[row - memberManagementRows]
  157. }
  158. private func isMemberManagementRow(row: Int) -> Bool {
  159. return row < memberManagementRows
  160. }
  161. // MARK: - coordinator
  162. private func showSingleChatEdit(contactId: Int) {
  163. let editContactController = EditContactController(dcContext: dcContext, contactIdForUpdate: contactId)
  164. navigationController?.pushViewController(editContactController, animated: true)
  165. }
  166. private func showAddGroupMember(chatId: Int) {
  167. let groupMemberViewController = AddGroupMembersViewController(chatId: chatId)
  168. navigationController?.pushViewController(groupMemberViewController, animated: true)
  169. }
  170. private func showQrCodeInvite(chatId: Int) {
  171. let qrInviteCodeController = QrInviteViewController(dcContext: dcContext, chatId: chatId)
  172. navigationController?.pushViewController(qrInviteCodeController, animated: true)
  173. }
  174. private func showGroupChatEdit(chat: DcChat) {
  175. let editGroupViewController = EditGroupViewController(dcContext: dcContext, chat: chat)
  176. navigationController?.pushViewController(editGroupViewController, animated: true)
  177. }
  178. private func showContactDetail(of contactId: Int) {
  179. let contactDetailController = ContactDetailViewController(dcContext: dcContext, contactId: contactId, chatId: nil)
  180. navigationController?.pushViewController(contactDetailController, animated: true)
  181. }
  182. private func showDocuments() {
  183. presentPreview(for: DC_MSG_FILE, messageType2: DC_MSG_AUDIO, messageType3: 0)
  184. }
  185. private func showGallery() {
  186. presentPreview(for: DC_MSG_IMAGE, messageType2: DC_MSG_GIF, messageType3: DC_MSG_VIDEO)
  187. }
  188. private func presentPreview(for messageType: Int32, messageType2: Int32, messageType3: Int32) {
  189. let messageIds = dcContext.getChatMedia(chatId: chatId, messageType: messageType, messageType2: messageType2, messageType3: messageType3)
  190. var mediaUrls: [URL] = []
  191. for messageId in messageIds {
  192. let message = DcMsg.init(id: messageId)
  193. if let url = message.fileURL {
  194. mediaUrls.insert(url, at: 0)
  195. }
  196. }
  197. let previewController = PreviewController(currentIndex: 0, urls: mediaUrls)
  198. navigationController?.pushViewController(previewController, animated: true)
  199. }
  200. private func deleteChat() {
  201. dcContext.deleteChat(chatId: chatId)
  202. // just pop to viewControllers - we've in chatlist or archive then
  203. // (no not use `navigationController?` here: popping self will make the reference becoming nil)
  204. if let navigationController = navigationController {
  205. navigationController.popViewController(animated: false)
  206. navigationController.popViewController(animated: true)
  207. }
  208. }
  209. }
  210. // MARK: - UITableViewDelegate, UITableViewDataSource
  211. extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSource {
  212. func numberOfSections(in _: UITableView) -> Int {
  213. return sections.count
  214. }
  215. func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  216. let sectionType = sections[section]
  217. switch sectionType {
  218. case .attachments:
  219. return 2
  220. case .members:
  221. return groupMemberIds.count + memberManagementRows
  222. case .chatActions:
  223. return 3
  224. }
  225. }
  226. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  227. let sectionType = sections[indexPath.section]
  228. let row = indexPath.row
  229. switch sectionType {
  230. case .attachments, .chatActions:
  231. return Constants.defaultCellHeight
  232. case .members:
  233. switch row {
  234. case membersRowAddMembers, membersRowQrInvite:
  235. return Constants.defaultCellHeight
  236. default:
  237. return ContactCell.cellHeight
  238. }
  239. }
  240. }
  241. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  242. let row = indexPath.row
  243. let sectionType = sections[indexPath.section]
  244. switch sectionType {
  245. case .attachments:
  246. if row == attachmentsRowGallery {
  247. return galleryCell
  248. } else if row == attachmentsRowDocuments {
  249. return documentsCell
  250. }
  251. case .members:
  252. if row == membersRowAddMembers || row == membersRowQrInvite {
  253. guard let actionCell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath) as? ActionCell else {
  254. safe_fatalError("could not dequeue action cell")
  255. break
  256. }
  257. if row == membersRowAddMembers {
  258. actionCell.actionTitle = String.localized("group_add_members")
  259. actionCell.actionColor = UIColor.systemBlue
  260. } else if row == membersRowQrInvite {
  261. actionCell.actionTitle = String.localized("qrshow_join_group_title")
  262. actionCell.actionColor = UIColor.systemBlue
  263. }
  264. return actionCell
  265. }
  266. guard let contactCell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as? ContactCell else {
  267. safe_fatalError("could not dequeue contactCell cell")
  268. break
  269. }
  270. let contactId: Int = getGroupMemberIdFor(row)
  271. let cellData = ContactCellData(
  272. contactId: contactId,
  273. chatId: dcContext.getChatIdByContactId(contactId)
  274. )
  275. let cellViewModel = ContactCellViewModel(contactData: cellData)
  276. contactCell.updateCell(cellViewModel: cellViewModel)
  277. return contactCell
  278. case .chatActions:
  279. if row == chatActionsRowArchiveChat {
  280. return archiveChatCell
  281. } else if row == chatActionsRowLeaveGroup {
  282. return leaveGroupCell
  283. } else if row == chatActionsRowDeleteChat {
  284. return deleteChatCell
  285. }
  286. }
  287. // should never get here
  288. return UITableViewCell(frame: .zero)
  289. }
  290. func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
  291. let sectionType = sections[indexPath.section]
  292. let row = indexPath.row
  293. switch sectionType {
  294. case .attachments:
  295. if row == attachmentsRowGallery {
  296. showGallery()
  297. } else if row == attachmentsRowDocuments {
  298. showDocuments()
  299. }
  300. case .members:
  301. if row == membersRowAddMembers {
  302. showAddGroupMember(chatId: chat.id)
  303. } else if row == membersRowQrInvite {
  304. showQrCodeInvite(chatId: chat.id)
  305. } else {
  306. let member = getGroupMember(at: row)
  307. showContactDetail(of: member.id)
  308. }
  309. case .chatActions:
  310. if row == chatActionsRowArchiveChat {
  311. toggleArchiveChat()
  312. } else if row == chatActionsRowLeaveGroup {
  313. showLeaveGroupConfirmationAlert()
  314. } else if row == chatActionsRowDeleteChat {
  315. showDeleteChatConfirmationAlert()
  316. }
  317. }
  318. }
  319. func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  320. if sections[section] == .members {
  321. return String.localized("tab_members")
  322. }
  323. return nil
  324. }
  325. func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  326. return Constants.defaultHeaderHeight
  327. }
  328. func tableView(_: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  329. guard let currentUser = self.currentUser else {
  330. return false
  331. }
  332. let row = indexPath.row
  333. let sectionType = sections[indexPath.section]
  334. if sectionType == .members &&
  335. !isMemberManagementRow(row: row) &&
  336. getGroupMemberIdFor(row) != currentUser.id {
  337. return true
  338. }
  339. return false
  340. }
  341. func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
  342. guard let currentUser = self.currentUser else {
  343. return nil
  344. }
  345. let row = indexPath.row
  346. let sectionType = sections[indexPath.section]
  347. if sectionType == .members &&
  348. !isMemberManagementRow(row: row) &&
  349. getGroupMemberIdFor(row) != currentUser.id {
  350. // action set for members except for current user
  351. let delete = UITableViewRowAction(style: .destructive, title: String.localized("remove_desktop")) { [weak self] _, indexPath in
  352. guard let self = self else { return }
  353. let contact = self.getGroupMember(at: row)
  354. let title = String.localizedStringWithFormat(String.localized("ask_remove_members"), contact.nameNAddr)
  355. let alert = UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
  356. alert.addAction(UIAlertAction(title: String.localized("remove_desktop"), style: .destructive, handler: { _ in
  357. let success = self.dcContext.removeContactFromChat(chatId: self.chat.id, contactId: contact.id)
  358. if success {
  359. self.removeGroupMemberFromTableAt(indexPath)
  360. }
  361. }))
  362. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  363. self.present(alert, animated: true, completion: nil)
  364. }
  365. delete.backgroundColor = UIColor.red
  366. return [delete]
  367. }
  368. return nil
  369. }
  370. private func getGroupMember(at row: Int) -> DcContact {
  371. return DcContact(id: getGroupMemberIdFor(row))
  372. }
  373. private func removeGroupMemberFromTableAt(_ indexPath: IndexPath) {
  374. self.groupMemberIds.remove(at: indexPath.row - memberManagementRows)
  375. self.tableView.deleteRows(at: [indexPath], with: .automatic)
  376. updateHeader() // to display correct group size
  377. }
  378. }
  379. // MARK: - alerts
  380. extension GroupChatDetailViewController {
  381. private func showDeleteChatConfirmationAlert() {
  382. let alert = UIAlertController(
  383. title: nil,
  384. message: String.localized("ask_delete_chat_desktop"),
  385. preferredStyle: .safeActionSheet
  386. )
  387. alert.addAction(UIAlertAction(title: String.localized("menu_delete_chat"), style: .destructive, handler: { _ in
  388. self.deleteChat()
  389. }))
  390. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  391. self.present(alert, animated: true, completion: nil)
  392. }
  393. private func showLeaveGroupConfirmationAlert() {
  394. if let userId = currentUser?.id {
  395. let alert = UIAlertController(title: String.localized("ask_leave_group"), message: nil, preferredStyle: .safeActionSheet)
  396. alert.addAction(UIAlertAction(title: String.localized("menu_leave_group"), style: .destructive, handler: { _ in
  397. _ = self.dcContext.removeContactFromChat(chatId: self.chat.id, contactId: userId)
  398. self.editBarButtonItem.isEnabled = false
  399. self.updateGroupMembers()
  400. }))
  401. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  402. present(alert, animated: true, completion: nil)
  403. }
  404. }
  405. }