// // Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation protocol AddGroupMembersViewControllerDelegate: AnyObject { func addGroupMembersViewDidUpdate() } // MARK: - @objc public class AddGroupMembersViewController: BaseGroupMemberViewController { weak var addGroupMembersViewControllerDelegate: AddGroupMembersViewControllerDelegate? private let groupThread: TSGroupThread private let oldGroupModel: TSGroupModel private var newRecipientSet = OrderedSet() public required init(groupThread: TSGroupThread) { self.groupThread = groupThread self.oldGroupModel = groupThread.groupModel super.init() groupMemberViewDelegate = self } // MARK: - View Lifecycle @objc public override func viewDidLoad() { super.viewDidLoad() title = NSLocalizedString("ADD_GROUP_MEMBERS_VIEW_TITLE", comment: "The title for the 'add group members' view.") } // MARK: - fileprivate func updateNavbar() { if hasUnsavedChanges { navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("EDIT_GROUP_UPDATE_BUTTON", comment: "The title for the 'update group' button."), style: .plain, target: self, action: #selector(updateGroupPressed), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "update")) } else { navigationItem.rightBarButtonItem = nil } } @objc func updateGroupPressed() { showConfirmationAlert() } private func showConfirmationAlert() { let newMemberCount = newRecipientSet.count guard newMemberCount > 0 else { owsFailDebug("No new members.") return } let groupName = contactsManager.displayNameWithSneakyTransaction(thread: groupThread) let alertTitle: String let alertMessage: String let actionTitle: String let messageFormat = NSLocalizedString("ADD_GROUP_MEMBERS_VIEW_CONFIRM_ALERT_MESSAGE_%d", tableName: "PluralAware", comment: "Format for the message for the 'add group members' confirmation alert. Embeds {{ %1$@ number of new members, %2$@ name of the group. }}.") alertMessage = String.localizedStringWithFormat(messageFormat, newRecipientSet.count, groupName) alertTitle = String.localizedStringWithFormat(NSLocalizedString("ADD_GROUP_MEMBERS_VIEW_CONFIRM_ALERT_TITLE_%d", tableName: "PluralAware", comment: "Title for the 'add group members' confirmation alert."), newRecipientSet.count) actionTitle = String.localizedStringWithFormat(NSLocalizedString("ADD_GROUP_MEMBERS_ACTION_TITLE_%d", tableName: "PluralAware", comment: "Label for the 'add group members' button."), newRecipientSet.count) let actionSheet = ActionSheetController(title: alertTitle, message: alertMessage) actionSheet.addAction(ActionSheetAction(title: actionTitle, style: .default, handler: { [weak self] _ in self?.updateGroupThreadAndDismiss() })) actionSheet.addAction(OWSActionSheets.cancelAction) self.presentActionSheet(actionSheet) } } // MARK: - private extension AddGroupMembersViewController { func buildGroupUpdate() -> GroupManager.GroupUpdate? { let newUuids = newRecipientSet.orderedMembers .compactMap { recipient -> UUID? in if let uuid = recipient.address?.uuid, !oldGroupModel.groupMembership.isFullMember(uuid) { owsFailDebug("Missing UUID, or recipient is already in group!") return uuid } return nil } guard !newUuids.isEmpty else { owsFailDebug("No valid recipients") return nil } return .addNormalMembers(uuids: newUuids) } func updateGroupThreadAndDismiss() { let dismissAndUpdateDelegate = { [weak self] in guard let self = self else { return } self.addGroupMembersViewControllerDelegate?.addGroupMembersViewDidUpdate() self.navigationController?.popViewController(animated: true) } guard hasUnsavedChanges else { owsFailDebug("!hasUnsavedChanges.") return dismissAndUpdateDelegate() } guard let groupUpdateToPerform = buildGroupUpdate() else { let error = OWSAssertionError("Couldn't build group update.") GroupViewUtils.showUpdateErrorUI(error: error) return } GroupViewUtils.updateGroupWithActivityIndicator(fromViewController: self, updatePromiseBlock: { self.updateGroupThreadPromise(update: groupUpdateToPerform) }, completion: { _ in dismissAndUpdateDelegate() }) } func updateGroupThreadPromise(update: GroupManager.GroupUpdate) -> Promise { firstly { () -> Promise in return GroupManager.messageProcessingPromise(for: oldGroupModel, description: self.logTag) }.then(on: .global()) { _ in GroupManager.updateExistingGroup(existingGroupModel: self.oldGroupModel, update: update) }.asVoid() } } // MARK: - extension AddGroupMembersViewController: GroupMemberViewDelegate { var groupMemberViewRecipientSet: OrderedSet { newRecipientSet } var groupMemberViewHasUnsavedChanges: Bool { !newRecipientSet.isEmpty } var shouldTryToEnableGroupsV2ForMembers: Bool { groupThread.isGroupV2Thread } func groupMemberViewRemoveRecipient(_ recipient: PickedRecipient) { newRecipientSet.remove(recipient) updateNavbar() } func groupMemberViewAddRecipient(_ recipient: PickedRecipient) { newRecipientSet.append(recipient) updateNavbar() } func groupMemberViewCanAddRecipient(_ recipient: PickedRecipient) -> Bool { guard groupThread.isGroupV2Thread else { return true } guard let address = recipient.address else { owsFailDebug("Invalid recipient.") return false } return GroupManager.doesUserSupportGroupsV2(address: address) } func groupMemberViewShouldShowMemberCount() -> Bool { groupThread.isGroupV2Thread } func groupMemberViewGroupMemberCountForDisplay() -> Int { (oldGroupModel.groupMembership.allMembersOfAnyKind.count + newRecipientSet.count) } func groupMemberViewIsGroupFull_HardLimit() -> Bool { groupMemberViewGroupMemberCountForDisplay() >= GroupManager.groupsV2MaxGroupSizeHardLimit } func groupMemberViewIsGroupFull_RecommendedLimit() -> Bool { groupMemberViewGroupMemberCountForDisplay() >= GroupManager.groupsV2MaxGroupSizeRecommended } func groupMemberViewIsPreExistingMember(_ recipient: PickedRecipient, transaction: SDSAnyReadTransaction) -> Bool { guard let address = recipient.address else { owsFailDebug("Invalid recipient.") return false } let groupMembership = oldGroupModel.groupMembership if groupMembership.isFullMember(address) { return true } if groupMembership.isInvitedMember(address) || groupMembership.isRequestingMember(address) { // We can "add" pending or requesting members if they support gv2 // and we know their profile key credential. let canAddMember: Bool = { guard GroupManager.doesUserSupportGroupsV2(address: address) else { return false } return self.groupsV2.hasProfileKeyCredential(for: address, transaction: transaction) }() return !canAddMember } return false } func groupMemberViewIsGroupsV2Required() -> Bool { groupThread.isGroupV2Thread } func groupMemberViewDismiss() { navigationController?.popViewController(animated: true) } var isNewGroup: Bool { false } var groupThreadForGroupMemberView: TSGroupThread? { groupThread } }