// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit public protocol MemberViewDelegate: AnyObject { var memberViewRecipientSet: OrderedSet { get } var memberViewHasUnsavedChanges: Bool { get } func memberViewRemoveRecipient(_ recipient: PickedRecipient) func memberViewAddRecipient(_ recipient: PickedRecipient) func memberViewCanAddRecipient(_ recipient: PickedRecipient) -> Bool func memberViewPrepareToSelectRecipient(_ recipient: PickedRecipient) -> AnyPromise func memberViewShouldShowMemberCount() -> Bool func memberViewShouldAllowBlockedSelection() -> Bool func memberViewMemberCountForDisplay() -> Int func memberViewIsPreExistingMember(_ recipient: PickedRecipient, transaction: SDSAnyReadTransaction) -> Bool func memberViewCustomIconNameForPickedMember(_ recipient: PickedRecipient) -> String? func memberViewCustomIconColorForPickedMember(_ recipient: PickedRecipient) -> UIColor? func memberViewDismiss() } // MARK: - open class BaseMemberViewController: RecipientPickerContainerViewController { // This delegate is the subclass. public weak var memberViewDelegate: MemberViewDelegate? private var recipientSet: OrderedSet { guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing memberViewDelegate.") return OrderedSet() } return memberViewDelegate.memberViewRecipientSet } open var hasUnsavedChanges: Bool { guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing memberViewDelegate.") return false } return memberViewDelegate.memberViewHasUnsavedChanges } private let memberBar = NewMembersBar() private let memberCountLabel = UILabel() private let memberCountWrapper = UIView() public override init() { super.init() } // MARK: - View Lifecycle open override func viewDidLoad() { super.viewDidLoad() // First section. memberBar.delegate = self // Don't use dynamic type in this label. memberCountLabel.font = UIFont.regularFont(ofSize: 12) memberCountLabel.textColor = Theme.isDarkThemeEnabled ? .ows_gray05 : .ows_gray60 memberCountLabel.textAlignment = CurrentAppContext().isRTL ? .left : .right memberCountWrapper.addSubview(memberCountLabel) memberCountLabel.autoPinEdgesToSuperviewMargins() memberCountWrapper.layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12) recipientPicker.groupsToShow = .noGroups recipientPicker.delegate = self addChild(recipientPicker) view.addSubview(recipientPicker.view) recipientPicker.view.autoPin(toTopLayoutGuideOf: self, withInset: 0) recipientPicker.view.autoPinEdge(toSuperviewSafeArea: .leading) recipientPicker.view.autoPinEdge(toSuperviewSafeArea: .trailing) recipientPicker.view.autoPinEdge(.bottom, to: .bottom, of: keyboardLayoutGuideView) updateMemberCount() } open override func viewWillLayoutSubviews() { updateMemberBarHeightConstraint() super.viewWillLayoutSubviews() } private func updateMemberBarHeightConstraint() { memberBar.updateHeightConstraint() } private func updateMemberCount() { guard !recipientSet.isEmpty else { memberCountWrapper.isHidden = true return } guard let memberViewDelegate = memberViewDelegate, memberViewDelegate.memberViewShouldShowMemberCount() else { memberCountWrapper.isHidden = true return } memberCountWrapper.isHidden = false let format = OWSLocalizedString("GROUP_MEMBER_COUNT_WITHOUT_LIMIT_%d", tableName: "PluralAware", comment: "Format string for the group member count indicator. Embeds {{ the number of members in the group }}.") let memberCount = memberViewDelegate.memberViewMemberCountForDisplay() memberCountLabel.text = String.localizedStringWithFormat(format, memberCount) if memberCount >= GroupManager.groupsV2MaxGroupSizeRecommended { memberCountLabel.textColor = .ows_accentRed } else { memberCountLabel.textColor = Theme.primaryTextColor } } public func removeRecipient(_ recipient: PickedRecipient) { guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing memberViewDelegate.") return } memberViewDelegate.memberViewRemoveRecipient(recipient) recipientPicker.pickedRecipients = recipientSet.orderedMembers updateMemberBar() updateMemberCount() } public func addRecipient(_ recipient: PickedRecipient) { guard !recipientSet.contains(recipient) else { owsFailDebug("Recipient already added.") return } guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing memberViewDelegate.") return } guard memberViewDelegate.memberViewCanAddRecipient(recipient) else { return } memberViewDelegate.memberViewAddRecipient(recipient) recipientPicker.pickedRecipients = recipientSet.orderedMembers recipientPicker.clearSearchText() updateMemberBar() updateMemberCount() memberBar.scrollToRecipient(recipient) } private func updateMemberBar() { memberBar.setMembers(databaseStorage.read { tx in let members = self.recipientSet.orderedMembers.compactMap { (pickedRecipient) -> (PickedRecipient, SignalServiceAddress)? in guard let address = pickedRecipient.address else { return nil } return (pickedRecipient, address) } let displayNames = contactsManager.displayNames(for: members.map { (_, address) in address }, tx: tx) return zip(members, displayNames).map { (member, displayName) in return NewMember( recipient: member.0, address: member.1, shortName: displayName.resolvedValue(useShortNameIfAvailable: true) ) } }) } public class func sortedMemberAddresses( recipientSet: OrderedSet, tx: SDSAnyReadTransaction ) -> [SignalServiceAddress] { return contactsManager.sortSignalServiceAddresses( recipientSet.orderedMembers.compactMap { $0.address }, transaction: tx ) } // MARK: - open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) recipientPicker.pickedRecipients = recipientSet.orderedMembers updateMemberBar() updateMemberCount() guard let navigationController = navigationController else { owsFailDebug("Missing navigationController.") return } if navigationController.viewControllers.count == 1 { navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissPressed)) } } @objc open func dismissPressed() { if !self.hasUnsavedChanges { // If user made no changes, dismiss. self.memberViewDelegate?.memberViewDismiss() return } OWSActionSheets.showPendingChangesActionSheet { [weak self] in self?.memberViewDelegate?.memberViewDismiss() } } // MARK: - Event Handling private func backButtonPressed() { guard let navigationController = navigationController else { owsFailDebug("Missing navigationController.") return } if !hasUnsavedChanges { // If user made no changes, return to previous view. navigationController.popViewController(animated: true) return } OWSActionSheets.showPendingChangesActionSheet { [weak self] in self?.memberViewDelegate?.memberViewDismiss() } } } // MARK: - extension BaseMemberViewController: RecipientPickerDelegate { public func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, getRecipientState recipient: PickedRecipient ) -> RecipientPickerRecipientState { guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing memberViewDelegate.") return .unknownError } return Self.databaseStorage.read { transaction -> RecipientPickerRecipientState in if memberViewDelegate.memberViewIsPreExistingMember( recipient, transaction: transaction ) { return .duplicateGroupMember } return .canBeSelected } } public func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, didSelectRecipient recipient: PickedRecipient ) { guard let address = recipient.address else { owsFailDebug("Missing address.") return } guard address.isValid else { owsFailDebug("Invalid address.") return } guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing memberViewDelegate.") return } let (isPreExistingMember, isBlocked) = databaseStorage.read { tx -> (Bool, Bool) in let isPreexisting = memberViewDelegate.memberViewIsPreExistingMember( recipient, transaction: tx) let isBlocked = blockingManager.isAddressBlocked(address, transaction: tx) return (isPreexisting, isBlocked) } guard !isPreExistingMember else { owsFailDebug("Can't re-add pre-existing member.") return } guard let navigationController = navigationController else { owsFailDebug("Missing navigationController.") return } let isCurrentMember = recipientSet.contains(recipient) let addRecipientCompletion = { [weak self] in guard let self = self else { return } self.addRecipient(recipient) navigationController.popToViewController(self, animated: true) } if isCurrentMember { removeRecipient(recipient) } else if isBlocked && !memberViewDelegate.memberViewShouldAllowBlockedSelection() { BlockListUIUtils.showUnblockAddressActionSheet(address, from: self) { isStillBlocked in if !isStillBlocked { addRecipientCompletion() } } } else { confirmSafetyNumber(for: address, untrustedThreshold: nil, thenAddRecipient: addRecipientCompletion) } } private func confirmSafetyNumber( for address: SignalServiceAddress, untrustedThreshold: Date?, thenAddRecipient addRecipient: @escaping () -> Void ) { let confirmationText = OWSLocalizedString( "SAFETY_NUMBER_CHANGED_CONFIRM_ADD_MEMBER_ACTION", comment: "button title to confirm adding a recipient when their safety number has recently changed" ) let newUntrustedThreshold = Date() let didShowSNAlert = SafetyNumberConfirmationSheet.presentIfNecessary( addresses: [address], confirmationText: confirmationText, untrustedThreshold: untrustedThreshold ) { [weak self] didConfirmIdentity in guard didConfirmIdentity else { return } self?.confirmSafetyNumber(for: address, untrustedThreshold: newUntrustedThreshold, thenAddRecipient: addRecipient) } if didShowSNAlert { return } addRecipient() } public func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, prepareToSelectRecipient recipient: PickedRecipient ) -> AnyPromise { guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing delegate.") return AnyPromise(Promise.value(())) } return memberViewDelegate.memberViewPrepareToSelectRecipient(recipient) } public func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, accessoryViewForRecipient recipient: PickedRecipient, transaction: SDSAnyReadTransaction ) -> ContactCellAccessoryView? { guard let address = recipient.address else { owsFailDebug("Missing address.") return nil } guard address.isValid else { owsFailDebug("Invalid address.") return nil } guard let memberViewDelegate = memberViewDelegate else { owsFailDebug("Missing memberViewDelegate.") return nil } let isCurrentMember = recipientSet.contains(recipient) let isPreExistingMember = memberViewDelegate.memberViewIsPreExistingMember(recipient, transaction: transaction) let pickedIconName = memberViewDelegate.memberViewCustomIconNameForPickedMember(recipient) ?? Theme.iconName(.checkCircleFill) let pickedIconColor = memberViewDelegate.memberViewCustomIconColorForPickedMember(recipient) ?? Theme.accentBlueColor let imageView = CVImageView() if isPreExistingMember { imageView.setTemplateImageName(pickedIconName, tintColor: Theme.washColor) } else if isCurrentMember { imageView.setTemplateImageName(pickedIconName, tintColor: pickedIconColor) } else { imageView.setTemplateImageName(Theme.iconName(.circle), tintColor: .ows_gray25) } return ContactCellAccessoryView(accessoryView: imageView, size: .square(24)) } public func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, attributedSubtitleForRecipient recipient: PickedRecipient, transaction: SDSAnyReadTransaction ) -> NSAttributedString? { guard let address = recipient.address else { owsFailDebug("Recipient missing address.") return nil } guard !address.isLocalAddress else { return nil } guard let bioForDisplay = Self.profileManagerImpl.profileBioForDisplay(for: address, transaction: transaction) else { return nil } return NSAttributedString(string: bioForDisplay) } public func recipientPickerCustomHeaderViews() -> [UIView] { return [memberBar, memberCountWrapper] } } // MARK: - extension BaseMemberViewController { public var shouldCancelNavigationBack: Bool { let hasUnsavedChanges = self.hasUnsavedChanges if hasUnsavedChanges { backButtonPressed() } return hasUnsavedChanges } } // MARK: - extension BaseMemberViewController: NewMembersBarDelegate { }