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.
		
		
		
		
		
			
		
			
				
	
	
		
			271 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			271 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import UIKit
 | |
| import SessionMessagingKit
 | |
| import SessionUtilitiesKit
 | |
| 
 | |
| 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()
 | |
| }
 |