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)
 | |
|     }
 | |
| 
 | |
|     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 = UIColor.white
 | |
| 
 | |
|         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 = UIColor.black
 | |
|         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: -
 | |
| 
 | |
|     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)
 | |
|     }
 | |
| 
 | |
|     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()
 | |
|     }
 | |
| }
 |