diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 2a04c703f..bc7f80f8e 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -485,7 +485,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat targetView: self.view, info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 1d9f66159..d5cc50a83 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -305,7 +305,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text @@ -350,7 +350,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate targetView: self?.view, info: ConfirmationModal.Info( title: "GROUP_CREATION_ERROR_TITLE".localized(), - explanation: "GROUP_CREATION_ERROR_MESSAGE".localized(), + body: .text("GROUP_CREATION_ERROR_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 2d98029d5..6f8cfb32b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -68,7 +68,7 @@ extension ConversationVC: let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_call_permission_request_title".localized(), - explanation: "modal_call_permission_request_explanation".localized(), + body: .text("modal_call_permission_request_explanation".localized()), confirmTitle: "vc_settings_title".localized(), confirmAccessibilityLabel: "Settings", cancelAccessibilityLabel: "Cancel", @@ -132,11 +132,13 @@ extension ConversationVC: format: "modal_blocked_title".localized(), self.viewModel.threadData.displayName ), - attributedExplanation: NSAttributedString(string: message) - .adding( - attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], - range: (message as NSString).range(of: self.viewModel.threadData.displayName) - ), + body: .attributedText( + NSAttributedString(string: message) + .adding( + attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], + range: (message as NSString).range(of: self.viewModel.threadData.displayName) + ) + ), confirmTitle: "modal_blocked_button_title".localized(), confirmAccessibilityLabel: "Confirm block", cancelAccessibilityLabel: "Cancel block", @@ -205,7 +207,7 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "GIPHY_PERMISSION_TITLE".localized(), - explanation: "GIPHY_PERMISSION_MESSAGE".localized(), + body: .text("GIPHY_PERMISSION_MESSAGE".localized()), confirmTitle: "continue_2".localized() ) { [weak self] _ in Storage.shared.writeAsync( @@ -295,7 +297,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: "Session", - explanation: "An error occurred.", + body: .text("An error occurred."), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -312,7 +314,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: "ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE".localized(), - explanation: "ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY".localized(), + body: .text("ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -410,7 +412,7 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_send_seed_title".localized(), - explanation: "modal_send_seed_explanation".localized(), + body: .text("modal_send_seed_explanation".localized()), confirmTitle: "modal_send_seed_send_button_title".localized(), confirmStyle: .danger, cancelStyle: .alert_text, @@ -540,7 +542,7 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_send_seed_title".localized(), - explanation: "modal_send_seed_explanation".localized(), + body: .text("modal_send_seed_explanation".localized()), confirmTitle: "modal_send_seed_send_button_title".localized(), confirmStyle: .danger, cancelStyle: .alert_text, @@ -646,7 +648,7 @@ extension ConversationVC: let linkPreviewModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_link_previews_title".localized(), - explanation: "modal_link_previews_explanation".localized(), + body: .text("modal_link_previews_explanation".localized()), confirmTitle: "modal_link_previews_button_title".localized() ) { [weak self] _ in Storage.shared.writeAsync { db in @@ -890,11 +892,13 @@ extension ConversationVC: format: "modal_download_attachment_title".localized(), cellViewModel.authorName ), - attributedExplanation: NSAttributedString(string: message) - .adding( - attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], - range: (message as NSString).range(of: cellViewModel.authorName) - ), + body: .attributedText( + NSAttributedString(string: message) + .adding( + attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], + range: (message as NSString).range(of: cellViewModel.authorName) + ) + ), confirmTitle: "modal_download_button_title".localized(), confirmAccessibilityLabel: "Download media", cancelAccessibilityLabel: "Don't download media", @@ -1541,11 +1545,13 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "Join \(finalName)?", - attributedExplanation: NSMutableAttributedString(string: message) - .adding( - attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], - range: (message as NSString).range(of: finalName) - ), + body: .attributedText( + NSMutableAttributedString(string: message) + .adding( + attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], + range: (message as NSString).range(of: finalName) + ) + ), confirmTitle: "JOIN_COMMUNITY_BUTTON_TITLE".localized(), onConfirm: { modal in guard let presentingViewController: UIViewController = modal.presentingViewController else { @@ -1582,7 +1588,7 @@ extension ConversationVC: let errorModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "COMMUNITY_ERROR_GENERIC".localized(), - explanation: error.localizedDescription, + body: .text(error.localizedDescription), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2048,7 +2054,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "Session", - explanation: "This will ban the selected user from this room. It won't ban them from other rooms.", + body: .text("This will ban the selected user from this room. It won't ban them from other rooms."), confirmTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, onConfirm: { [weak self] _ in @@ -2072,7 +2078,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "context_menu_ban_user_error_alert_message".localized(), + body: .text("context_menu_ban_user_error_alert_message".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2097,7 +2103,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "Session", - explanation: "This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.", + body: .text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there."), confirmTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, onConfirm: { [weak self] _ in @@ -2121,7 +2127,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "context_menu_ban_user_error_alert_message".localized(), + body: .text("context_menu_ban_user_error_alert_message".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2231,7 +2237,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE".localized(), - explanation: "VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized(), + body: .text("VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2302,7 +2308,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "ATTACHMENT_ERROR_ALERT_TITLE".localized(), - explanation: (attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage), + body: .text(attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, afterClosed: onDismiss diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index d5ec2f3cc..17ab87f7d 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1304,7 +1304,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl targetView: self?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(), + body: .text("INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 447732eeb..fe5722e7d 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -3,6 +3,7 @@ import Foundation import Combine import GRDB +import YYImage import DifferenceKit import SessionUIKit import SessionMessagingKit @@ -395,7 +396,7 @@ class ThreadSettingsViewModel: SessionTableViewModel ())? = nil) { - super.init(targetView: targetView, afterClosed: afterClosed) + override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) { + super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed) self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve @@ -135,7 +135,7 @@ final class NukeDataModal: Modal { let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_clear_all_data_title".localized(), - explanation: "modal_clear_all_data_explanation_2".localized(), + body: .text("modal_clear_all_data_explanation_2".localized()), confirmTitle: "modal_clear_all_data_confirm".localized(), confirmStyle: .danger, cancelStyle: .alert_text, @@ -184,7 +184,7 @@ final class NukeDataModal: Modal { targetView: self?.view, info: ConfirmationModal.Info( title: "ALERT_ERROR_TITLE".localized(), - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -199,7 +199,7 @@ final class NukeDataModal: Modal { targetView: self?.view, info: ConfirmationModal.Info( title: "ALERT_ERROR_TITLE".localized(), - explanation: error.localizedDescription, + body: .text(error.localizedDescription), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index b7a279b5e..e4f2b7690 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -209,8 +209,8 @@ class PrivacySettingsViewModel: SessionTableViewModel ())? = nil) { - super.init(targetView: targetView, afterClosed: afterClosed) + override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) { + super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed) self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 476277407..509adaee6 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -50,6 +50,10 @@ class SettingsViewModel: SessionTableViewModel { - navState - .map { [weak self] navState -> [NavItem] in - switch navState { - case .standard: - return [ + let userSessionId: String = self.userSessionId + + return navState + .map { [weak self] navState -> [NavItem] in + switch navState { + case .standard: + return [ NavItem( id: .qrCode, image: UIImage(named: "QRCode")? @@ -117,10 +123,10 @@ class SettingsViewModel: SessionTableViewModel ())? = nil ) { - let imageFilePath: String? = ( - profilePictureFilePath ?? - ProfileManager.profileAvatarFilepath(id: self.userSessionId) - ) - let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in ProfileManager.updateLocal( queue: DispatchQueue.global(qos: .default), profileName: name, image: profilePicture, - imageFilePath: imageFilePath, + imageFilePath: profilePictureFilePath, success: { db, updatedProfile in if isUpdatingDisplayName { UserDefaults.standard[.lastDisplayNameUpdate] = Date() @@ -453,7 +522,9 @@ class SettingsViewModel: SessionTableViewModel Bool { - switch self { - case .whenEnabled: return (value == true) - case .whenDisabled: return (value == false) - case .always: return true - } + private static let imageSize: CGFloat = 80 + 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 + + return result + }() + + private lazy var imageView: UIImageView = { + let result: UIImageView = UIImageView() + result.clipsToBounds = true + result.contentMode = .scaleAspectFill + result.set(.width, to: ConfirmationModal.imageSize) + result.set(.height, to: ConfirmationModal.imageSize) + + return result + }() + + 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 + ) + + let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(bodyTapped) + ) + result.addGestureRecognizer(gestureRecogniser) + + 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.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(imageView) + imageView.center(.horizontal, in: imageViewContainer) + imageView.pin(.top, to: .top, of: imageViewContainer, withInset: 15) + imageView.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 style, let onClick): + mainStackView.spacing = 0 + imageView.image = (value ?? placeholder) + imageView.layer.cornerRadius = (style == .circular ? + (ConfirmationModal.imageSize / 2) : + 0 + ) + imageViewContainer.isHidden = false + internalOnBodyTap = onClick } + confirmButton.accessibilityLabel = info.confirmAccessibilityLabel + confirmButton.accessibilityIdentifier = info.confirmAccessibilityLabel + 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.cancelAccessibilityLabel + cancelButton.accessibilityIdentifier = info.cancelAccessibilityLabel + 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.accessibilityLabel + contentView.accessibilityIdentifier = info.accessibilityIdentifier + } + + // MARK: - Interaction + + @objc private func bodyTapped() { + 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 explanation: String? - let attributedExplanation: NSAttributedString? + let body: Body let accessibilityLabel: String? let accessibilityIdentifier: String? - public let stateToShow: State + public let showCondition: ShowCondition let confirmTitle: String? let confirmAccessibilityLabel: String? let confirmStyle: ThemeValue + let confirmEnabled: Bool let cancelTitle: String let cancelAccessibilityLabel: String? let cancelStyle: ThemeValue + let cancelEnabled: Bool + let hasCloseButton: Bool let dismissOnConfirm: Bool - let onConfirm: ((UIViewController) -> ())? + let dismissType: Modal.DismissType + let onConfirm: ((ConfirmationModal) -> ())? + let onCancel: ((ConfirmationModal) -> ())? let afterClosed: (() -> ())? // MARK: - Initialization public init( title: String, - explanation: String? = nil, - attributedExplanation: NSAttributedString? = nil, + body: Body = .none, accessibilityLabel: String? = nil, accessibilityId: String? = nil, - stateToShow: State = .always, + showCondition: ShowCondition = .none, confirmTitle: String? = nil, confirmAccessibilityLabel: String? = nil, confirmStyle: ThemeValue = .alert_text, + confirmEnabled: Bool = true, cancelTitle: String = "TXT_CANCEL_TITLE".localized(), cancelAccessibilityLabel: String? = nil, cancelStyle: ThemeValue = .danger, + cancelEnabled: Bool = true, + hasCloseButton: Bool = false, dismissOnConfirm: Bool = true, - onConfirm: ((UIViewController) -> ())? = nil, + dismissType: Modal.DismissType = .recursive, + onConfirm: ((ConfirmationModal) -> ())? = nil, + onCancel: ((ConfirmationModal) -> ())? = nil, afterClosed: (() -> ())? = nil ) { self.title = title - self.explanation = explanation - self.attributedExplanation = attributedExplanation + self.body = body self.accessibilityLabel = accessibilityLabel self.accessibilityIdentifier = accessibilityId - self.stateToShow = stateToShow + self.showCondition = showCondition self.confirmTitle = confirmTitle self.confirmAccessibilityLabel = confirmAccessibilityLabel self.confirmStyle = confirmStyle + self.confirmEnabled = confirmEnabled self.cancelTitle = cancelTitle self.cancelAccessibilityLabel = cancelAccessibilityLabel 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( - onConfirm: ((UIViewController) -> ())? = nil, + body: Body? = nil, + confirmEnabled: Bool? = nil, + cancelEnabled: Bool? = nil, + onConfirm: ((ConfirmationModal) -> ())? = nil, + onCancel: ((ConfirmationModal) -> ())? = nil, afterClosed: (() -> ())? = nil ) -> Info { return Info( title: self.title, - explanation: self.explanation, - attributedExplanation: self.attributedExplanation, + body: (body ?? self.body), accessibilityLabel: self.accessibilityLabel, - stateToShow: self.stateToShow, + showCondition: self.showCondition, confirmTitle: self.confirmTitle, confirmAccessibilityLabel: self.confirmAccessibilityLabel, confirmStyle: self.confirmStyle, + confirmEnabled: (confirmEnabled ?? self.confirmEnabled), cancelTitle: self.cancelTitle, cancelAccessibilityLabel: self.cancelAccessibilityLabel, 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) ) } @@ -99,165 +337,123 @@ public class ConfirmationModal: Modal { public static func == (lhs: ConfirmationModal.Info, rhs: ConfirmationModal.Info) -> Bool { return ( lhs.title == rhs.title && - lhs.explanation == rhs.explanation && - lhs.attributedExplanation == rhs.attributedExplanation && + lhs.body == rhs.body && lhs.accessibilityLabel == rhs.accessibilityLabel && - lhs.stateToShow == rhs.stateToShow && + lhs.showCondition == rhs.showCondition && lhs.confirmTitle == rhs.confirmTitle && lhs.confirmAccessibilityLabel == rhs.confirmAccessibilityLabel && lhs.confirmStyle == rhs.confirmStyle && + lhs.confirmEnabled == rhs.confirmEnabled && lhs.cancelTitle == rhs.cancelTitle && lhs.cancelAccessibilityLabel == rhs.cancelAccessibilityLabel && lhs.cancelStyle == rhs.cancelStyle && - lhs.dismissOnConfirm == rhs.dismissOnConfirm + 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) - explanation.hash(into: &hasher) - attributedExplanation.hash(into: &hasher) + body.hash(into: &hasher) accessibilityLabel.hash(into: &hasher) - stateToShow.hash(into: &hasher) + showCondition.hash(into: &hasher) confirmTitle.hash(into: &hasher) confirmAccessibilityLabel.hash(into: &hasher) confirmStyle.hash(into: &hasher) + confirmEnabled.hash(into: &hasher) cancelTitle.hash(into: &hasher) cancelAccessibilityLabel.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 - private let internalOnConfirm: (UIViewController) -> () - - // 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 + enum ShowCondition { + case none + case enabled + case disabled - 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 - - return result - }() + public func shouldShow(for value: Bool) -> Bool { + switch self { + case .none: return true + case .enabled: return (value == true) + case .disabled: return (value == false) + } + } + } - private lazy var confirmButton: UIButton = { - let result: UIButton = Modal.createButton( - title: "", - titleColor: .danger - ) - result.addTarget(self, action: #selector(confirmationPressed), for: .touchUpInside) - - return result - }() + // MARK: - Body - private lazy var buttonStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ confirmButton, cancelButton ]) - result.axis = .horizontal - result.distribution = .fillEqually + enum Body: Equatable, Hashable { + public enum ImageStyle: Equatable, Hashable { + case inherit + case circular + } - return result - }() - - private lazy var contentStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel ]) - 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 + 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( + placeholder: UIImage?, + value: UIImage?, + style: ImageStyle, + onClick: (() -> ()) ) - return result - }() - - private lazy var mainStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ]) - result.axis = .vertical - result.spacing = Values.largeSpacing - Values.smallFontSize / 2 - - return result - }() - - // MARK: - Lifecycle - - public init(targetView: UIView? = nil, info: Info) { - self.internalOnConfirm = { viewController in - if info.dismissOnConfirm { - viewController.dismiss(animated: true) + 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 lhsStyle, _), .image(let rhsPlaceholder, let rhsValue, let rhsStyle, _)): + return ( + lhsPlaceholder == rhsPlaceholder && + lhsValue == rhsValue && + lhsStyle == rhsStyle + ) + + default: return false } - - info.onConfirm?(viewController) } - super.init(targetView: targetView, afterClosed: info.afterClosed) - - self.modalPresentationStyle = .overFullScreen - self.modalTransitionStyle = .crossDissolve - - // Set the content based on the provided info - titleLabel.text = info.title - - // Note: We should only set the appropriate explanation/attributedExplanation value (as - // setting both when one is null can result in the other being removed) - if let explanation: String = info.explanation { - explanationLabel.text = explanation - } - - if let attributedExplanation: NSAttributedString = info.attributedExplanation { - explanationLabel.attributedText = attributedExplanation + 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 style, _): + placeholder.hash(into: &hasher) + value.hash(into: &hasher) + style.hash(into: &hasher) + } } - - explanationLabel.isHidden = ( - info.explanation == nil && - info.attributedExplanation == nil - ) - confirmButton.accessibilityLabel = info.confirmAccessibilityLabel - confirmButton.accessibilityIdentifier = info.confirmAccessibilityLabel - confirmButton.isAccessibilityElement = true - confirmButton.setTitle(info.confirmTitle, for: .normal) - confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal) - confirmButton.isHidden = (info.confirmTitle == nil) - cancelButton.accessibilityLabel = info.cancelAccessibilityLabel - cancelButton.accessibilityIdentifier = info.cancelAccessibilityLabel - cancelButton.isAccessibilityElement = true - cancelButton.setTitle(info.cancelTitle, for: .normal) - cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal) - - self.contentView.accessibilityLabel = info.accessibilityLabel - self.contentView.accessibilityIdentifier = info.accessibilityIdentifier - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func populateContentView() { - contentView.addSubview(mainStackView) - - mainStackView.pin(to: contentView) - } - - // MARK: - Interaction - - @objc private func confirmationPressed() { - internalOnConfirm(self) } } diff --git a/SessionUIKit/Components/Modal.swift b/SessionUIKit/Components/Modal.swift index f42d9bdd0..9d52c5748 100644 --- a/SessionUIKit/Components/Modal.swift +++ b/SessionUIKit/Components/Modal.swift @@ -6,6 +6,12 @@ import SessionUtilitiesKit open class Modal: UIViewController, UIGestureRecognizerDelegate { private static let cornerRadius: CGFloat = 11 + public enum DismissType: Equatable, Hashable { + case single + case recursive + } + + private let dismissType: DismissType private let afterClosed: (() -> ())? // MARK: - Components @@ -47,14 +53,19 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { public lazy var cancelButton: UIButton = { let result: UIButton = Modal.createButton(title: "cancel".localized(), titleColor: .textPrimary) - result.addTarget(self, action: #selector(close), for: .touchUpInside) + result.addTarget(self, action: #selector(cancel), for: .touchUpInside) return result }() // MARK: - Lifecycle - public init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) { + public init( + targetView: UIView? = nil, + dismissType: DismissType = .recursive, + afterClosed: (() -> ())? = nil + ) { + self.dismissType = dismissType self.afterClosed = afterClosed super.init(nibName: nil, bundle: nil) @@ -129,13 +140,22 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { // MARK: - Interaction - @objc func close() { + @objc public func cancel() { + close() + } + + @objc public final func close() { // Recursively dismiss all modals (ie. find the first modal presented by a non-modal // and get that to dismiss it's presented view controller) var targetViewController: UIViewController? = self - while targetViewController?.presentingViewController is Modal { - targetViewController = targetViewController?.presentingViewController + switch dismissType { + case .single: break + + case .recursive: + while targetViewController?.presentingViewController is Modal { + targetViewController = targetViewController?.presentingViewController + } } targetViewController?.presentingViewController?.dismiss(animated: true) { [weak self] in diff --git a/SessionUIKit/Style Guide/Values.swift b/SessionUIKit/Style Guide/Values.swift index fd12c70c9..a66d47801 100644 --- a/SessionUIKit/Style Guide/Values.swift +++ b/SessionUIKit/Style Guide/Values.swift @@ -21,7 +21,7 @@ public final class Values : NSObject { @objc public static let smallButtonHeight = isIPhone5OrSmaller ? CGFloat(24) : CGFloat(28) @objc public static let mediumButtonHeight = isIPhone5OrSmaller ? CGFloat(30) : CGFloat(34) @objc public static let largeButtonHeight = isIPhone5OrSmaller ? CGFloat(40) : CGFloat(45) - @objc public static let alertButtonHeight: CGFloat = 50 + @objc public static let alertButtonHeight: CGFloat = 51 // 19px tall font with 16px margins @objc public static let accentLineThickness = CGFloat(4) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 99cd2c61b..f59f8357e 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -653,7 +653,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { targetView: CurrentAppContext().frontmostViewController()?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(), + body: .text("INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text )