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

public import SignalServiceKit

public protocol ContactShareViewControllerDelegate: AnyObject {

    func contactShareViewController(
        _ viewController: ContactShareViewController,
        didApproveContactShare contactShare: ContactShareDraft,
    )

    func contactShareViewControllerDidCancel(_ viewController: ContactShareViewController)

    func titleForContactShareViewController(_ viewController: ContactShareViewController) -> String?

    func recipientsDescriptionForContactShareViewController(_ viewController: ContactShareViewController) -> String?

    func approvalModeForContactShareViewController(_ viewController: ContactShareViewController) -> ApprovalMode
}

public class ContactShareViewController: OWSTableViewController2 {

    public weak var shareDelegate: ContactShareViewControllerDelegate?

    private var approvalMode: ApprovalMode {
        return shareDelegate?.approvalModeForContactShareViewController(self) ?? .send
    }

    // MARK: Contact data

    private var contactShareDraft: ContactShareDraft

    private lazy var avatarField: ContactShareField? = {
        guard let avatarData = contactShareDraft.avatarImageData else { return nil }
        guard let avatarImage = contactShareDraft.avatarImage else {
            owsFailDebug("could not load avatar image.")
            return nil
        }
        return ContactShareAvatarField(OWSContactAvatar(
            avatarImage: avatarImage,
            avatarData: avatarData,
            existingAttachment: contactShareDraft.existingAvatarAttachment,
        ))
    }()

    private lazy var contactShareFields: [ContactShareField] = {
        var fields = [ContactShareField]()

        fields += contactShareDraft.phoneNumbers.map { ContactSharePhoneNumber($0) }
        fields += contactShareDraft.emails.map { ContactShareEmail($0) }
        fields += contactShareDraft.addresses.map { ContactShareAddress($0) }

        return fields
    }()

    private func filteredContactShare() -> ContactShareDraft {
        let result = contactShareDraft.newContact(withName: contactShareDraft.name)

        if let avatarField, avatarField.isIncluded {
            avatarField.applyToContact(contact: result)
        }

        for field in contactShareFields {
            if field.isIncluded {
                field.applyToContact(contact: result)
            }
        }

        return result
    }

    // MARK: UIViewController

    public init(contactShareDraft: ContactShareDraft) {
        self.contactShareDraft = contactShareDraft

        super.init()
    }

    override public func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.leftBarButtonItem = .cancelButton { [weak self] in
            self?.didPressCancel()
        }
        if let title = shareDelegate?.titleForContactShareViewController(self) {
            navigationItem.title = title
        } else {
            navigationItem.title = OWSLocalizedString(
                "CONTACT_SHARE_APPROVAL_VIEW_TITLE",
                comment: "Title for the 'Approve contact share' view.",
            )
        }
        if let recipientsDescription = shareDelegate?.recipientsDescriptionForContactShareViewController(self) {
            footerView.setNamesText(recipientsDescription, animated: false)
        }

        updateContent()
        updateProceedButtonState()
    }

    override public var canBecomeFirstResponder: Bool {
        return true
    }

    override public var inputAccessoryView: UIView? {
        return footerView
    }

    // MARK: UI

    private lazy var footerView: ApprovalFooterView = {
        let footerView = ApprovalFooterView()
        footerView.delegate = self
        return footerView
    }()

    private func isAtLeastOneFieldSelected() -> Bool {
        for field in contactShareFields {
            if field.isIncluded {
                return true
            }
        }
        return false
    }

    private func updateContent() {
        var tableItems = [OWSTableItem]()

        // Name
        tableItems.append(OWSTableItem(
            customCellBlock: { [weak self] in
                guard let contactName = self?.contactShareDraft.displayName else {
                    return OWSTableItem.newCell()
                }
                return ContactShareFieldCell.contactNameCell(for: contactName)
            },
            actionBlock: { [weak self] in
                self?.openContactNameEditingView()
            },
        ))

        // Avatar
        if let avatarField {
            tableItems.append(OWSTableItem(
                customCellBlock: {
                    return ContactShareFieldCell(field: avatarField)
                },
                actionBlock: { [weak self] in
                    self?.toggleSelection(for: avatarField)
                },
            ))
        }

        // Other fields
        tableItems += contactShareFields.map { field in
            return OWSTableItem(
                customCellBlock: {
                    return ContactShareFieldCell(field: field)
                },
                actionBlock: { [weak self] in
                    self?.toggleSelection(for: field)
                },
            )
        }
        contents = OWSTableContents(sections: [OWSTableSection(items: tableItems)])
    }

    private func updateProceedButtonState() {
        footerView.proceedButton.isEnabled = isAtLeastOneFieldSelected()
    }

    private func toggleSelection(for contactShareField: ContactShareField) {
        contactShareField.isIncluded = !contactShareField.isIncluded

        guard
            let cell = tableView.visibleCells.first(where: { visibleCell in
                guard let contactFieldCell = visibleCell as? ContactShareFieldCell else { return false }
                return contactFieldCell.field === contactShareField
            }) as? ContactShareFieldCell else { return }

        cell.updateCheckmarkState()

        updateProceedButtonState()
    }

    // MARK: -

    @objc
    private func didPressSendButton() {
        AssertIsOnMainThread()

        guard isAtLeastOneFieldSelected() else { return }

        guard contactShareDraft.ows_isValid else {
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "CONTACT_SHARE_INVALID_CONTACT",
                comment: "Error indicating that an invalid contact cannot be shared.",
            ))
            return
        }

        guard let shareDelegate else {
            owsFailDebug("missing delegate.")
            return
        }

        let filteredContactShare = filteredContactShare()
        owsPrecondition(filteredContactShare.ows_isValid)
        shareDelegate.contactShareViewController(self, didApproveContactShare: filteredContactShare)
    }

    private func didPressCancel() {
        guard let shareDelegate else {
            owsFailDebug("missing delegate.")
            return
        }

        shareDelegate.contactShareViewControllerDidCancel(self)
    }

    private func openContactNameEditingView() {
        let view = EditContactShareNameViewController(contactShareDraft: contactShareDraft, delegate: self)
        navigationController?.pushViewController(view, animated: true)
    }

    private class ContactShareFieldCell: UITableViewCell {

        let field: ContactShareField

        private lazy var checkmark: UIImageView = {
            let checkmark = UIImageView(
                image: Theme.iconImage(.circle).withTintColor(.ows_gray25, renderingMode: .automatic),
                highlightedImage: Theme.iconImage(.checkCircleFill).withTintColor(Theme.accentBlueColor, renderingMode: .automatic),
            )
            checkmark.autoSetDimensions(to: .square(24))
            return checkmark
        }()

        init(field: ContactShareField) {
            self.field = field

            let fieldContentView: UIView? = {
                switch field {
                case let avatarField as ContactShareAvatarField:
                    return ContactFieldViewHelper.contactFieldView(forAvatarImage: avatarField.value.avatarImage)

                case let phoneNumberField as ContactSharePhoneNumber:
                    return ContactFieldViewHelper.contactFieldView(forPhoneNumber: phoneNumberField.value)

                case let emailField as ContactShareEmail:
                    return ContactFieldViewHelper.contactFieldView(forEmail: emailField.value)

                case let addressField as ContactShareAddress:
                    return ContactFieldViewHelper.contactFieldView(forAddress: addressField.value)

                default:
                    owsFailDebug("Invalid field")
                    return nil
                }
            }()

            super.init(style: .default, reuseIdentifier: nil)

            selectionStyle = .none

            let stackView = UIStackView(arrangedSubviews: [checkmark])
            if let fieldContentView {
                stackView.addArrangedSubview(fieldContentView)
            }
            stackView.axis = .horizontal
            stackView.spacing = 12
            stackView.alignment = .center
            contentView.addSubview(stackView)
            stackView.autoPinHeightToSuperview(withMargin: 10)
            stackView.autoPinWidthToSuperviewMargins()

            updateCheckmarkState()
        }

        class func contactNameCell(for contactName: String) -> UITableViewCell {
            let checkmark = UIImageView(image: Theme.iconImage(.checkCircleFill).withTintColor(.ows_gray25, renderingMode: .automatic))
            let nameField = ContactFieldViewHelper.contactFieldView(forContactName: contactName)

            let stackView = UIStackView(arrangedSubviews: [checkmark, nameField])
            stackView.axis = .horizontal
            stackView.spacing = 12
            stackView.alignment = .center

            let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
            cell.accessoryType = .disclosureIndicator
            cell.contentView.addSubview(stackView)
            stackView.autoPinHeightToSuperview(withMargin: 14)
            stackView.autoPinWidthToSuperviewMargins()

            return cell
        }

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

        func updateCheckmarkState() {
            checkmark.isHighlighted = field.isIncluded
        }
    }
}

extension ContactShareViewController: EditContactShareNameViewControllerDelegate {

    public func editContactShareNameView(
        _ editContactShareNameView: EditContactShareNameViewController,
        didFinishWith contactName: OWSContactName,
    ) {
        contactShareDraft.name = contactName
        tableView.reloadData()
    }
}

// MARK: -

extension ContactShareViewController: ApprovalFooterDelegate {

    public func approvalFooterDelegateDidRequestProceed(_ approvalFooterView: ApprovalFooterView) {
        didPressSendButton()
    }

    public func approvalMode(_ approvalFooterView: ApprovalFooterView) -> ApprovalMode {
        return approvalMode
    }

    public func approvalFooterDidBeginEditingText() {}
}