mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			513 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			513 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import SignalServiceKit
 | 
						|
 | 
						|
@objc
 | 
						|
public protocol ContactShareApprovalViewControllerDelegate: class {
 | 
						|
    func approveContactShare(_ approveContactShare: ContactShareApprovalViewController,
 | 
						|
                             didApproveContactShare contactShare: ContactShareViewModel)
 | 
						|
    func approveContactShare(_ approveContactShare: ContactShareApprovalViewController,
 | 
						|
                             didCancelContactShare contactShare: ContactShareViewModel)
 | 
						|
}
 | 
						|
 | 
						|
protocol ContactShareField: class {
 | 
						|
 | 
						|
    var isAvatar: Bool { get }
 | 
						|
 | 
						|
    func localizedLabel() -> String
 | 
						|
 | 
						|
    func isIncluded() -> Bool
 | 
						|
 | 
						|
    func setIsIncluded(_ isIncluded: Bool)
 | 
						|
 | 
						|
    func applyToContact(contact: ContactShareViewModel)
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
class ContactShareFieldBase<ContactFieldType: OWSContactField>: NSObject, ContactShareField {
 | 
						|
 | 
						|
    let value: ContactFieldType
 | 
						|
 | 
						|
    private var isIncludedFlag = true
 | 
						|
 | 
						|
    var isAvatar: Bool { return false }
 | 
						|
 | 
						|
    required init(_ value: ContactFieldType) {
 | 
						|
        self.value = value
 | 
						|
 | 
						|
        super.init()
 | 
						|
    }
 | 
						|
 | 
						|
    func localizedLabel() -> String {
 | 
						|
        return value.localizedLabel()
 | 
						|
    }
 | 
						|
 | 
						|
    func isIncluded() -> Bool {
 | 
						|
        return isIncludedFlag
 | 
						|
    }
 | 
						|
 | 
						|
    func setIsIncluded(_ isIncluded: Bool) {
 | 
						|
        isIncludedFlag = isIncluded
 | 
						|
    }
 | 
						|
 | 
						|
    func applyToContact(contact: ContactShareViewModel) {
 | 
						|
        preconditionFailure("This method must be overridden")
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
class ContactSharePhoneNumber: ContactShareFieldBase<OWSContactPhoneNumber> {
 | 
						|
 | 
						|
    override func applyToContact(contact: ContactShareViewModel) {
 | 
						|
        assert(isIncluded())
 | 
						|
 | 
						|
        var values = [OWSContactPhoneNumber]()
 | 
						|
        values += contact.phoneNumbers
 | 
						|
        values.append(value)
 | 
						|
        contact.phoneNumbers = values
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
class ContactShareEmail: ContactShareFieldBase<OWSContactEmail> {
 | 
						|
 | 
						|
    override func applyToContact(contact: ContactShareViewModel) {
 | 
						|
        assert(isIncluded())
 | 
						|
 | 
						|
        var values = [OWSContactEmail]()
 | 
						|
        values += contact.emails
 | 
						|
        values.append(value)
 | 
						|
        contact.emails = values
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
class ContactShareAddress: ContactShareFieldBase<OWSContactAddress> {
 | 
						|
 | 
						|
    override func applyToContact(contact: ContactShareViewModel) {
 | 
						|
        assert(isIncluded())
 | 
						|
 | 
						|
        var values = [OWSContactAddress]()
 | 
						|
        values += contact.addresses
 | 
						|
        values.append(value)
 | 
						|
        contact.addresses = values
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Stub class so that avatars conform to OWSContactField.
 | 
						|
class OWSContactAvatar: NSObject, OWSContactField {
 | 
						|
 | 
						|
    public let avatarImage: UIImage
 | 
						|
    public let avatarData: Data
 | 
						|
 | 
						|
    required init(avatarImage: UIImage, avatarData: Data) {
 | 
						|
        self.avatarImage = avatarImage
 | 
						|
        self.avatarData = avatarData
 | 
						|
 | 
						|
        super.init()
 | 
						|
    }
 | 
						|
 | 
						|
    public func ows_isValid() -> Bool {
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    public func localizedLabel() -> String {
 | 
						|
        return ""
 | 
						|
    }
 | 
						|
 | 
						|
    override public var debugDescription: String {
 | 
						|
        return "Avatar"
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
class ContactShareAvatarField: ContactShareFieldBase<OWSContactAvatar> {
 | 
						|
    override var isAvatar: Bool { return true }
 | 
						|
 | 
						|
    override func applyToContact(contact: ContactShareViewModel) {
 | 
						|
        assert(isIncluded())
 | 
						|
 | 
						|
        contact.avatarImageData = value.avatarData
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
protocol ContactShareFieldViewDelegate: class {
 | 
						|
    func contactShareFieldViewDidChangeSelectedState()
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
class ContactShareFieldView: UIStackView {
 | 
						|
 | 
						|
    weak var delegate: ContactShareFieldViewDelegate?
 | 
						|
 | 
						|
    let field: ContactShareField
 | 
						|
 | 
						|
    let previewViewBlock : (() -> UIView)
 | 
						|
 | 
						|
    private var checkbox: UIButton!
 | 
						|
 | 
						|
    // MARK: - Initializers
 | 
						|
 | 
						|
    @available(*, unavailable, message: "use init(call:) constructor instead.")
 | 
						|
    required init(coder aDecoder: NSCoder) {
 | 
						|
        fatalError("Unimplemented")
 | 
						|
    }
 | 
						|
 | 
						|
    required init(field: ContactShareField, previewViewBlock : @escaping (() -> UIView), delegate: ContactShareFieldViewDelegate) {
 | 
						|
        self.field = field
 | 
						|
        self.previewViewBlock = previewViewBlock
 | 
						|
        self.delegate = delegate
 | 
						|
 | 
						|
        super.init(frame: CGRect.zero)
 | 
						|
 | 
						|
        self.isUserInteractionEnabled = true
 | 
						|
        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
 | 
						|
 | 
						|
        createContents()
 | 
						|
    }
 | 
						|
 | 
						|
    let hSpacing = CGFloat(10)
 | 
						|
    let hMargin = CGFloat(16)
 | 
						|
 | 
						|
    func createContents() {
 | 
						|
        self.axis = .horizontal
 | 
						|
        self.spacing = hSpacing
 | 
						|
        self.alignment = .center
 | 
						|
        self.layoutMargins = UIEdgeInsets(top: 0, left: hMargin, bottom: 0, right: hMargin)
 | 
						|
        self.isLayoutMarginsRelativeArrangement = true
 | 
						|
 | 
						|
        let checkbox = UIButton(type: .custom)
 | 
						|
        self.checkbox = checkbox
 | 
						|
 | 
						|
        let checkedIcon = #imageLiteral(resourceName: "contact_checkbox_checked")
 | 
						|
        let uncheckedIcon = #imageLiteral(resourceName: "contact_checkbox_unchecked")
 | 
						|
        checkbox.setImage(uncheckedIcon, for: .normal)
 | 
						|
        checkbox.setImage(checkedIcon, for: .selected)
 | 
						|
        checkbox.isSelected = field.isIncluded()
 | 
						|
        // Disable the checkbox; the entire row is hot.
 | 
						|
        checkbox.isUserInteractionEnabled = false
 | 
						|
        self.addArrangedSubview(checkbox)
 | 
						|
        checkbox.setCompressionResistanceHigh()
 | 
						|
        checkbox.setContentHuggingHigh()
 | 
						|
 | 
						|
        let previewView = previewViewBlock()
 | 
						|
        self.addArrangedSubview(previewView)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc func wasTapped(sender: UIGestureRecognizer) {
 | 
						|
        Logger.info("\(self.logTag) \(#function)")
 | 
						|
 | 
						|
        guard sender.state == .recognized else {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        field.setIsIncluded(!field.isIncluded())
 | 
						|
        checkbox.isSelected = field.isIncluded()
 | 
						|
 | 
						|
        delegate?.contactShareFieldViewDidChangeSelectedState()
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
// TODO: Rename to ContactShareApprovalViewController
 | 
						|
@objc
 | 
						|
public class ContactShareApprovalViewController: OWSViewController, EditContactShareNameViewControllerDelegate, ContactShareFieldViewDelegate {
 | 
						|
 | 
						|
    weak var delegate: ContactShareApprovalViewControllerDelegate?
 | 
						|
 | 
						|
    let contactsManager: OWSContactsManager
 | 
						|
 | 
						|
    var contactShare: ContactShareViewModel
 | 
						|
 | 
						|
    var fieldViews = [ContactShareFieldView]()
 | 
						|
 | 
						|
    var nameLabel: UILabel!
 | 
						|
 | 
						|
    // MARK: Initializers
 | 
						|
 | 
						|
    @available(*, unavailable, message:"use other constructor instead.")
 | 
						|
    required public init?(coder aDecoder: NSCoder) {
 | 
						|
        fatalError("unimplemented")
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    required public init(contactShare: ContactShareViewModel, contactsManager: OWSContactsManager, delegate: ContactShareApprovalViewControllerDelegate) {
 | 
						|
        self.contactsManager = contactsManager
 | 
						|
        self.contactShare = contactShare
 | 
						|
        self.delegate = delegate
 | 
						|
 | 
						|
        super.init(nibName: nil, bundle: nil)
 | 
						|
 | 
						|
        buildFields()
 | 
						|
    }
 | 
						|
 | 
						|
    func buildFields() {
 | 
						|
        var fieldViews = [ContactShareFieldView]()
 | 
						|
 | 
						|
        let previewInsets = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0)
 | 
						|
 | 
						|
        if let avatarData = contactShare.avatarImageData {
 | 
						|
            if let avatarImage = contactShare.avatarImage {
 | 
						|
                let field = ContactShareAvatarField(OWSContactAvatar(avatarImage: avatarImage, avatarData: avatarData))
 | 
						|
                let fieldView = ContactShareFieldView(field: field, previewViewBlock: {
 | 
						|
                    return ContactFieldView.contactFieldView(forAvatarImage: avatarImage, layoutMargins: previewInsets, actionBlock: nil)
 | 
						|
                },
 | 
						|
                                                      delegate: self)
 | 
						|
                fieldViews.append(fieldView)
 | 
						|
            } else {
 | 
						|
                owsFail("\(logTag) could not load avatar image.")
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        for phoneNumber in contactShare.phoneNumbers {
 | 
						|
            let field = ContactSharePhoneNumber(phoneNumber)
 | 
						|
            let fieldView = ContactShareFieldView(field: field, previewViewBlock: {
 | 
						|
                return ContactFieldView.contactFieldView(forPhoneNumber: phoneNumber, layoutMargins: previewInsets, actionBlock: nil)
 | 
						|
            },
 | 
						|
                                                  delegate: self)
 | 
						|
            fieldViews.append(fieldView)
 | 
						|
        }
 | 
						|
 | 
						|
        for email in contactShare.emails {
 | 
						|
            let field = ContactShareEmail(email)
 | 
						|
            let fieldView = ContactShareFieldView(field: field, previewViewBlock: {
 | 
						|
                return ContactFieldView.contactFieldView(forEmail: email, layoutMargins: previewInsets, actionBlock: nil)
 | 
						|
            },
 | 
						|
                                                  delegate: self)
 | 
						|
            fieldViews.append(fieldView)
 | 
						|
        }
 | 
						|
 | 
						|
        for address in contactShare.addresses {
 | 
						|
            let field = ContactShareAddress(address)
 | 
						|
            let fieldView = ContactShareFieldView(field: field, previewViewBlock: {
 | 
						|
                return ContactFieldView.contactFieldView(forAddress: address, layoutMargins: previewInsets, actionBlock: nil)
 | 
						|
            },
 | 
						|
                                                  delegate: self)
 | 
						|
            fieldViews.append(fieldView)
 | 
						|
        }
 | 
						|
 | 
						|
        self.fieldViews = fieldViews
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - View Lifecycle
 | 
						|
 | 
						|
    override public func viewWillAppear(_ animated: Bool) {
 | 
						|
        super.viewWillAppear(animated)
 | 
						|
 | 
						|
        updateNavigationBar()
 | 
						|
    }
 | 
						|
 | 
						|
    override public func viewDidAppear(_ animated: Bool) {
 | 
						|
        super.viewDidAppear(animated)
 | 
						|
    }
 | 
						|
 | 
						|
    override public func viewWillDisappear(_ animated: Bool) {
 | 
						|
        super.viewWillDisappear(animated)
 | 
						|
    }
 | 
						|
 | 
						|
    override public func viewDidDisappear(_ animated: Bool) {
 | 
						|
        super.viewDidDisappear(animated)
 | 
						|
    }
 | 
						|
 | 
						|
    override public func loadView() {
 | 
						|
        super.loadView()
 | 
						|
 | 
						|
        self.navigationItem.title = NSLocalizedString("CONTACT_SHARE_APPROVAL_VIEW_TITLE",
 | 
						|
                                                      comment: "Title for the 'Approve contact share' view.")
 | 
						|
 | 
						|
        self.view.backgroundColor = Theme.backgroundColor
 | 
						|
 | 
						|
        updateContent()
 | 
						|
 | 
						|
        updateNavigationBar()
 | 
						|
    }
 | 
						|
 | 
						|
    func isAtLeastOneFieldSelected() -> Bool {
 | 
						|
        for fieldView in fieldViews {
 | 
						|
            if fieldView.field.isIncluded(), !fieldView.field.isAvatar {
 | 
						|
                return true
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return false
 | 
						|
    }
 | 
						|
 | 
						|
    func updateNavigationBar() {
 | 
						|
        self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
 | 
						|
                                                                target: self,
 | 
						|
                                                                action: #selector(didPressCancel))
 | 
						|
 | 
						|
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON",
 | 
						|
                                                                                          comment: "Label for 'send' button in the 'attachment approval' dialog."),
 | 
						|
                                                                 style: .plain, target: self, action: #selector(didPressSendButton))
 | 
						|
    }
 | 
						|
 | 
						|
    private func updateContent() {
 | 
						|
        SwiftAssertIsOnMainThread(#function)
 | 
						|
 | 
						|
        guard let rootView = self.view else {
 | 
						|
            owsFail("\(logTag) missing root view.")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        for subview in rootView.subviews {
 | 
						|
            subview.removeFromSuperview()
 | 
						|
        }
 | 
						|
 | 
						|
        let scrollView = UIScrollView()
 | 
						|
        scrollView.preservesSuperviewLayoutMargins = false
 | 
						|
        self.view.addSubview(scrollView)
 | 
						|
        scrollView.layoutMargins = .zero
 | 
						|
        scrollView.autoPinWidthToSuperview()
 | 
						|
        scrollView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
 | 
						|
        scrollView.autoPinEdge(toSuperviewEdge: .bottom)
 | 
						|
 | 
						|
        let fieldsView = createFieldsView()
 | 
						|
 | 
						|
        scrollView.addSubview(fieldsView)
 | 
						|
        // Use layoutMarginsGuide for views inside UIScrollView
 | 
						|
        // that should have same width as scroll view.
 | 
						|
        fieldsView.autoPinLeadingToSuperviewMargin()
 | 
						|
        fieldsView.autoPinTrailingToSuperviewMargin()
 | 
						|
        fieldsView.autoPinEdge(toSuperviewEdge: .top)
 | 
						|
        fieldsView.autoPinEdge(toSuperviewEdge: .bottom)
 | 
						|
        fieldsView.setContentHuggingHorizontalLow()
 | 
						|
    }
 | 
						|
 | 
						|
    private func createFieldsView() -> UIView {
 | 
						|
        SwiftAssertIsOnMainThread(#function)
 | 
						|
 | 
						|
        var rows = [UIView]()
 | 
						|
 | 
						|
        rows.append(createNameRow())
 | 
						|
 | 
						|
        for fieldView in fieldViews {
 | 
						|
            rows.append(fieldView)
 | 
						|
        }
 | 
						|
 | 
						|
        return ContactFieldView(rows: rows, hMargin: hMargin)
 | 
						|
    }
 | 
						|
 | 
						|
    private let hMargin = CGFloat(16)
 | 
						|
 | 
						|
    func createNameRow() -> UIView {
 | 
						|
        let nameVMargin = CGFloat(16)
 | 
						|
 | 
						|
        let stackView = TappableStackView(actionBlock: { [weak self] in
 | 
						|
            guard let strongSelf = self else { return }
 | 
						|
            strongSelf.didPressEditName()
 | 
						|
        })
 | 
						|
 | 
						|
        stackView.axis = .horizontal
 | 
						|
        stackView.alignment = .center
 | 
						|
        stackView.layoutMargins = UIEdgeInsets(top: nameVMargin, left: hMargin, bottom: nameVMargin, right: hMargin)
 | 
						|
        stackView.spacing = 10
 | 
						|
        stackView.isLayoutMarginsRelativeArrangement = true
 | 
						|
 | 
						|
        let nameLabel = UILabel()
 | 
						|
        self.nameLabel = nameLabel
 | 
						|
        nameLabel.text = contactShare.name.displayName
 | 
						|
        nameLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
 | 
						|
        nameLabel.textColor = Theme.primaryColor
 | 
						|
        nameLabel.lineBreakMode = .byTruncatingTail
 | 
						|
        stackView.addArrangedSubview(nameLabel)
 | 
						|
 | 
						|
        let editNameLabel = UILabel()
 | 
						|
        editNameLabel.text = NSLocalizedString("CONTACT_EDIT_NAME_BUTTON", comment: "Label for the 'edit name' button in the contact share approval view.")
 | 
						|
        editNameLabel.font = UIFont.ows_dynamicTypeBody
 | 
						|
        editNameLabel.textColor = UIColor.ows_materialBlue
 | 
						|
        stackView.addArrangedSubview(editNameLabel)
 | 
						|
        editNameLabel.setContentHuggingHigh()
 | 
						|
        editNameLabel.setCompressionResistanceHigh()
 | 
						|
 | 
						|
        return stackView
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: -
 | 
						|
 | 
						|
    func filteredContactShare() -> ContactShareViewModel {
 | 
						|
        let result = self.contactShare.newContact(withName: self.contactShare.name)
 | 
						|
 | 
						|
        for fieldView in fieldViews {
 | 
						|
            if fieldView.field.isIncluded() {
 | 
						|
                fieldView.field.applyToContact(contact: result)
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return result
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: -
 | 
						|
 | 
						|
    @objc func didPressSendButton() {
 | 
						|
        SwiftAssertIsOnMainThread(#function)
 | 
						|
 | 
						|
        guard isAtLeastOneFieldSelected() else {
 | 
						|
            OWSAlerts.showErrorAlert(message: NSLocalizedString("CONTACT_SHARE_NO_FIELDS_SELECTED",
 | 
						|
                                                                comment: "Error indicating that at least one contact field must be selected before sharing a contact."))
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard contactShare.ows_isValid else {
 | 
						|
            OWSAlerts.showErrorAlert(message: NSLocalizedString("CONTACT_SHARE_INVALID_CONTACT",
 | 
						|
                                                                comment: "Error indicating that an invalid contact cannot be shared."))
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        Logger.info("\(logTag) \(#function)")
 | 
						|
 | 
						|
        guard let delegate = self.delegate else {
 | 
						|
            owsFail("\(logTag) missing delegate.")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        let filteredContactShare = self.filteredContactShare()
 | 
						|
 | 
						|
        assert(filteredContactShare.ows_isValid)
 | 
						|
 | 
						|
        delegate.approveContactShare(self, didApproveContactShare: filteredContactShare)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc func didPressCancel() {
 | 
						|
        Logger.info("\(logTag) \(#function)")
 | 
						|
 | 
						|
        guard let delegate = self.delegate else {
 | 
						|
            owsFail("\(logTag) missing delegate.")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        delegate.approveContactShare(self, didCancelContactShare: contactShare)
 | 
						|
    }
 | 
						|
 | 
						|
    func didPressEditName() {
 | 
						|
        Logger.info("\(logTag) \(#function)")
 | 
						|
 | 
						|
        let view = EditContactShareNameViewController(contactShare: contactShare, delegate: self)
 | 
						|
        self.navigationController?.pushViewController(view, animated: true)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - EditContactShareNameViewControllerDelegate
 | 
						|
 | 
						|
    public func editContactShareNameView(_ editContactShareNameView: EditContactShareNameViewController,
 | 
						|
                                         didEditContactShare contactShare: ContactShareViewModel) {
 | 
						|
        self.contactShare = contactShare
 | 
						|
 | 
						|
        nameLabel.text = contactShare.name.displayName
 | 
						|
 | 
						|
        self.updateNavigationBar()
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - ContactShareFieldViewDelegate
 | 
						|
 | 
						|
    public func contactShareFieldViewDidChangeSelectedState() {
 | 
						|
        self.updateNavigationBar()
 | 
						|
    }
 | 
						|
}
 |