TM-SGNL-iOS/SignalUI/ContactSharing/ContactShareViewController.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

326 lines
11 KiB
Swift

//
// 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()
}
public override var canBecomeFirstResponder: Bool {
return true
}
public override 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() {}
}