// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import SessionMessagingKit extension ContextMenuVC { struct Action { let icon: UIImage? let title: String let isEmojiAction: Bool let isEmojiPlus: Bool let isDismissAction: Bool let accessibilityLabel: String? let work: () -> Void // MARK: - Initialization init( icon: UIImage? = nil, title: String = "", isEmojiAction: Bool = false, isEmojiPlus: Bool = false, isDismissAction: Bool = false, accessibilityLabel: String? = nil, work: @escaping () -> Void ) { self.icon = icon self.title = title self.isEmojiAction = isEmojiAction self.isEmojiPlus = isEmojiPlus self.isDismissAction = isDismissAction self.accessibilityLabel = accessibilityLabel self.work = work } // MARK: - Actions static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(named: "ic_info"), title: "context_menu_info".localized(), accessibilityLabel: "Message info" ) { delegate?.info(cellViewModel, using: dependencies) } } static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(systemName: "arrow.triangle.2.circlepath"), title: (cellViewModel.state == .failedToSync ? "context_menu_resync".localized() : "context_menu_resend".localized() ), accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message") ) { delegate?.retry(cellViewModel, using: dependencies) } } static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(named: "ic_reply"), title: "context_menu_reply".localized(), accessibilityLabel: "Reply to message" ) { delegate?.reply(cellViewModel, using: dependencies) } } static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(named: "ic_copy"), title: "copy".localized(), accessibilityLabel: "Copy text" ) { delegate?.copy(cellViewModel, using: dependencies) } } static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_copy"), title: "vc_conversation_settings_copy_session_id_button_title".localized(), accessibilityLabel: "Copy Session ID" ) { delegate?.copySessionID(cellViewModel) } } static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(named: "ic_trash"), title: "TXT_DELETE_TITLE".localized(), accessibilityLabel: "Delete message" ) { delegate?.delete(cellViewModel, using: dependencies) } } static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(named: "ic_download"), title: "context_menu_save".localized(), accessibilityLabel: "Save attachment" ) { delegate?.save(cellViewModel, using: dependencies) } } static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(named: "ic_block"), title: "context_menu_ban_user".localized(), accessibilityLabel: "Ban user" ) { delegate?.ban(cellViewModel, using: dependencies) } } static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( icon: UIImage(named: "ic_block"), title: "context_menu_ban_and_delete_all".localized(), accessibilityLabel: "Ban user and delete" ) { delegate?.banAndDeleteAllMessages(cellViewModel, using: dependencies) } } static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( title: emoji.rawValue, isEmojiAction: true ) { delegate?.react(cellViewModel, with: emoji, using: dependencies) } } static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { return Action( isEmojiPlus: true, accessibilityLabel: "Add emoji" ) { delegate?.showFullEmojiKeyboard(cellViewModel, using: dependencies) } } static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action { return Action( isDismissAction: true ) { delegate?.contextMenuDismissed() } } } static func viewModelCanReply(_ cellViewModel: MessageViewModel) -> Bool { return ( cellViewModel.variant == .standardIncoming || ( cellViewModel.variant == .standardOutgoing && cellViewModel.state != .failed && cellViewModel.state != .sending ) ) } static func actions( for cellViewModel: MessageViewModel, recentEmojis: [EmojiWithSkinTones], currentUserPublicKey: String, currentUserBlinded15PublicKey: String?, currentUserBlinded25PublicKey: String?, currentUserIsOpenGroupModerator: Bool, currentThreadIsMessageRequest: Bool, delegate: ContextMenuActionDelegate?, using dependencies: Dependencies = Dependencies() ) -> [Action]? { switch cellViewModel.variant { case .standardIncomingDeleted, .infoCall, .infoScreenshotNotification, .infoMediaSavedNotification, .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate: // Let the user delete info messages and unsent messages return [ Action.delete(cellViewModel, delegate, using: dependencies) ] case .standardOutgoing, .standardIncoming: break } let canRetry: Bool = ( cellViewModel.variant == .standardOutgoing && ( cellViewModel.state == .failed || ( cellViewModel.threadVariant == .contact && cellViewModel.state == .failedToSync ) ) ) let canCopy: Bool = ( cellViewModel.cellType == .textOnlyMessage || ( ( cellViewModel.cellType == .genericAttachment || cellViewModel.cellType == .mediaMessage ) && (cellViewModel.attachments ?? []).count == 1 && (cellViewModel.attachments ?? []).first?.isVisualMedia == true && (cellViewModel.attachments ?? []).first?.isValid == true && ( (cellViewModel.attachments ?? []).first?.state == .downloaded || (cellViewModel.attachments ?? []).first?.state == .uploaded ) ) ) let canSave: Bool = ( cellViewModel.cellType == .mediaMessage && (cellViewModel.attachments ?? []) .filter { attachment in attachment.isValid && attachment.isVisualMedia && ( attachment.state == .downloaded || attachment.state == .uploaded ) }.isEmpty == false ) let canCopySessionId: Bool = ( cellViewModel.variant == .standardIncoming && cellViewModel.threadVariant != .community ) let canDelete: Bool = ( cellViewModel.threadVariant != .community || currentUserIsOpenGroupModerator || cellViewModel.authorId == currentUserPublicKey || cellViewModel.authorId == currentUserBlinded15PublicKey || cellViewModel.authorId == currentUserBlinded25PublicKey || cellViewModel.state == .failed ) let canBan: Bool = ( cellViewModel.threadVariant == .community && currentUserIsOpenGroupModerator ) let shouldShowEmojiActions: Bool = { if cellViewModel.threadVariant == .community { return OpenGroupManager.doesOpenGroupSupport( capability: .reactions, on: cellViewModel.threadOpenGroupServer ) } return !currentThreadIsMessageRequest }() let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false) let generatedActions: [Action] = [ (canRetry ? Action.retry(cellViewModel, delegate, using: dependencies) : nil), (viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate, using: dependencies) : nil), (canCopy ? Action.copy(cellViewModel, delegate, using: dependencies) : nil), (canSave ? Action.save(cellViewModel, delegate, using: dependencies) : nil), (canCopySessionId ? Action.copySessionID(cellViewModel, delegate) : nil), (canDelete ? Action.delete(cellViewModel, delegate, using: dependencies) : nil), (canBan ? Action.ban(cellViewModel, delegate, using: dependencies) : nil), (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate, using: dependencies) : nil), (shouldShowInfo ? Action.info(cellViewModel, delegate, using: dependencies) : nil), ] .appending( contentsOf: (shouldShowEmojiActions ? recentEmojis : []) .map { Action.react(cellViewModel, $0, delegate, using: dependencies) } ) .appending(Action.emojiPlusButton(cellViewModel, delegate, using: dependencies)) .compactMap { $0 } guard !generatedActions.isEmpty else { return [] } return generatedActions.appending(Action.dismiss(delegate)) } } // MARK: - Delegate protocol ContextMenuActionDelegate { func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func reply(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func copySessionID(_ cellViewModel: MessageViewModel) func delete(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func save(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies) func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) func contextMenuDismissed() }