GroupChatDetailViewController.swift 23 KB

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