From 2260eb9b8fa3032baeff4dd3f8f9d5dfe41577e7 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Fri, 29 May 2026 06:21:35 -0700 Subject: [PATCH] Convert ContactCellConfiguration and ContactCellAccessoryView to structs. --- .../UserInterface/NewCallViewController.swift | 13 +- .../BlockingAnnouncementOnlyView.swift | 2 +- .../Privacy/BlockListViewController.swift | 5 +- .../GroupStorySettingsViewController.swift | 5 +- .../PrivateStorySettingsViewController.swift | 5 +- .../StoryPrivacySettingsViewController.swift | 8 +- .../HomeView/Stories/StoryInfoSheet.swift | 10 +- .../MemberLabelViewController.swift | 2 +- .../MessageDetailViewController.swift | 12 +- .../NewGroupConfirmViewController.swift | 5 +- .../Polls/PollDetailsViewController.swift | 2 +- ...ationSettingsViewController+Contents.swift | 2 +- ...mberRequestsAndInvitesViewController.swift | 14 +- .../ReplaceAdminViewController.swift | 7 +- .../BaseMemberViewController.swift | 6 +- .../RecipientPickers/ContactCellView.swift | 244 ++++++++---------- .../ContactTableViewCell.swift | 6 +- .../RecipientPickers/ConversationPicker.swift | 18 +- .../RecipientPickerDelegate.swift | 8 +- .../RecipientPickerViewController.swift | 8 +- .../SafetyNumberConfirmationSheet.swift | 6 +- ...NewPrivateStoryConfirmViewController.swift | 2 +- 22 files changed, 180 insertions(+), 210 deletions(-) diff --git a/Signal/Calls/UserInterface/NewCallViewController.swift b/Signal/Calls/UserInterface/NewCallViewController.swift index 03b4e948d6..65a611e961 100644 --- a/Signal/Calls/UserInterface/NewCallViewController.swift +++ b/Signal/Calls/UserInterface/NewCallViewController.swift @@ -125,6 +125,7 @@ extension NewCallViewController: RecipientContextMenuHelperDelegate { // MARK: - RecipientPickerDelegate extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelegate { + func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, selectionStyleForRecipient recipient: PickedRecipient, @@ -133,7 +134,10 @@ extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelega return .default } - func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController, didSelectRecipient recipient: PickedRecipient) { + func recipientPicker( + _ recipientPickerViewController: RecipientPickerViewController, + didSelectRecipient recipient: PickedRecipient, + ) { switch recipient.identifier { case let .address(address): let thread = TSContactThread.getOrCreateThread(contactAddress: address) @@ -143,7 +147,12 @@ extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelega } } - func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController, accessoryViewForRecipient recipient: PickedRecipient, transaction: DBReadTransaction) -> ContactCellAccessoryView? { + func recipientPicker( + _ recipientPickerViewController: RecipientPickerViewController, + contactCellAccessoryForRecipient recipient: PickedRecipient, + transaction: DBReadTransaction, + ) -> ContactCellView.Accessory? { + let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = 20 diff --git a/Signal/ConversationView/BlockingAnnouncementOnlyView.swift b/Signal/ConversationView/BlockingAnnouncementOnlyView.swift index 6e795596fc..fe71c33887 100644 --- a/Signal/ConversationView/BlockingAnnouncementOnlyView.swift +++ b/Signal/ConversationView/BlockingAnnouncementOnlyView.swift @@ -159,7 +159,7 @@ class MessageUserSubsetSheet: OWSTableSheetViewController { cell.selectionStyle = .none - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asLocalUser) + var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asLocalUser) configuration.forceDarkAppearance = self?.forceDarkMode ?? false if diff --git a/Signal/src/ViewControllers/AppSettings/Privacy/BlockListViewController.swift b/Signal/src/ViewControllers/AppSettings/Privacy/BlockListViewController.swift index 0b4e78eb5f..21a7c77bb8 100644 --- a/Signal/src/ViewControllers/AppSettings/Privacy/BlockListViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Privacy/BlockListViewController.swift @@ -95,10 +95,7 @@ class BlockListViewController: OWSTableViewController2 { OWSTableItem( dequeueCellBlock: { [weak self] tableView in let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableViewCell.reuseIdentifier) as! ContactTableViewCell - let config = ContactCellConfiguration( - address: address, - localUserDisplayMode: .asUser, - ) + let config = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser) if self != nil { SSKEnvironment.shared.databaseStorageRef.read { transaction in cell.configure(configuration: config, transaction: transaction) diff --git a/Signal/src/ViewControllers/HomeView/Stories/Settings/GroupStorySettingsViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/Settings/GroupStorySettingsViewController.swift index c543f604b6..d991f9ba1d 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Settings/GroupStorySettingsViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Settings/GroupStorySettingsViewController.swift @@ -89,10 +89,7 @@ class GroupStorySettingsViewController: OWSTableViewController2 { return UITableViewCell() } - SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: viewerAddress, localUserDisplayMode: .asLocalUser) - cell.configure(configuration: configuration, transaction: transaction) - } + cell.configureWithSneakyTransaction(address: viewerAddress, localUserDisplayMode: .asLocalUser) return cell }, actionBlock: { [weak self] in diff --git a/Signal/src/ViewControllers/HomeView/Stories/Settings/PrivateStorySettingsViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/Settings/PrivateStorySettingsViewController.swift index cb67bec482..1051c09f6e 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Settings/PrivateStorySettingsViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Settings/PrivateStorySettingsViewController.swift @@ -121,10 +121,7 @@ final class PrivateStorySettingsViewController: OWSTableViewController2 { return UITableViewCell() } - SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: viewerAddress, localUserDisplayMode: .asLocalUser) - cell.configure(configuration: configuration, transaction: transaction) - } + cell.configureWithSneakyTransaction(address: viewerAddress, localUserDisplayMode: .asLocalUser) return cell }, actionBlock: { [weak self] in diff --git a/Signal/src/ViewControllers/HomeView/Stories/Settings/StoryPrivacySettingsViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/Settings/StoryPrivacySettingsViewController.swift index e45b180e1c..f74f20594c 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Settings/StoryPrivacySettingsViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Settings/StoryPrivacySettingsViewController.swift @@ -253,7 +253,7 @@ private class StoryThreadCell: ContactTableViewCell { // MARK: - ContactTableViewCell func configure(conversationItem: StoryConversationItem, transaction: DBReadTransaction) { - let configuration: ContactCellConfiguration + var configuration: ContactCellView.Configuration switch conversationItem.messageRecipient { case .contact: owsFailDebug("Unexpected recipient for story") @@ -268,21 +268,21 @@ private class StoryThreadCell: ContactTableViewCell { owsFailDebug("Failed to find group thread") return } - configuration = ContactCellConfiguration(groupThread: groupThread, localUserDisplayMode: .noteToSelf) + configuration = ContactCellView.Configuration(groupThread: groupThread, localUserDisplayMode: .noteToSelf) case .privateStory(_, let isMyStory): if isMyStory { guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress else { owsFailDebug("Unexpectedly missing local address") return } - configuration = ContactCellConfiguration(address: localAddress, localUserDisplayMode: .asUser) + configuration = ContactCellView.Configuration(address: localAddress, localUserDisplayMode: .asUser) configuration.customName = conversationItem.title(transaction: transaction) } else { guard let image = conversationItem.image else { owsFailDebug("Unexpectedly missing image for private story") return } - configuration = ContactCellConfiguration(name: conversationItem.title(transaction: transaction), avatar: image) + configuration = ContactCellView.Configuration(name: conversationItem.title(transaction: transaction), avatar: image) } } diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoryInfoSheet.swift b/Signal/src/ViewControllers/HomeView/Stories/StoryInfoSheet.swift index 592ef006ab..3bb27a2a13 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoryInfoSheet.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoryInfoSheet.swift @@ -301,9 +301,9 @@ class StoryInfoSheet: OWSTableViewController2, DatabaseChangeDelegate, UIAdaptiv } SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser) + var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser) configuration.forceDarkAppearance = true - configuration.accessoryView = self.buildAccessoryView( + configuration.accessory = self.buildContactCellAccessory( text: accessoryText, transaction: transaction, ) @@ -325,10 +325,10 @@ class StoryInfoSheet: OWSTableViewController2, DatabaseChangeDelegate, UIAdaptiv }) } - private func buildAccessoryView( + private func buildContactCellAccessory( text: String, transaction: DBReadTransaction, - ) -> ContactCellAccessoryView { + ) -> ContactCellView.Accessory { let label = CVLabel() let labelConfig = CVLabelConfig.unstyledText( text, @@ -338,7 +338,7 @@ class StoryInfoSheet: OWSTableViewController2, DatabaseChangeDelegate, UIAdaptiv labelConfig.applyForRendering(label: label) let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: .greatestFiniteMagnitude) - return ContactCellAccessoryView(accessoryView: label, size: labelSize) + return .init(accessoryView: label, size: labelSize) } // MARK: - DatabaseChangeDelegate diff --git a/Signal/src/ViewControllers/MemberLabels/MemberLabelViewController.swift b/Signal/src/ViewControllers/MemberLabels/MemberLabelViewController.swift index c5bcca1f19..9e172af754 100644 --- a/Signal/src/ViewControllers/MemberLabels/MemberLabelViewController.swift +++ b/Signal/src/ViewControllers/MemberLabels/MemberLabelViewController.swift @@ -501,7 +501,7 @@ class MemberLabelViewController: OWSViewController, UITextFieldDelegate { for (memberAddress, memberLabel) in sortedNonLocalMembers { let cell = ContactCellView() SSKEnvironment.shared.databaseStorageRef.read { tx in - let configuration = ContactCellConfiguration(address: memberAddress, localUserDisplayMode: .asLocalUser) + var configuration = ContactCellView.Configuration(address: memberAddress, localUserDisplayMode: .asLocalUser) configuration.memberLabel = memberLabel diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 41ff4277ac..b724916d99 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -522,8 +522,8 @@ class MessageDetailViewController: OWSTableViewController2 { } SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser) - configuration.accessoryView = self.buildAccessoryView( + var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser) + configuration.accessory = self.buildContactCellAccessory( text: accessoryText, displayUDIndicator: displayUDIndicator, transaction: transaction, @@ -548,11 +548,11 @@ class MessageDetailViewController: OWSTableViewController2 { expiryLabel?.attributedText = expiryLabelAttributedText } - private func buildAccessoryView( + private func buildContactCellAccessory( text: String, displayUDIndicator: Bool, transaction: DBReadTransaction, - ) -> ContactCellAccessoryView { + ) -> ContactCellView.Accessory { let label = CVLabel() label.textAlignment = .right let labelConfig = CVLabelConfig.unstyledText( @@ -566,7 +566,7 @@ class MessageDetailViewController: OWSTableViewController2 { let shouldShowUD = SSKEnvironment.shared.preferencesRef.shouldShowUnidentifiedDeliveryIndicators(transaction: transaction) guard displayUDIndicator, shouldShowUD else { - return ContactCellAccessoryView(accessoryView: label, size: labelSize) + return .init(accessoryView: label, size: labelSize) } let imageView = CVImageView() @@ -589,7 +589,7 @@ class MessageDetailViewController: OWSTableViewController2 { ], ) let hStackSize = hStackMeasurement.measuredSize - return ContactCellAccessoryView(accessoryView: hStack, size: hStackSize) + return .init(accessoryView: hStack, size: hStackSize) } private static func valueLabelAttributedText(name: String, value: String) -> NSAttributedString { diff --git a/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift b/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift index 020d2df0b1..68393186d6 100644 --- a/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift +++ b/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift @@ -194,11 +194,8 @@ public class NewGroupConfirmViewController: OWSTableViewController2 { } cell.selectionStyle = .none + cell.configureWithSneakyTransaction(address: address, localUserDisplayMode: .asUser) - SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser) - cell.configure(configuration: configuration, transaction: transaction) - } return cell }, )) diff --git a/Signal/src/ViewControllers/Polls/PollDetailsViewController.swift b/Signal/src/ViewControllers/Polls/PollDetailsViewController.swift index 3a4000e661..93de680bbb 100644 --- a/Signal/src/ViewControllers/Polls/PollDetailsViewController.swift +++ b/Signal/src/ViewControllers/Polls/PollDetailsViewController.swift @@ -264,7 +264,7 @@ struct PollDetailsView: View { private func addressCell(address: SignalServiceAddress) -> ManualStackView? { let cell = ContactCellView() - let config = ContactCellConfiguration(address: address, localUserDisplayMode: .asLocalUser) + var config = ContactCellView.Configuration(address: address, localUserDisplayMode: .asLocalUser) config.avatarSizeClass = .twentyEight SSKEnvironment.shared.databaseStorageRef.read { transaction in diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift index 57b6cb8e0b..4c4fc86dcb 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift @@ -913,7 +913,7 @@ extension ConversationSettingsViewController { } SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: memberAddress, localUserDisplayMode: .asLocalUser) + var configuration = ContactCellView.Configuration(address: memberAddress, localUserDisplayMode: .asLocalUser) let isGroupAdmin = groupMembership.isFullMemberAndAdministrator(memberAddress) let isVerified = verificationState == .verified let isNoLongerVerified = verificationState == .noLongerVerified diff --git a/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift b/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift index 9c469c17a9..3c6cd80039 100644 --- a/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift @@ -172,12 +172,12 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController let cell = ContactTableViewCell(style: .default, reuseIdentifier: nil) SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asLocalUser) + var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asLocalUser) configuration.allowUserInteraction = true configuration.avatarSizeClass = .forty if canApproveMemberRequests { - configuration.accessoryView = self.buildMemberRequestButtons(address: address) + configuration.accessory = self.buildMemberRequestButtons(address: address) } if address.isLocalAddress { @@ -204,7 +204,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController contents.add(section) } - private func buildMemberRequestButtons(address: SignalServiceAddress) -> ContactCellAccessoryView { + private func buildMemberRequestButtons(address: SignalServiceAddress) -> ContactCellView.Accessory { let denyButton = UIButton( configuration: .roundGray(image: UIImage(resource: .x)), primaryAction: UIAction { [weak self] _ in @@ -242,7 +242,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController ], ) let stackSize = stackMeasurement.measuredSize - return ContactCellAccessoryView(accessoryView: stackView, size: stackSize) + return ContactCellView.Accessory(accessoryView: stackView, size: stackSize) } private func approveMemberRequest(address: SignalServiceAddress) { @@ -311,7 +311,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController cell.selectionStyle = canRevokeInvites ? .default : .none SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser) + var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser) configuration.avatarSizeClass = .forty cell.configure(configuration: configuration, transaction: transaction) } @@ -366,8 +366,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController cell.selectionStyle = canRevokeInvites ? .default : .none databaseStorage.read { transaction in - let configuration = ContactCellConfiguration(address: inviterAddress, localUserDisplayMode: .asUser) - configuration.avatarSizeClass = .forty + var configuration = ContactCellView.Configuration(address: inviterAddress, localUserDisplayMode: .asUser) let inviterName = contactManager.displayName(for: inviterAddress, tx: transaction).resolvedValue() let format = OWSLocalizedString( "PENDING_GROUP_MEMBERS_MEMBER_INVITED_USERS_%d", @@ -375,6 +374,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController comment: "Format for label indicating the a group member has invited N other users to the group. Embeds {{ %1$@ the number of users they have invited, %2$@ name of the inviting group member }}.", ) configuration.customName = String.localizedStringWithFormat(format, invitedAddresses.count, inviterName) + configuration.avatarSizeClass = .forty cell.configure(configuration: configuration, transaction: transaction) } diff --git a/Signal/src/ViewControllers/ThreadSettings/ReplaceAdminViewController.swift b/Signal/src/ViewControllers/ThreadSettings/ReplaceAdminViewController.swift index f4a737349d..8cfe73234c 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ReplaceAdminViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ReplaceAdminViewController.swift @@ -82,12 +82,7 @@ class ReplaceAdminViewController: OWSTableViewController2 { owsFailDebug("Missing cell.") return UITableViewCell() } - - SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser) - cell.configure(configuration: configuration, transaction: transaction) - } - + cell.configureWithSneakyTransaction(address: address, localUserDisplayMode: .asUser) return cell }, actionBlock: { [weak self] in diff --git a/SignalUI/RecipientPickers/BaseMemberViewController.swift b/SignalUI/RecipientPickers/BaseMemberViewController.swift index 658710d002..087c3912e9 100644 --- a/SignalUI/RecipientPickers/BaseMemberViewController.swift +++ b/SignalUI/RecipientPickers/BaseMemberViewController.swift @@ -399,9 +399,9 @@ extension BaseMemberViewController: RecipientPickerDelegate { public func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, - accessoryViewForRecipient recipient: PickedRecipient, + contactCellAccessoryForRecipient recipient: PickedRecipient, transaction: DBReadTransaction, - ) -> ContactCellAccessoryView? { + ) -> ContactCellView.Accessory? { guard let address = recipient.address else { owsFailDebug("Missing address.") return nil @@ -441,7 +441,7 @@ extension BaseMemberViewController: RecipientPickerDelegate { accessoryView = SelectionIndicatorView() } let accessoryViewWrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(accessoryView) - return ContactCellAccessoryView(accessoryView: accessoryViewWrapper, size: .square(24)) + return ContactCellView.Accessory(accessoryView: accessoryViewWrapper, size: .square(24)) } public func recipientPicker( diff --git a/SignalUI/RecipientPickers/ContactCellView.swift b/SignalUI/RecipientPickers/ContactCellView.swift index 0137269eaa..6e8b5d6fa5 100644 --- a/SignalUI/RecipientPickers/ContactCellView.swift +++ b/SignalUI/RecipientPickers/ContactCellView.swift @@ -3,102 +3,88 @@ // SPDX-License-Identifier: AGPL-3.0-only // -import Foundation public import SignalServiceKit -public class ContactCellAccessoryView: NSObject { - let accessoryView: UIView - let size: CGSize - - public init(accessoryView: UIView, size: CGSize) { - self.accessoryView = accessoryView - self.size = size - } -} - -// MARK: - - -public class ContactCellConfiguration: NSObject { - fileprivate enum CellDataSource { - case address(SignalServiceAddress) - case groupThread(TSGroupThread) - case `static`(name: String, avatar: UIImage) - } - - fileprivate let dataSource: CellDataSource - - public let localUserDisplayMode: LocalUserDisplayMode - - public var forceDarkAppearance = false - - public var accessoryMessage: String? - - public var customName: String? - - public var accessoryView: ContactCellAccessoryView? - - public var attributedSubtitle: NSAttributedString? - - public var shouldShowContactIcon = false - - public var allowUserInteraction = false - - public var badged = true // TODO: Badges — Default false? Configure each use-case? - - public var storyState: StoryContextViewState? - - public var hasAccessoryText: Bool { - accessoryMessage?.nilIfEmpty != nil - } - - public var avatarSizeClass: ConversationAvatarView.Configuration.SizeClass? - - public var memberLabel: MemberLabelForRendering? - - public init(address: SignalServiceAddress, localUserDisplayMode: LocalUserDisplayMode) { - self.dataSource = .address(address) - self.localUserDisplayMode = localUserDisplayMode - super.init() - } - - public init(groupThread: TSGroupThread, localUserDisplayMode: LocalUserDisplayMode) { - self.dataSource = .groupThread(groupThread) - self.localUserDisplayMode = localUserDisplayMode - super.init() - } - - public init(name: String, avatar: UIImage) { - self.dataSource = .static(name: name, avatar: avatar) - self.localUserDisplayMode = .asUser - super.init() - } - - public func useVerifiedSubtitle() { - let text = NSMutableAttributedString() - text.append(SignalSymbol.safetyNumber.attributedString(for: .caption1, clamped: true)) - text.append(" ", attributes: [:]) - text.append(SafetyNumberStrings.verified, attributes: [:]) - self.attributedSubtitle = text - } -} - -// MARK: - - public class ContactCellView: ManualStackView { - private var configuration: ContactCellConfiguration? { - didSet { - ensureObservers() + public struct Configuration { + fileprivate enum CellDataSource { + case address(SignalServiceAddress) + case groupThread(TSGroupThread) + case `static`(name: String, avatar: UIImage) + } + + fileprivate let dataSource: CellDataSource + + public let localUserDisplayMode: LocalUserDisplayMode + + public var forceDarkAppearance = false + + public var accessoryMessage: String? + + public var customName: String? + + public var accessory: ContactCellView.Accessory? + + public var attributedSubtitle: NSAttributedString? + + public var shouldShowContactIcon = false + + public var allowUserInteraction = false + + public var badged = true // TODO: Badges — Default false? Configure each use-case? + + public var storyState: StoryContextViewState? + + public var hasAccessoryText: Bool { + accessoryMessage?.nilIfEmpty != nil + } + + public var avatarSizeClass: ConversationAvatarView.Configuration.SizeClass? + + public var memberLabel: MemberLabelForRendering? + + public init(address: SignalServiceAddress, localUserDisplayMode: LocalUserDisplayMode) { + self.dataSource = .address(address) + self.localUserDisplayMode = localUserDisplayMode + } + + public init(groupThread: TSGroupThread, localUserDisplayMode: LocalUserDisplayMode) { + self.dataSource = .groupThread(groupThread) + self.localUserDisplayMode = localUserDisplayMode + } + + public init(name: String, avatar: UIImage) { + self.dataSource = .static(name: name, avatar: avatar) + self.localUserDisplayMode = .asUser + } + + public mutating func useVerifiedSubtitle() { + let text = NSMutableAttributedString() + text.append(SignalSymbol.safetyNumber.attributedString(for: .caption1, clamped: true)) + text.append(" ", attributes: [:]) + text.append(SafetyNumberStrings.verified, attributes: [:]) + self.attributedSubtitle = text + } + } + + public struct Accessory { + let accessoryView: UIView + let size: CGSize + + public init(accessoryView: UIView, size: CGSize) { + self.accessoryView = accessoryView + self.size = size } } public static var avatarSizeClass: ConversationAvatarView.Configuration.SizeClass { .thirtySix } - private var avatarDataSource: ConversationAvatarDataSource? { - switch configuration?.dataSource { + + private func avatarDataSource(configuration: Configuration) -> ConversationAvatarDataSource { + switch configuration.dataSource { case .groupThread(let thread): return .thread(thread) case .address(let address): return .address(address) case .static(_, let avatar): return .asset(avatar: avatar, badge: nil) - case nil: return nil } } @@ -165,15 +151,16 @@ public class ContactCellView: ManualStackView { } public func configure( - configuration: ContactCellConfiguration, + configuration: Configuration, transaction: DBReadTransaction, ) { - AssertIsOnMainThread() owsAssertDebug(!shouldDeactivateConstraints) - self.configuration = configuration + setupObservations(configuration: configuration) - self.isUserInteractionEnabled = configuration.allowUserInteraction + isUserInteractionEnabled = configuration.allowUserInteraction + + let avatarDataSource = avatarDataSource(configuration: configuration) avatarView.update(transaction) { config in config.dataSource = avatarDataSource @@ -189,16 +176,13 @@ public class ContactCellView: ManualStackView { } } - if - avatarDataSource?.isGroupAvatar ?? false, - let storyState = configuration.storyState - { + if avatarDataSource.isGroupAvatar, let storyState = configuration.storyState { // Group story. Add badge avatarView.addSubview(groupStoryBadgeView) let badgeColor: UIColor switch storyState { case .unviewed: - badgeColor = .ows_accentBlue + badgeColor = .Signal.accent case .viewed, .noStories: badgeColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray25 } @@ -277,17 +261,18 @@ public class ContactCellView: ManualStackView { rootStackSubviewInfos.append(textStackMeasurement.measuredSize.asManualSubviewInfo) } + var accessory = configuration.accessory if let accessoryMessage = configuration.accessoryMessage { accessoryLabel.text = accessoryMessage let labelSize = accessoryLabel.sizeThatFits(.square(.greatestFiniteMagnitude)) - configuration.accessoryView = ContactCellAccessoryView( + accessory = Accessory( accessoryView: accessoryLabel, size: labelSize, ) } - if let accessoryView = configuration.accessoryView { - rootStackSubviews.append(accessoryView.accessoryView) - rootStackSubviewInfos.append(accessoryView.size.asManualSubviewInfo(hasFixedSize: true)) + if let accessory { + rootStackSubviews.append(accessory.accessoryView) + rootStackSubviewInfos.append(accessory.size.asManualSubviewInfo(hasFixedSize: true)) } let rootStackConfig = ManualStackView.Config( @@ -310,32 +295,44 @@ public class ContactCellView: ManualStackView { // MARK: - Notifications - private func ensureObservers() { - NotificationCenter.default.removeObserver(self) - if case .address = configuration?.dataSource { - NotificationCenter.default.addObserver( - self, - selector: #selector(otherUsersProfileChanged(notification:)), + private var observation: NotificationCenter.Observer? + + private func setupObservations(configuration: Configuration) { + if let observation { + NotificationCenter.default.removeObserver(observation) + self.observation = nil + } + + if case .address = configuration.dataSource { + observation = NotificationCenter.default.addObserver( name: UserProfileNotifications.otherUsersProfileDidChange, - object: nil, - ) + ) { [weak self] notification in + guard + let changedAddress = notification.userInfo?[UserProfileNotifications.profileAddressKey] as? SignalServiceAddress, + changedAddress.isValid + else { + owsFailDebug("changedAddress was unexpectedly nil") + return + } + if case .address(changedAddress) = configuration.dataSource { + self?.updateNameLabelsWithSneakyTransaction(configuration: configuration) + } + } } } // MARK: - - private func updateNameLabelsWithSneakyTransaction(configuration: ContactCellConfiguration) { + private func updateNameLabelsWithSneakyTransaction(configuration: Configuration) { SSKEnvironment.shared.databaseStorageRef.read { transaction in updateNameLabels(configuration: configuration, transaction: transaction) } } private func updateNameLabels( - configuration: ContactCellConfiguration, + configuration: Configuration, transaction: DBReadTransaction, ) { - AssertIsOnMainThread() - let textColor = self.nameLabelColor(forceDarkAppearance: configuration.forceDarkAppearance) let nameString = { () -> NSAttributedString in @@ -389,9 +386,10 @@ public class ContactCellView: ManualStackView { override public func reset() { super.reset() - NotificationCenter.default.removeObserver(self) - - configuration = nil + if let observation { + NotificationCenter.default.removeObserver(observation) + self.observation = nil + } avatarView.reset() textStack.reset() @@ -400,24 +398,4 @@ public class ContactCellView: ManualStackView { subtitleLabel.text = nil accessoryLabel.text = nil } - - @objc - private func otherUsersProfileChanged(notification: Notification) { - AssertIsOnMainThread() - - guard let configuration = self.configuration else { - return - } - guard - let changedAddress = notification.userInfo?[UserProfileNotifications.profileAddressKey] as? SignalServiceAddress, - changedAddress.isValid - else { - owsFailDebug("changedAddress was unexpectedly nil") - return - } - - if case .address(changedAddress) = configuration.dataSource { - updateNameLabelsWithSneakyTransaction(configuration: configuration) - } - } } diff --git a/SignalUI/RecipientPickers/ContactTableViewCell.swift b/SignalUI/RecipientPickers/ContactTableViewCell.swift index c820da2910..fdcc40fcf8 100644 --- a/SignalUI/RecipientPickers/ContactTableViewCell.swift +++ b/SignalUI/RecipientPickers/ContactTableViewCell.swift @@ -56,7 +56,7 @@ open class ContactTableViewCell: UITableViewCell, ReusableTableViewCell { localUserDisplayMode: LocalUserDisplayMode, transaction: DBReadTransaction, ) { - let configuration = ContactCellConfiguration( + let configuration = ContactCellView.Configuration( address: address, localUserDisplayMode: localUserDisplayMode, ) @@ -68,7 +68,7 @@ open class ContactTableViewCell: UITableViewCell, ReusableTableViewCell { localUserDisplayMode: LocalUserDisplayMode, transaction: DBReadTransaction, ) { - let configuration = ContactCellConfiguration( + let configuration = ContactCellView.Configuration( address: thread.contactAddress, localUserDisplayMode: localUserDisplayMode, ) @@ -76,7 +76,7 @@ open class ContactTableViewCell: UITableViewCell, ReusableTableViewCell { } open func configure( - configuration: ContactCellConfiguration, + configuration: ContactCellView.Configuration, transaction: DBReadTransaction, ) { OWSTableItem.configureCell(self) diff --git a/SignalUI/RecipientPickers/ConversationPicker.swift b/SignalUI/RecipientPickers/ConversationPicker.swift index ba625ca3f5..38a242d148 100644 --- a/SignalUI/RecipientPickers/ConversationPicker.swift +++ b/SignalUI/RecipientPickers/ConversationPicker.swift @@ -1295,10 +1295,10 @@ class ConversationPickerCell: ContactTableViewCell { // MARK: - ContactTableViewCell func configure(conversationItem: ConversationItem, transaction: DBReadTransaction) { - let configuration: ContactCellConfiguration + var configuration: ContactCellView.Configuration switch conversationItem.messageRecipient { case .contact(let address): - configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .noteToSelf) + configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .noteToSelf) case .group(let groupThreadId): guard let groupThread = TSGroupThread.fetchGroupThreadViaCache( @@ -1309,27 +1309,27 @@ class ConversationPickerCell: ContactTableViewCell { owsFailDebug("Failed to find group thread") return } - configuration = ContactCellConfiguration(groupThread: groupThread, localUserDisplayMode: .noteToSelf) + configuration = ContactCellView.Configuration(groupThread: groupThread, localUserDisplayMode: .noteToSelf) case .privateStory(_, let isMyStory): if isMyStory { guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress else { owsFailDebug("Unexpectedly missing local address") return } - configuration = ContactCellConfiguration(address: localAddress, localUserDisplayMode: .asUser) + configuration = ContactCellView.Configuration(address: localAddress, localUserDisplayMode: .asUser) configuration.customName = conversationItem.title(transaction: transaction) } else { guard let image = conversationItem.image else { owsFailDebug("Unexpectedly missing image for private story") return } - configuration = ContactCellConfiguration(name: conversationItem.title(transaction: transaction), avatar: image) + configuration = ContactCellView.Configuration(name: conversationItem.title(transaction: transaction), avatar: image) } } if conversationItem.isBlocked { configuration.accessoryMessage = MessageStrings.conversationIsBlocked } else { - configuration.accessoryView = buildAccessoryView(disappearingMessagesConfig: conversationItem.disappearingMessagesConfig) + configuration.accessory = buildContactCellAccessory(disappearingMessagesConfig: conversationItem.disappearingMessagesConfig) } if let storyItem = conversationItem as? StoryConversationItem { @@ -1355,7 +1355,7 @@ class ConversationPickerCell: ContactTableViewCell { private lazy var selectionView = SelectionIndicatorView() - func buildAccessoryView(disappearingMessagesConfig: DisappearingMessagesConfigurationRecord?) -> ContactCellAccessoryView { + func buildContactCellAccessory(disappearingMessagesConfig: DisappearingMessagesConfigurationRecord?) -> ContactCellView.Accessory { selectionView.removeFromSuperview() let selectionWrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(selectionView) @@ -1364,7 +1364,7 @@ class ConversationPickerCell: ContactTableViewCell { let disappearingMessagesConfig, disappearingMessagesConfig.isEnabled else { - return ContactCellAccessoryView( + return ContactCellView.Accessory( accessoryView: selectionWrapper, size: selectionView.intrinsicContentSize, ) @@ -1389,7 +1389,7 @@ class ConversationPickerCell: ContactTableViewCell { ], ) let stackSize = stackMeasurement.measuredSize - return ContactCellAccessoryView(accessoryView: stackView, size: stackSize) + return ContactCellView.Accessory(accessoryView: stackView, size: stackSize) } } diff --git a/SignalUI/RecipientPickers/RecipientPickerDelegate.swift b/SignalUI/RecipientPickers/RecipientPickerDelegate.swift index a4c11e2d88..912f86f463 100644 --- a/SignalUI/RecipientPickers/RecipientPickerDelegate.swift +++ b/SignalUI/RecipientPickers/RecipientPickerDelegate.swift @@ -25,9 +25,9 @@ public protocol RecipientPickerDelegate: RecipientContextMenuHelperDelegate { func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, - accessoryViewForRecipient recipient: PickedRecipient, + contactCellAccessoryForRecipient recipient: PickedRecipient, transaction: DBReadTransaction, - ) -> ContactCellAccessoryView? + ) -> ContactCellView.Accessory? func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, @@ -58,9 +58,9 @@ public extension RecipientPickerDelegate { func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, - accessoryViewForRecipient recipient: PickedRecipient, + contactCellAccessoryForRecipient recipient: PickedRecipient, transaction: DBReadTransaction, - ) -> ContactCellAccessoryView? { nil } + ) -> ContactCellView.Accessory? { nil } func recipientPicker( _ recipientPickerViewController: RecipientPickerViewController, diff --git a/SignalUI/RecipientPickers/RecipientPickerViewController.swift b/SignalUI/RecipientPickers/RecipientPickerViewController.swift index 3fd00b3e71..ec05df034f 100644 --- a/SignalUI/RecipientPickers/RecipientPickerViewController.swift +++ b/SignalUI/RecipientPickers/RecipientPickerViewController.swift @@ -1021,11 +1021,11 @@ extension RecipientPickerViewController { private func addressCell(for address: SignalServiceAddress, recipient: PickedRecipient, tableView: UITableView) -> UITableViewCell? { guard let cell = tableView.dequeueReusableCell(ContactTableViewCell.self) else { return nil } SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .noteToSelf) + var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .noteToSelf) if let delegate { cell.selectionStyle = delegate.recipientPicker(self, selectionStyleForRecipient: recipient, transaction: transaction) - if let accessoryView = delegate.recipientPicker(self, accessoryViewForRecipient: recipient, transaction: transaction) { - configuration.accessoryView = accessoryView + if let accessory = delegate.recipientPicker(self, contactCellAccessoryForRecipient: recipient, transaction: transaction) { + configuration.accessory = accessory } else { let accessoryMessage = delegate.recipientPicker(self, accessoryMessageForRecipient: recipient, transaction: transaction) configuration.accessoryMessage = accessoryMessage @@ -1051,7 +1051,7 @@ extension RecipientPickerViewController { SSKEnvironment.shared.databaseStorageRef.read { tx in cell.selectionStyle = delegate.recipientPicker(self, selectionStyleForRecipient: recipient, transaction: tx) cell.accessoryMessage = delegate.recipientPicker(self, accessoryMessageForRecipient: recipient, transaction: tx) - cell.customAccessoryView = delegate.recipientPicker(self, accessoryViewForRecipient: recipient, transaction: tx)?.accessoryView + cell.customAccessoryView = delegate.recipientPicker(self, contactCellAccessoryForRecipient: recipient, transaction: tx)?.accessoryView } } diff --git a/SignalUI/SafetyNumbers/SafetyNumberConfirmationSheet.swift b/SignalUI/SafetyNumbers/SafetyNumberConfirmationSheet.swift index d4bc287820..213cd4d6ea 100644 --- a/SignalUI/SafetyNumbers/SafetyNumberConfirmationSheet.swift +++ b/SignalUI/SafetyNumbers/SafetyNumberConfirmationSheet.swift @@ -682,7 +682,7 @@ private class SafetyNumberCell: ContactTableViewCell { } SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: item.address, localUserDisplayMode: .asUser) + var configuration = ContactCellView.Configuration(address: item.address, localUserDisplayMode: .asUser) configuration.allowUserInteraction = true configuration.forceDarkAppearance = traitCollection.userInterfaceStyle == .dark @@ -690,7 +690,7 @@ private class SafetyNumberCell: ContactTableViewCell { let buttonSize = button.intrinsicContentSize button.removeFromSuperview() let buttonWrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(button) - configuration.accessoryView = ContactCellAccessoryView( + configuration.accessory = ContactCellView.Accessory( accessoryView: buttonWrapper, size: buttonSize, ) @@ -720,7 +720,7 @@ private class SafetyNumberCell: ContactTableViewCell { } } - override func configure(configuration: ContactCellConfiguration, transaction: DBReadTransaction) { + override func configure(configuration: ContactCellView.Configuration, transaction: DBReadTransaction) { super.configure(configuration: configuration, transaction: transaction) backgroundColor = nil } diff --git a/SignalUI/Stories/NewPrivateStoryConfirmViewController.swift b/SignalUI/Stories/NewPrivateStoryConfirmViewController.swift index 01d2f060ec..31c6ad8ddd 100644 --- a/SignalUI/Stories/NewPrivateStoryConfirmViewController.swift +++ b/SignalUI/Stories/NewPrivateStoryConfirmViewController.swift @@ -142,7 +142,7 @@ public class NewPrivateStoryConfirmViewController: OWSTableViewController2 { cell.selectionStyle = .none SSKEnvironment.shared.databaseStorageRef.read { transaction in - let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser) + let configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser) cell.configure(configuration: configuration, transaction: transaction) } return cell