// // Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation import SignalMessaging @objc public class ContactCellAccessoryView: NSObject { let accessoryView: UIView let size: CGSize public init(accessoryView: UIView, size: CGSize) { self.accessoryView = accessoryView self.size = size } } // MARK: - @objc public class ContactCellConfiguration: NSObject { fileprivate enum CellDataSource { case address(SignalServiceAddress) case groupThread(TSGroupThread) } fileprivate let dataSource: CellDataSource @objc public let localUserDisplayMode: LocalUserDisplayMode @objc public var forceDarkAppearance = false @objc public var accessoryMessage: String? @objc public var customName: String? @objc public var accessoryView: ContactCellAccessoryView? @objc public var attributedSubtitle: NSAttributedString? @objc public var allowUserInteraction = false @objc public var badged = true // TODO: Badges — Default false? Configure each use-case? @objc public var hasAccessoryText: Bool { accessoryMessage?.nilIfEmpty != nil } @objc 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 func useVerifiedSubtitle() { let text = NSMutableAttributedString() text.appendTemplatedImage(named: "check-12", font: ContactCellView.subtitleFont) text.append(" ", attributes: [:]) text.append(NSLocalizedString("PRIVACY_IDENTITY_IS_VERIFIED_BADGE", comment: "Badge indicating that the user is verified."), attributes: [:]) self.attributedSubtitle = text } } // MARK: - public class ContactCellView: ManualStackView { private var configuration: ContactCellConfiguration? { didSet { ensureObservers() } } public static var avatarSizeClass: ConversationAvatarView.Configuration.SizeClass { .thirtySix } private var avatarDataSource: ConversationAvatarDataSource? { switch configuration?.dataSource { case let .groupThread(thread): return .thread(thread) case let .address(address): return .address(address) case nil: return nil } } // TODO: Update localUserDisplayMode. private let avatarView = ConversationAvatarView( sizeClass: avatarSizeClass, localUserDisplayMode: .asUser, useAutolayout: false) @objc public static let avatarTextHSpacing: CGFloat = 12 private let nameLabel = CVLabel() private let subtitleLabel = CVLabel() private let accessoryLabel = CVLabel() private let textStack = ManualStackView(name: "textStack") public required init() { super.init(name: "ContactCellView") nameLabel.lineBreakMode = .byTruncatingTail accessoryLabel.textAlignment = .right self.shouldDeactivateConstraints = false } @available(swift, obsoleted: 1.0) required init(name: String, arrangedSubviews: [UIView] = []) { owsFail("Do not use this initializer.") } private var nameLabelFont: UIFont { OWSTableItem.primaryLabelFont } fileprivate static var subtitleFont: UIFont { .ows_dynamicTypeCaption1Clamped } private func configureFontsAndColors(forceDarkAppearance: Bool) { nameLabel.font = nameLabelFont subtitleLabel.font = Self.subtitleFont accessoryLabel.font = .ows_dynamicTypeSubheadlineClamped nameLabel.textColor = forceDarkAppearance ? Theme.darkThemePrimaryColor : Theme.primaryTextColor subtitleLabel.textColor = (forceDarkAppearance ? Theme.darkThemeSecondaryTextAndIconColor : Theme.secondaryTextAndIconColor) accessoryLabel.textColor = Theme.isDarkThemeEnabled ? .ows_gray25 : .ows_gray45 if let nameLabelText = nameLabel.attributedText?.string.nilIfEmpty, let nameLabelColor = nameLabel.textColor { nameLabel.attributedText = nameLabelText.asAttributedString(attributes: [ .foregroundColor: nameLabelColor ]) } } public func configure(configuration: ContactCellConfiguration, transaction: SDSAnyReadTransaction) { AssertIsOnMainThread() owsAssertDebug(!shouldDeactivateConstraints) self.configuration = configuration self.isUserInteractionEnabled = configuration.allowUserInteraction avatarView.update(transaction) { config in config.dataSource = avatarDataSource config.addBadgeIfApplicable = configuration.badged config.localUserDisplayMode = configuration.localUserDisplayMode } // Update fonts to reflect changes to dynamic type. configureFontsAndColors(forceDarkAppearance: configuration.forceDarkAppearance) updateNameLabels(configuration: configuration, transaction: transaction) // Configure self. do { var rootStackSubviews: [UIView] = [ avatarView ] let avatarSize = Self.avatarSizeClass.avatarSize var rootStackSubviewInfos = [ avatarSize.asManualSubviewInfo(hasFixedSize: true) ] // Configure textStack. do { var textStackSubviews = [ nameLabel ] let nameSize = nameLabel.sizeThatFits(.square(.greatestFiniteMagnitude)) var textStackSubviewInfos = [ nameSize.asManualSubviewInfo ] if let attributedSubtitle = configuration.attributedSubtitle?.nilIfEmpty { subtitleLabel.attributedText = attributedSubtitle textStackSubviews.append(subtitleLabel) let subtitleSize = subtitleLabel.sizeThatFits(.square(.greatestFiniteMagnitude)) textStackSubviewInfos.append(subtitleSize.asManualSubviewInfo) } let textStackConfig = ManualStackView.Config(axis: .vertical, alignment: .leading, spacing: 0, layoutMargins: .zero) let textStackMeasurement = textStack.configure(config: textStackConfig, subviews: textStackSubviews, subviewInfos: textStackSubviewInfos) rootStackSubviews.append(textStack) rootStackSubviewInfos.append(textStackMeasurement.measuredSize.asManualSubviewInfo) } if let accessoryMessage = configuration.accessoryMessage { accessoryLabel.text = accessoryMessage let labelSize = accessoryLabel.sizeThatFits(.square(.greatestFiniteMagnitude)) configuration.accessoryView = ContactCellAccessoryView(accessoryView: accessoryLabel, size: labelSize) } if let accessoryView = configuration.accessoryView { rootStackSubviews.append(accessoryView.accessoryView) rootStackSubviewInfos.append(accessoryView.size.asManualSubviewInfo(hasFixedSize: true)) } let rootStackConfig = ManualStackView.Config(axis: .horizontal, alignment: .center, spacing: Self.avatarTextHSpacing, layoutMargins: .zero) let rootStackMeasurement = ManualStackView.measure(config: rootStackConfig, subviewInfos: rootStackSubviewInfos) self.configure(config: rootStackConfig, measurement: rootStackMeasurement, subviews: rootStackSubviews) } } // MARK: - Notifications private func ensureObservers() { NotificationCenter.default.removeObserver(self) if case .address = configuration?.dataSource { NotificationCenter.default.addObserver(self, selector: #selector(otherUsersProfileChanged(notification:)), name: .otherUsersProfileDidChange, object: nil) } } // MARK: - private func updateNameLabelsWithSneakyTransaction(configuration: ContactCellConfiguration) { databaseStorage.read { transaction in updateNameLabels(configuration: configuration, transaction: transaction) } } private func updateNameLabels(configuration: ContactCellConfiguration, transaction: SDSAnyReadTransaction) { AssertIsOnMainThread() nameLabel.attributedText = { () -> NSAttributedString in if let customName = configuration.customName?.nilIfEmpty { return customName.asAttributedString } func nameForAddress(_ address: SignalServiceAddress) -> NSAttributedString { let name: String if address.isLocalAddress { switch configuration.localUserDisplayMode { case .noteToSelf: name = MessageStrings.noteToSelf case .asLocalUser: name = NSLocalizedString("GROUP_MEMBER_LOCAL_USER", comment: "Label indicating the local user.") case .asUser: name = contactsManager.displayName(for: address, transaction: transaction) } } else { name = contactsManager.displayName(for: address, transaction: transaction) } return name.asAttributedString } switch configuration.dataSource { case .address(let address): return nameForAddress(address) case .groupThread(let thread): // TODO: Ensure nameLabel.textColor. let threadName = contactsManager.displayName(for: thread, transaction: transaction) if let nameLabelColor = nameLabel.textColor { return threadName.asAttributedString(attributes: [ .foregroundColor: nameLabelColor ]) } else { owsFailDebug("Missing nameLabelColor.") return TSGroupThread.defaultGroupName.asAttributedString } } }() } public override func reset() { super.reset() NotificationCenter.default.removeObserver(self) configuration = nil avatarView.reset() textStack.reset() nameLabel.text = nil 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?[kNSNotificationKey_ProfileAddress] as? SignalServiceAddress, changedAddress.isValid else { owsFailDebug("changedAddress was unexpectedly nil") return } if case .address(changedAddress) = configuration.dataSource { updateNameLabelsWithSneakyTransaction(configuration: configuration) } } }