Signal-iOS/SignalUI/ViewControllers/Member Picker/BaseMemberViewController.swift
Max Radermacher ef1bd4064f Don’t fetch UUIDs when opening BaseMemberVC
(Also, don’t call out recipients that don’t support New Groups.)

We try to populate this picker with users we believe are registered. Now
that GV2 is fully rolled out, all the recipients we believe are
registered should have a UUID. (If any don’t, we’ll resolve that on
launch via UUIDBackfillTask.) Since these proactive fetches *shouldn’t*
run, prevent them from running by removing them entirely.

If we hit a rare race condition where a recipient is missing a UUID,
we’ll still perform a lookup when trying to select that recipient.

In these rare edge cases where a recipient doesn’t have a UUID (which,
again, shouldn’t happen), the subtitle indicating that they don’t
support New Groups will be out of date. By removing proactive fetches,
it’ll remain out of date until the user taps the row. To avoid
confusion, defer showing an error until the user taps on the recipient.
2022-10-25 00:41:24 -07:00

484 lines
17 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public protocol MemberViewDelegate: AnyObject {
var memberViewRecipientSet: OrderedSet<PickedRecipient> { get }
var memberViewHasUnsavedChanges: Bool { get }
func memberViewRemoveRecipient(_ recipient: PickedRecipient)
func memberViewAddRecipient(_ recipient: PickedRecipient)
func memberViewCanAddRecipient(_ recipient: PickedRecipient) -> Bool
func memberViewWillRenderRecipient(_ recipient: PickedRecipient)
func memberViewPrepareToSelectRecipient(_ recipient: PickedRecipient) -> AnyPromise
func memberViewNoUuidSubtitleForRecipient(_ recipient: PickedRecipient) -> String?
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: -
@objc
open class BaseMemberViewController: RecipientPickerContainerViewController {
// This delegate is the subclass.
public weak var memberViewDelegate: MemberViewDelegate?
private var recipientSet: OrderedSet<PickedRecipient> {
guard let memberViewDelegate = memberViewDelegate else {
owsFailDebug("Missing memberViewDelegate.")
return OrderedSet<PickedRecipient>()
}
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
@objc
open override func viewDidLoad() {
super.viewDidLoad()
// First section.
memberBar.delegate = self
// Don't use dynamic type in this label.
memberCountLabel.font = UIFont.ows_dynamicTypeBody2.withSize(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 = .showNoGroups
recipientPicker.allowsSelectingUnregisteredPhoneNumbers = false
recipientPicker.shouldUseAsyncSelection = true
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)
autoPinView(toBottomOfViewControllerOrKeyboard: recipientPicker.view, avoidNotch: false)
updateMemberCount()
}
@objc
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 { transaction in
self.orderedMembers(shouldSort: false, transaction: transaction)
})
}
private func orderedMembers(shouldSort: Bool, transaction: SDSAnyReadTransaction) -> [NewMember] {
return Self.orderedMembers(recipientSet: recipientSet, shouldSort: shouldSort, transaction: transaction)
}
public class func orderedMembers(recipientSet: OrderedSet<PickedRecipient>,
shouldSort: Bool,
transaction: SDSAnyReadTransaction) -> [NewMember] {
var members = recipientSet.orderedMembers.compactMap { (recipient: PickedRecipient) -> NewMember? in
guard let address = recipient.address else {
owsFailDebug("Invalid recipient.")
return nil
}
let displayName = self.contactsManager.displayName(for: address, transaction: transaction)
let shortDisplayName = self.contactsManager.shortDisplayName(for: address, transaction: transaction)
let comparableName = self.contactsManager.comparableName(for: address, transaction: transaction)
return NewMember(recipient: recipient,
address: address,
displayName: displayName,
shortName: shortDisplayName,
comparableName: comparableName)
}
if shouldSort {
members.sort { (left, right) in
return left.comparableName < right.comparableName
}
}
return members
}
// 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 { readTx -> (Bool, Bool) in
let isPreexisting = memberViewDelegate.memberViewIsPreExistingMember(
recipient,
transaction: readTx)
let isBlocked = blockingManager.isAddressBlocked(address, transaction: readTx)
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 {
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 didShowSNAlert = SafetyNumberConfirmationSheet.presentIfNecessary(
address: address,
confirmationText: confirmationText
) { didConfirmIdentity in
guard didConfirmIdentity else { return }
addRecipientCompletion()
}
if didShowSNAlert {
return
}
addRecipientCompletion()
}
}
public func recipientPicker(
_ recipientPickerViewController: RecipientPickerViewController,
willRenderRecipient recipient: PickedRecipient
) {
guard let memberViewDelegate = memberViewDelegate else {
owsFailDebug("Missing delegate.")
return
}
memberViewDelegate.memberViewWillRenderRecipient(recipient)
}
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,
accessoryMessageForRecipient recipient: PickedRecipient,
transaction: SDSAnyReadTransaction
) -> String? { nil }
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) ?? "check-circle-solid-24"
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("empty-circle-outline-24", 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 let memberViewDelegate = memberViewDelegate else {
owsFailDebug("Missing memberViewDelegate.")
return nil
}
if address.uuid == nil, let warning = memberViewDelegate.memberViewNoUuidSubtitleForRecipient(recipient) {
return NSAttributedString(string: warning)
}
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 recipientPickerTableViewWillBeginDragging(_ recipientPickerViewController: RecipientPickerViewController) {}
public func recipientPickerNewGroupButtonWasPressed() {}
public func recipientPickerCustomHeaderViews() -> [UIView] {
return [memberBar, memberCountWrapper]
}
}
// MARK: -
extension BaseMemberViewController: OWSNavigationView {
public func shouldCancelNavigationBack() -> Bool {
let hasUnsavedChanges = self.hasUnsavedChanges
if hasUnsavedChanges {
backButtonPressed()
}
return hasUnsavedChanges
}
}
// MARK: -
extension BaseMemberViewController: NewMembersBarDelegate {
}