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.
		
		
		
		
		
			
		
			
				
	
	
		
			464 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			464 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import UIKit
 | |
| import SessionUtilitiesKit
 | |
| 
 | |
| // FIXME: Refactor as part of the Groups Rebuild
 | |
| public class ConfirmationModal: Modal {
 | |
|     private static let closeSize: CGFloat = 24
 | |
|     
 | |
|     private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil
 | |
|     private var internalOnCancel: ((ConfirmationModal) -> ())? = nil
 | |
|     private var internalOnBodyTap: (() -> ())? = nil
 | |
|     
 | |
|     // MARK: - Components
 | |
|     
 | |
|     private lazy var titleLabel: UILabel = {
 | |
|         let result: UILabel = UILabel()
 | |
|         result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
 | |
|         result.themeTextColor = .alert_text
 | |
|         result.textAlignment = .center
 | |
|         result.lineBreakMode = .byWordWrapping
 | |
|         result.numberOfLines = 0
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var explanationLabel: UILabel = {
 | |
|         let result: UILabel = UILabel()
 | |
|         result.font = .systemFont(ofSize: Values.smallFontSize)
 | |
|         result.themeTextColor = .alert_text
 | |
|         result.textAlignment = .center
 | |
|         result.lineBreakMode = .byWordWrapping
 | |
|         result.numberOfLines = 0
 | |
|         result.isHidden = true
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var imageViewContainer: UIView = {
 | |
|         let result: UIView = UIView()
 | |
|         result.isHidden = true
 | |
|         
 | |
|         let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer(
 | |
|             target: self,
 | |
|             action: #selector(imageViewTapped)
 | |
|         )
 | |
|         result.addGestureRecognizer(gestureRecogniser)
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var profileView: ProfilePictureView = ProfilePictureView(size: .hero)
 | |
|     
 | |
|     private lazy var confirmButton: UIButton = {
 | |
|         let result: UIButton = Modal.createButton(
 | |
|             title: "",
 | |
|             titleColor: .danger
 | |
|         )
 | |
|         result.addTarget(self, action: #selector(confirmationPressed), for: .touchUpInside)
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var buttonStackView: UIStackView = {
 | |
|         let result = UIStackView(arrangedSubviews: [ confirmButton, cancelButton ])
 | |
|         result.axis = .horizontal
 | |
|         result.distribution = .fillEqually
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var contentStackView: UIStackView = {
 | |
|         let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, imageViewContainer ])
 | |
|         result.axis = .vertical
 | |
|         result.spacing = Values.smallSpacing
 | |
|         result.isLayoutMarginsRelativeArrangement = true
 | |
|         result.layoutMargins = UIEdgeInsets(
 | |
|             top: Values.largeSpacing,
 | |
|             left: Values.largeSpacing,
 | |
|             bottom: Values.verySmallSpacing,
 | |
|             right: Values.largeSpacing
 | |
|         )
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var mainStackView: UIStackView = {
 | |
|         let result = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
 | |
|         result.axis = .vertical
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var closeButton: UIButton = {
 | |
|         let result: UIButton = UIButton()
 | |
|         result.setImage(
 | |
|             UIImage(named: "X")?
 | |
|                 .withRenderingMode(.alwaysTemplate),
 | |
|             for: .normal
 | |
|         )
 | |
|         result.imageView?.contentMode = .scaleAspectFit
 | |
|         result.themeTintColor = .textPrimary
 | |
|         result.contentEdgeInsets = UIEdgeInsets(
 | |
|             top: 6,
 | |
|             left: 6,
 | |
|             bottom: 6,
 | |
|             right: 6
 | |
|         )
 | |
|         result.isAccessibilityElement = true
 | |
|         result.accessibilityIdentifier = "Close button"
 | |
|         result.accessibilityLabel = "Close button"
 | |
|         result.set(.width, to: ConfirmationModal.closeSize)
 | |
|         result.set(.height, to: ConfirmationModal.closeSize)
 | |
|         result.addTarget(self, action: #selector(close), for: .touchUpInside)
 | |
|         result.isHidden = true
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     // MARK: - Lifecycle
 | |
|     
 | |
|     public init(targetView: UIView? = nil, info: Info) {
 | |
|         super.init(targetView: targetView, dismissType: info.dismissType, afterClosed: info.afterClosed)
 | |
|         
 | |
|         self.modalPresentationStyle = .overFullScreen
 | |
|         self.modalTransitionStyle = .crossDissolve
 | |
|         self.updateContent(with: info)
 | |
|     }
 | |
|     
 | |
|     required init?(coder: NSCoder) {
 | |
|         fatalError("init(coder:) has not been implemented")
 | |
|     }
 | |
|     
 | |
|     public override func populateContentView() {
 | |
|         contentView.addSubview(mainStackView)
 | |
|         contentView.addSubview(closeButton)
 | |
|         
 | |
|         imageViewContainer.addSubview(profileView)
 | |
|         profileView.center(.horizontal, in: imageViewContainer)
 | |
|         profileView.pin(.top, to: .top, of: imageViewContainer)//, withInset: 15)
 | |
|         profileView.pin(.bottom, to: .bottom, of: imageViewContainer)//, withInset: -15)
 | |
|         
 | |
|         mainStackView.pin(to: contentView)
 | |
|         closeButton.pin(.top, to: .top, of: contentView, withInset: 8)
 | |
|         closeButton.pin(.right, to: .right, of: contentView, withInset: -8)
 | |
|     }
 | |
|     
 | |
|     // MARK: - Content
 | |
|     
 | |
|     public func updateContent(with info: Info) {
 | |
|         internalOnBodyTap = nil
 | |
|         internalOnConfirm = { modal in
 | |
|             if info.dismissOnConfirm {
 | |
|                 modal.close()
 | |
|             }
 | |
|             
 | |
|             info.onConfirm?(modal)
 | |
|         }
 | |
|         internalOnCancel = { modal in
 | |
|             guard info.onCancel != nil else { return modal.close() }
 | |
|             
 | |
|             info.onCancel?(modal)
 | |
|         }
 | |
|         
 | |
|         // Set the content based on the provided info
 | |
|         titleLabel.text = info.title
 | |
|         
 | |
|         switch info.body {
 | |
|             case .none:
 | |
|                 mainStackView.spacing = Values.smallSpacing
 | |
|                 
 | |
|             case .text(let text):
 | |
|                 mainStackView.spacing = Values.smallSpacing
 | |
|                 explanationLabel.text = text
 | |
|                 explanationLabel.isHidden = false
 | |
|                 
 | |
|             case .attributedText(let attributedText):
 | |
|                 mainStackView.spacing = Values.smallSpacing
 | |
|                 explanationLabel.attributedText = attributedText
 | |
|                 explanationLabel.isHidden = false
 | |
|                 
 | |
|             case .image(let placeholder, let value, let icon, let style, let accessibility, let onClick):
 | |
|                 imageViewContainer.isAccessibilityElement = (accessibility != nil)
 | |
|                 imageViewContainer.accessibilityIdentifier = accessibility?.identifier
 | |
|                 imageViewContainer.accessibilityLabel = accessibility?.label
 | |
|                 mainStackView.spacing = 0
 | |
|                 imageViewContainer.isHidden = false
 | |
|                 profileView.clipsToBounds = (style == .circular)
 | |
|                 profileView.update(
 | |
|                     ProfilePictureView.Info(
 | |
|                         imageData: (value ?? placeholder),
 | |
|                         icon: icon
 | |
|                     )
 | |
|                 )
 | |
|                 internalOnBodyTap = onClick
 | |
|         }
 | |
|         
 | |
|         confirmButton.accessibilityLabel = info.confirmAccessibility?.label
 | |
|         confirmButton.accessibilityIdentifier = info.confirmAccessibility?.identifier
 | |
|         confirmButton.isAccessibilityElement = true
 | |
|         confirmButton.setTitle(info.confirmTitle, for: .normal)
 | |
|         confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal)
 | |
|         confirmButton.setThemeTitleColor(.disabled, for: .disabled)
 | |
|         confirmButton.isHidden = (info.confirmTitle == nil)
 | |
|         confirmButton.isEnabled = info.confirmEnabled
 | |
|         cancelButton.accessibilityLabel = info.cancelAccessibility?.label
 | |
|         cancelButton.accessibilityIdentifier = info.cancelAccessibility?.identifier
 | |
|         cancelButton.isAccessibilityElement = true
 | |
|         cancelButton.setTitle(info.cancelTitle, for: .normal)
 | |
|         cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal)
 | |
|         cancelButton.setThemeTitleColor(.disabled, for: .disabled)
 | |
|         cancelButton.isEnabled = info.cancelEnabled
 | |
|         closeButton.isHidden = !info.hasCloseButton
 | |
|         
 | |
|         contentView.accessibilityLabel = info.accessibility?.label
 | |
|         contentView.accessibilityIdentifier = info.accessibility?.identifier
 | |
|     }
 | |
|     
 | |
|     // MARK: - Interaction
 | |
|     
 | |
|     @objc private func imageViewTapped() {
 | |
|         internalOnBodyTap?()
 | |
|     }
 | |
|     
 | |
|     @objc private func confirmationPressed() {
 | |
|         internalOnConfirm?(self)
 | |
|     }
 | |
|     
 | |
|     override public func cancel() {
 | |
|         internalOnCancel?(self)
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: - Types
 | |
| 
 | |
| public extension ConfirmationModal {
 | |
|     struct Info: Equatable, Hashable {
 | |
|         let title: String
 | |
|         let body: Body
 | |
|         let accessibility: Accessibility?
 | |
|         public let showCondition: ShowCondition
 | |
|         let confirmTitle: String?
 | |
|         let confirmAccessibility: Accessibility?
 | |
|         let confirmStyle: ThemeValue
 | |
|         let confirmEnabled: Bool
 | |
|         let cancelTitle: String
 | |
|         let cancelAccessibility: Accessibility?
 | |
|         let cancelStyle: ThemeValue
 | |
|         let cancelEnabled: Bool
 | |
|         let hasCloseButton: Bool
 | |
|         let dismissOnConfirm: Bool
 | |
|         let dismissType: Modal.DismissType
 | |
|         let onConfirm: ((ConfirmationModal) -> ())?
 | |
|         let onCancel: ((ConfirmationModal) -> ())?
 | |
|         let afterClosed: (() -> ())?
 | |
|         
 | |
|         // MARK: - Initialization
 | |
|         
 | |
|         public init(
 | |
|             title: String,
 | |
|             body: Body = .none,
 | |
|             accessibility: Accessibility? = nil,
 | |
|             showCondition: ShowCondition = .none,
 | |
|             confirmTitle: String? = nil,
 | |
|             confirmAccessibility: Accessibility? = nil,
 | |
|             confirmStyle: ThemeValue = .alert_text,
 | |
|             confirmEnabled: Bool = true,
 | |
|             cancelTitle: String = "TXT_CANCEL_TITLE".localized(),
 | |
|             cancelAccessibility: Accessibility? = Accessibility(
 | |
|                 identifier: "Cancel"
 | |
|             ),
 | |
|             cancelStyle: ThemeValue = .danger,
 | |
|             cancelEnabled: Bool = true,
 | |
|             hasCloseButton: Bool = false,
 | |
|             dismissOnConfirm: Bool = true,
 | |
|             dismissType: Modal.DismissType = .recursive,
 | |
|             onConfirm: ((ConfirmationModal) -> ())? = nil,
 | |
|             onCancel: ((ConfirmationModal) -> ())? = nil,
 | |
|             afterClosed: (() -> ())? = nil
 | |
|         ) {
 | |
|             self.title = title
 | |
|             self.body = body
 | |
|             self.accessibility = accessibility
 | |
|             self.showCondition = showCondition
 | |
|             self.confirmTitle = confirmTitle
 | |
|             self.confirmAccessibility = confirmAccessibility
 | |
|             self.confirmStyle = confirmStyle
 | |
|             self.confirmEnabled = confirmEnabled
 | |
|             self.cancelTitle = cancelTitle
 | |
|             self.cancelAccessibility = cancelAccessibility
 | |
|             self.cancelStyle = cancelStyle
 | |
|             self.cancelEnabled = cancelEnabled
 | |
|             self.hasCloseButton = hasCloseButton
 | |
|             self.dismissOnConfirm = dismissOnConfirm
 | |
|             self.dismissType = dismissType
 | |
|             self.onConfirm = onConfirm
 | |
|             self.onCancel = onCancel
 | |
|             self.afterClosed = afterClosed
 | |
|         }
 | |
|         
 | |
|         // MARK: - Mutation
 | |
|         
 | |
|         public func with(
 | |
|             body: Body? = nil,
 | |
|             confirmEnabled: Bool? = nil,
 | |
|             cancelEnabled: Bool? = nil,
 | |
|             onConfirm: ((ConfirmationModal) -> ())? = nil,
 | |
|             onCancel: ((ConfirmationModal) -> ())? = nil,
 | |
|             afterClosed: (() -> ())? = nil
 | |
|         ) -> Info {
 | |
|             return Info(
 | |
|                 title: self.title,
 | |
|                 body: (body ?? self.body),
 | |
|                 accessibility: self.accessibility,
 | |
|                 showCondition: self.showCondition,
 | |
|                 confirmTitle: self.confirmTitle,
 | |
|                 confirmAccessibility: self.confirmAccessibility,
 | |
|                 confirmStyle: self.confirmStyle,
 | |
|                 confirmEnabled: (confirmEnabled ?? self.confirmEnabled),
 | |
|                 cancelTitle: self.cancelTitle,
 | |
|                 cancelAccessibility: self.cancelAccessibility,
 | |
|                 cancelStyle: self.cancelStyle,
 | |
|                 cancelEnabled: (cancelEnabled ?? self.cancelEnabled),
 | |
|                 hasCloseButton: self.hasCloseButton,
 | |
|                 dismissOnConfirm: self.dismissOnConfirm,
 | |
|                 dismissType: self.dismissType,
 | |
|                 onConfirm: (onConfirm ?? self.onConfirm),
 | |
|                 onCancel: (onCancel ?? self.onCancel),
 | |
|                 afterClosed: (afterClosed ?? self.afterClosed)
 | |
|             )
 | |
|         }
 | |
|         
 | |
|         // MARK: - Confirmance
 | |
|         
 | |
|         public static func == (lhs: ConfirmationModal.Info, rhs: ConfirmationModal.Info) -> Bool {
 | |
|             return (
 | |
|                 lhs.title == rhs.title &&
 | |
|                 lhs.body == rhs.body &&
 | |
|                 lhs.accessibility == rhs.accessibility &&
 | |
|                 lhs.showCondition == rhs.showCondition &&
 | |
|                 lhs.confirmTitle == rhs.confirmTitle &&
 | |
|                 lhs.confirmAccessibility == rhs.confirmAccessibility &&
 | |
|                 lhs.confirmStyle == rhs.confirmStyle &&
 | |
|                 lhs.confirmEnabled == rhs.confirmEnabled &&
 | |
|                 lhs.cancelTitle == rhs.cancelTitle &&
 | |
|                 lhs.cancelAccessibility == rhs.cancelAccessibility &&
 | |
|                 lhs.cancelStyle == rhs.cancelStyle &&
 | |
|                 lhs.cancelEnabled == rhs.cancelEnabled &&
 | |
|                 lhs.hasCloseButton == rhs.hasCloseButton &&
 | |
|                 lhs.dismissOnConfirm == rhs.dismissOnConfirm &&
 | |
|                 lhs.dismissType == rhs.dismissType
 | |
|             )
 | |
|         }
 | |
|         
 | |
|         public func hash(into hasher: inout Hasher) {
 | |
|             title.hash(into: &hasher)
 | |
|             body.hash(into: &hasher)
 | |
|             accessibility.hash(into: &hasher)
 | |
|             showCondition.hash(into: &hasher)
 | |
|             confirmTitle.hash(into: &hasher)
 | |
|             confirmAccessibility.hash(into: &hasher)
 | |
|             confirmStyle.hash(into: &hasher)
 | |
|             confirmEnabled.hash(into: &hasher)
 | |
|             cancelTitle.hash(into: &hasher)
 | |
|             cancelAccessibility.hash(into: &hasher)
 | |
|             cancelStyle.hash(into: &hasher)
 | |
|             cancelEnabled.hash(into: &hasher)
 | |
|             hasCloseButton.hash(into: &hasher)
 | |
|             dismissOnConfirm.hash(into: &hasher)
 | |
|             dismissType.hash(into: &hasher)
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| public extension ConfirmationModal.Info {
 | |
|     // MARK: - ShowCondition
 | |
|     
 | |
|     enum ShowCondition {
 | |
|         case none
 | |
|         case enabled
 | |
|         case disabled
 | |
|         
 | |
|         public func shouldShow(for value: Bool) -> Bool {
 | |
|             switch self {
 | |
|                 case .none: return true
 | |
|                 case .enabled: return (value == true)
 | |
|                 case .disabled: return (value == false)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Body
 | |
|     
 | |
|     enum Body: Equatable, Hashable {
 | |
|         public enum ImageStyle: Equatable, Hashable {
 | |
|             case inherit
 | |
|             case circular
 | |
|         }
 | |
|         
 | |
|         case none
 | |
|         case text(String)
 | |
|         case attributedText(NSAttributedString)
 | |
|         // FIXME: Implement these
 | |
|         // case input(placeholder: String, value: String?)
 | |
|         // case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)])
 | |
|         case image(
 | |
|             placeholderData: Data?,
 | |
|             valueData: Data?,
 | |
|             icon: ProfilePictureView.ProfileIcon = .none,
 | |
|             style: ImageStyle,
 | |
|             accessibility: Accessibility?,
 | |
|             onClick: (() -> ())
 | |
|         )
 | |
|         
 | |
|         public static func == (lhs: ConfirmationModal.Info.Body, rhs: ConfirmationModal.Info.Body) -> Bool {
 | |
|             switch (lhs, rhs) {
 | |
|                 case (.none, .none): return true
 | |
|                 case (.text(let lhsText), .text(let rhsText)): return (lhsText == rhsText)
 | |
|                 case (.attributedText(let lhsText), .attributedText(let rhsText)): return (lhsText == rhsText)
 | |
|                 
 | |
|                 // FIXME: Implement these
 | |
|                 //case (.input(let lhsPlaceholder, let lhsValue), .input(let rhsPlaceholder, let rhsValue)):
 | |
|                 //    return (
 | |
|                 //        lhsPlaceholder == rhsPlaceholder &&
 | |
|                 //        lhsValue == rhsValue &&
 | |
|                 //    )
 | |
|                 
 | |
|                 // FIXME: Implement these
 | |
|                 //case (.radio(let lhsExplanation, let lhsOptions), .radio(let rhsExplanation, let rhsOptions)):
 | |
|                 //    return (
 | |
|                 //        lhsExplanation == rhsExplanation &&
 | |
|                 //        lhsOptions.map { "\($0.0)-\($0.1)" } == rhsValue.map { "\($0.0)-\($0.1)" }
 | |
|                 //    )
 | |
|                     
 | |
|                 case (.image(let lhsPlaceholder, let lhsValue, let lhsIcon, let lhsStyle, let lhsAccessibility, _), .image(let rhsPlaceholder, let rhsValue, let rhsIcon, let rhsStyle, let rhsAccessibility, _)):
 | |
|                     return (
 | |
|                         lhsPlaceholder == rhsPlaceholder &&
 | |
|                         lhsValue == rhsValue &&
 | |
|                         lhsIcon == rhsIcon &&
 | |
|                         lhsStyle == rhsStyle &&
 | |
|                         lhsAccessibility == rhsAccessibility
 | |
|                     )
 | |
|                     
 | |
|                 default: return false
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         public func hash(into hasher: inout Hasher) {
 | |
|             switch self {
 | |
|                 case .none: break
 | |
|                 case .text(let text): text.hash(into: &hasher)
 | |
|                 case .attributedText(let text): text.hash(into: &hasher)
 | |
|                 
 | |
|                 case .image(let placeholder, let value, let icon, let style, let accessibility, _):
 | |
|                     placeholder.hash(into: &hasher)
 | |
|                     value.hash(into: &hasher)
 | |
|                     icon.hash(into: &hasher)
 | |
|                     style.hash(into: &hasher)
 | |
|                     accessibility.hash(into: &hasher)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 |