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)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |