Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/views/NameCollisionReviewCell.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

struct NameCollisionCellModel {
    let address: SignalServiceAddress
    let name: String
    let shortName: String

    let profileNameChange: (oldestProfileName: String, newestProfileName: String)?
    let updateTimestamp: UInt64?

    /// The thread the collision appears in.
    /// Not necessarily the contacts thread for the address.
    let thread: TSThread
    let mutualGroups: [TSGroupThread]
    let isVerified: Bool
    let isConnection: Bool
    let isBlocked: Bool
    let hasPendingRequest: Bool
    let isSystemContact: Bool

    let viewControllerForPresentation: UIViewController
}

extension NameCollision {
    func collisionCellModels(
        thread: TSThread,
        identityManager: any OWSIdentityManager,
        profileManager: any ProfileManager,
        blockingManager: BlockingManager,
        contactsManager: any ContactManager,
        viewControllerForPresentation: UIViewController,
        tx: DBReadTransaction,
    ) -> [NameCollisionCellModel] {
        elements.map {
            return NameCollisionCellModel(
                address: $0.address,
                name: $0.comparableName.resolvedValue(),
                shortName: $0.comparableName.resolvedValue(useShortNameIfAvailable: true),
                profileNameChange: $0.profileNameChange,
                updateTimestamp: $0.latestUpdateTimestamp,
                thread: thread,
                mutualGroups: TSGroupThread.groupThreads(with: $0.address, transaction: tx),
                isVerified: identityManager.verificationState(for: $0.address, tx: tx) == .verified,
                isConnection: profileManager.isUser(inProfileWhitelist: $0.address, transaction: tx),
                isBlocked: blockingManager.isAddressBlocked($0.address, transaction: tx),
                hasPendingRequest: ContactThreadFinder().contactThread(for: $0.address, tx: tx)?.hasPendingMessageRequest(transaction: tx) ?? false,
                isSystemContact: contactsManager.fetchSignalAccount(for: $0.address, transaction: tx) != nil,
                viewControllerForPresentation: viewControllerForPresentation,
            )
        }
    }
}

final class NameCollisionCell: UITableViewCell {
    let avatarView = ConversationAvatarView(sizeClass: .fiftySix, localUserDisplayMode: .asUser)
    let nameLabel: UILabel = {
        let label = UILabel()
        label.textColor = .Signal.label
        label.font = UIFont.dynamicTypeHeadline
        label.adjustsFontForContentSizeCategory = true
        label.numberOfLines = 0

        return label
    }()

    let separatorView: UIView = {
        let hairline = UIView()
        hairline.backgroundColor = .Signal.opaqueSeparator
        hairline.autoSetDimension(.height, toSize: .hairlineWidth)
        let separator = UIView()
        separator.addSubview(hairline)
        hairline.autoPinEdgesToSuperviewEdges(with: .init(top: 8, leading: 0, bottom: 0, trailing: 0))
        return separator
    }()

    private let verticalStack: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 12
        return stackView
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        let horizontalStack = UIStackView(arrangedSubviews: [
            avatarView,
            verticalStack,
        ])

        horizontalStack.axis = .horizontal
        horizontalStack.spacing = 16
        horizontalStack.alignment = .top

        contentView.addSubview(horizontalStack)
        horizontalStack.autoPinEdgesToSuperviewMargins()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    static func createWithModel(
        _ model: NameCollisionCellModel,
        action: NameCollisionCell.Action?,
    ) -> Self {
        let cell = self.init(style: .default, reuseIdentifier: nil)
        cell.configure(model: model, action: action)
        return cell
    }

    override func prepareForReuse() {
        verticalStack.removeAllSubviews()
        avatarView.reset()
        nameLabel.text = ""
    }

    func configure(model: NameCollisionCellModel, action: NameCollisionCell.Action?) {
        verticalStack.removeAllSubviews()

        // Avatar
        avatarView.updateWithSneakyTransactionIfNecessary { config in
            config.dataSource = .address(model.address)
        }

        // Name
        if model.address.isLocalAddress {
            nameLabel.text = CommonStrings.you
        } else {
            nameLabel.text = model.name
        }
        verticalStack.addArrangedSubview(nameLabel)

        let detailFont = UIFont.dynamicTypeSubheadline

        // Verified
        if model.isVerified {
            verticalStack.addArrangedSubview(ProfileDetailLabel.verified(font: detailFont))
        }

        // Name change
        if let profileNameChange = model.profileNameChange {
            let formatString = OWSLocalizedString(
                "NAME_COLLISION_RECENT_CHANGE_FORMAT_STRING",
                comment: "Format string describing a recent profile name change that led to a name collision. Embeds {{ %1$@ current name, which may be a profile name or an address book name }}, {{ %2$@ old profile name }}, and {{ %3$@ current profile name }}",
            )
            let string = String.nonPluralLocalizedStringWithFormat(
                formatString,
                model.shortName,
                profileNameChange.oldestProfileName,
                profileNameChange.newestProfileName,
            )
            verticalStack.addArrangedSubview(ProfileDetailLabel.profile(
                displayName: string,
                font: detailFont,
            ))
        }

        // Connection
        if model.isConnection {
            verticalStack.addArrangedSubview(ProfileDetailLabel.signalConnectionLink(
                font: detailFont,
                shouldDismissOnNavigation: false,
                presentEducationFrom: model.viewControllerForPresentation,
            ))
        } else if model.isBlocked {
            verticalStack.addArrangedSubview(ProfileDetailLabel.blocked(
                name: model.shortName,
                font: detailFont,
            ))
        } else if model.hasPendingRequest {
            verticalStack.addArrangedSubview(ProfileDetailLabel.pendingRequest(
                name: model.shortName,
                font: detailFont,
            ))
        } else {
            verticalStack.addArrangedSubview(ProfileDetailLabel.noDirectChat(
                name: model.shortName,
                font: detailFont,
            ))
        }

        // System contacts
        if model.isSystemContact {
            verticalStack.addArrangedSubview(ProfileDetailLabel.inSystemContacts(
                name: model.shortName,
                font: detailFont,
            ))
        }

        // Phone number
        if let phoneNumber = model.address.phoneNumber {
            verticalStack.addArrangedSubview(ProfileDetailLabel.phoneNumber(
                phoneNumber,
                font: detailFont,
                presentSuccessToastFrom: model.viewControllerForPresentation,
            ))
        }

        // Mutual groups
        verticalStack.addArrangedSubview(ProfileDetailLabel.mutualGroups(
            for: model.thread,
            mutualGroups: model.mutualGroups,
            font: detailFont,
        ))

        separatorView.isHidden = action == nil
        verticalStack.addArrangedSubview(separatorView)

        if let action {
            verticalStack.addArrangedSubview(createButton(for: action))
        }
    }

    struct Action {
        enum Role {
            case normal
            case destructive

            var color: UIColor {
                switch self {
                case .normal:
                    return .Signal.label
                case .destructive:
                    return .Signal.red
                }
            }
        }

        let title: String
        let icon: ThemeIcon
        let role: Role
        let action: () -> Void

        static func block(_ action: @escaping () -> Void) -> Action {
            Action(
                title: MessageRequestView.LocalizedStrings.block,
                icon: .chatSettingsBlock,
                role: .destructive,
                action: action,
            )
        }

        static func unblock(_ action: @escaping () -> Void) -> Action {
            Action(
                title: MessageRequestView.LocalizedStrings.unblock,
                icon: .chatSettingsBlock,
                role: .normal,
                action: action,
            )
        }

        static func removeFromGroup(_ action: @escaping () -> Void) -> Action {
            Action(
                title: OWSLocalizedString(
                    "CONVERSATION_SETTINGS_REMOVE_FROM_GROUP_BUTTON",
                    comment: "Label for 'remove from group' button in conversation settings view.",
                ),
                icon: .groupMemberRemoveFromGroup,
                role: .destructive,
                action: action,
            )
        }

        static func updateContact(_ action: @escaping () -> Void) -> Action {
            Action(
                title: OWSLocalizedString(
                    "MESSAGE_REQUEST_NAME_COLLISON_UPDATE_CONTACT_ACTION",
                    comment: "A button that updates a known contact's information to resolve a name collision",
                ),
                icon: .profileAbout,
                role: .normal,
                action: action,
            )
        }
    }

    private func createButton(for action: Action) -> UIButton {
        let button = UIButton(
            configuration: .plain(),
            primaryAction: UIAction { _ in
                action.action()
            },
        )
        button.configuration?.title = action.title
        button.configuration?.baseForegroundColor = action.role.color
        button.configuration?.image = Theme.iconImage(action.icon)
        button.configuration?.imagePadding = 12
        button.configuration?.contentInsets = .init(hMargin: 0, vMargin: 4)
        button.contentHorizontalAlignment = .leading
        return button
    }
}