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.
		
		
		
		
		
			
		
			
				
	
	
		
			519 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			519 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import Foundation
 | 
						|
import Combine
 | 
						|
import GRDB
 | 
						|
import SessionUIKit
 | 
						|
import SessionSnodeKit
 | 
						|
import SessionUtilitiesKit
 | 
						|
 | 
						|
public extension MessageViewModel {
 | 
						|
    struct DeletionBehaviours {
 | 
						|
        public enum Actions {
 | 
						|
            case individual([Behaviour])
 | 
						|
            case multiple([NamedAction])
 | 
						|
            
 | 
						|
            var count: Int {
 | 
						|
                switch self {
 | 
						|
                    case .individual: return 1
 | 
						|
                    case .multiple(let namedActions): return namedActions.count
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        public enum Behaviour {
 | 
						|
            case markAsDeleted(localOnly: Bool, ids: [Int64])
 | 
						|
            case deleteFromDatabase([Int64])
 | 
						|
            case cancelPendingSendJobs([Int64])
 | 
						|
            case preparedRequest(Network.PreparedRequest<Void>)
 | 
						|
        }
 | 
						|
        
 | 
						|
        public struct NamedAction {
 | 
						|
            public let title: String
 | 
						|
            public let isDefault: Bool
 | 
						|
            public let accessibility: Accessibility
 | 
						|
            let behaviours: [Behaviour]
 | 
						|
            
 | 
						|
            init(title: String, isDefault: Bool, accessibility: Accessibility, behaviours: [Behaviour]) {
 | 
						|
                self.title = title
 | 
						|
                self.isDefault = isDefault
 | 
						|
                self.accessibility = accessibility
 | 
						|
                self.behaviours = behaviours
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        public let title: String
 | 
						|
        public let body: String
 | 
						|
        public let actions: Actions
 | 
						|
        
 | 
						|
        /// Collect the actions and construct a publisher which triggers each action before returning the result
 | 
						|
        public func publisherForAction(at index: Int, using dependencies: Dependencies) -> AnyPublisher<Void, Error> {
 | 
						|
            guard index >= 0, index < actions.count else {
 | 
						|
                return Fail(error: StorageError.objectNotFound).eraseToAnyPublisher()
 | 
						|
            }
 | 
						|
            
 | 
						|
            let behaviours: [Behaviour] = {
 | 
						|
                switch actions {
 | 
						|
                    case .individual(let actionBehaviours): return actionBehaviours
 | 
						|
                    case .multiple(let namedActions): return namedActions[index].behaviours
 | 
						|
                }
 | 
						|
            }()
 | 
						|
            
 | 
						|
            var result: AnyPublisher<Void, Error> = Just(())
 | 
						|
                .setFailureType(to: Error.self)
 | 
						|
                .eraseToAnyPublisher()
 | 
						|
            
 | 
						|
            behaviours.forEach { behaviour in
 | 
						|
                switch behaviour {
 | 
						|
                    case .cancelPendingSendJobs(let ids):
 | 
						|
                        result = result.flatMapStorageWritePublisher(using: dependencies) { db, _ in
 | 
						|
                            /// Cancel any `messageSend` jobs related to the message we are deleting
 | 
						|
                            let jobs: [Job] = (try? Job
 | 
						|
                                .filter(Job.Columns.variant == Job.Variant.messageSend)
 | 
						|
                                .filter(ids.contains(Job.Columns.interactionId))
 | 
						|
                                .fetchAll(db))
 | 
						|
                                .defaulting(to: [])
 | 
						|
                            
 | 
						|
                            jobs.forEach { dependencies[singleton: .jobRunner].removePendingJob($0) }
 | 
						|
                            
 | 
						|
                            _ = try? Job.deleteAll(db, ids: jobs.compactMap { $0.id })
 | 
						|
                        }
 | 
						|
                    
 | 
						|
                    case .markAsDeleted(let localOnly, let ids):
 | 
						|
                        result = result.flatMapStorageWritePublisher(using: dependencies) { db, _ in
 | 
						|
                            try Interaction
 | 
						|
                                .fetchAll(db, ids: ids)
 | 
						|
                                .map { $0.markingAsDeleted(localOnly: localOnly) }
 | 
						|
                                .forEach { try $0.upserted(db) }
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                    case .deleteFromDatabase(let ids):
 | 
						|
                        result = result.flatMapStorageWritePublisher(using: dependencies) { db, _ in
 | 
						|
                            _ = try Interaction
 | 
						|
                                .filter(ids: ids)
 | 
						|
                                .deleteAll(db)
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                    case .preparedRequest(let preparedRequest):
 | 
						|
                        result = result
 | 
						|
                            .flatMap { _ in preparedRequest.send(using: dependencies) }
 | 
						|
                            .map { _, _ in () }
 | 
						|
                            .eraseToAnyPublisher()
 | 
						|
                }
 | 
						|
            }
 | 
						|
            
 | 
						|
            return result
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
public extension MessageViewModel.DeletionBehaviours {
 | 
						|
    static func deletionActions(
 | 
						|
        for cellViewModels: [MessageViewModel],
 | 
						|
        with threadData: SessionThreadViewModel,
 | 
						|
        using dependencies: Dependencies
 | 
						|
    ) -> MessageViewModel.DeletionBehaviours? {
 | 
						|
        enum SelectedMessageState {
 | 
						|
            case pendingOnly
 | 
						|
            case mixedPendingAndSent
 | 
						|
            
 | 
						|
            case incomingOnly
 | 
						|
            case outgoingOnly
 | 
						|
            case deletedOnly
 | 
						|
            case controlMessageOnly
 | 
						|
            
 | 
						|
            case mixedIncomingAndOutgoing
 | 
						|
            case mixedDeletedAndControlMessage
 | 
						|
            case mixed
 | 
						|
        }
 | 
						|
        
 | 
						|
        /// First determine the state of the selected messages
 | 
						|
        let state: SelectedMessageState = {
 | 
						|
            let allStates: Set<RecipientState.State> = Set(cellViewModels.map { $0.state })
 | 
						|
            let allVariants: Set<Interaction.Variant> = Set(cellViewModels.map { $0.variant })
 | 
						|
            let allHashes: Set<String> = Set(cellViewModels.compactMap { $0.serverHash ?? $0.openGroupServerMessageId.map { "\($0)" } })
 | 
						|
            
 | 
						|
            /// Determine the message types
 | 
						|
            let deletedMessageVariants: Set<Interaction.Variant> = Set(Interaction.Variant.allCases.filter { $0.isDeletedMessage })
 | 
						|
            let controlMessageVariants: Set<Interaction.Variant> = Set(Interaction.Variant.allCases.filter { $0.isInfoMessage })
 | 
						|
            let isIncomingOnly: Bool = (allVariants == [.standardIncoming])
 | 
						|
            let isOutgoingOnly: Bool = (allVariants == [.standardOutgoing])
 | 
						|
            let isDeletedOnly: Bool = allVariants.subtracting(deletedMessageVariants).isEmpty
 | 
						|
            let isControlMessageOnly: Bool = allVariants.subtracting(controlMessageVariants).isEmpty
 | 
						|
            
 | 
						|
            switch (isIncomingOnly, isOutgoingOnly, isDeletedOnly, isControlMessageOnly) {
 | 
						|
                case (true, false, false, false): return .incomingOnly
 | 
						|
                case (false, true, false, false):
 | 
						|
                    /// Determine the message statuses (if a message doesn't have a `serverHash` or an `openGroupMessageId` then consider
 | 
						|
                    /// it to be the same as a "pending" message)
 | 
						|
                    let pendingStates: Set<RecipientState.State> = [.failed, .sending]
 | 
						|
                    let sentStates: Set<RecipientState.State> = [.sent, .failedToSync, .syncing]
 | 
						|
                    let isPendingOnly: Bool = allStates.subtracting(pendingStates).isEmpty
 | 
						|
                    let isSentOnly: Bool = allStates.subtracting(sentStates).isEmpty
 | 
						|
                  
 | 
						|
                    switch (isPendingOnly, isSentOnly, allHashes.count == cellViewModels.count) {
 | 
						|
                        case (true, false, _): return .pendingOnly
 | 
						|
                        case (false, true, true): return .outgoingOnly
 | 
						|
                        default: return .mixedPendingAndSent
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                case (false, false, true, false): return .deletedOnly
 | 
						|
                case (false, false, false, true): return .controlMessageOnly
 | 
						|
                default: break
 | 
						|
            }
 | 
						|
            
 | 
						|
            /// Handle the combination types
 | 
						|
            let isIncomingAndOutgoing: Bool = allVariants.subtracting([.standardIncoming, .standardOutgoing]).isEmpty
 | 
						|
            let isDeletedAndControlMessageOnly: Bool = allVariants
 | 
						|
                .subtracting(controlMessageVariants)
 | 
						|
                .subtracting(deletedMessageVariants)
 | 
						|
                .isEmpty
 | 
						|
            
 | 
						|
            switch (isIncomingAndOutgoing, isDeletedAndControlMessageOnly) {
 | 
						|
                case (true, false): return .mixedIncomingAndOutgoing
 | 
						|
                case (false, true): return .mixedDeletedAndControlMessage
 | 
						|
                default: return .mixed
 | 
						|
            }
 | 
						|
        }()
 | 
						|
        
 | 
						|
        /// The user can only delete messages which are within the same group of states, either "pending" messages (`failed` & `sending`)
 | 
						|
        /// or "sent" messages (all other states), selecting a combination of states is not valid and shouldn't allow deletion
 | 
						|
        guard state != .mixedPendingAndSent else { return nil }
 | 
						|
        
 | 
						|
        /// The remaining deletion options are more complicated to determine
 | 
						|
        return dependencies[singleton: .storage].read { [dependencies] db -> MessageViewModel.DeletionBehaviours? in
 | 
						|
            let isAdmin: Bool = {
 | 
						|
                switch threadData.threadVariant {
 | 
						|
                    case .contact: return false
 | 
						|
                    case .group, .legacyGroup: return (threadData.currentUserIsClosedGroupAdmin == true)
 | 
						|
                    case .community:
 | 
						|
                        guard
 | 
						|
                            let server: String = threadData.openGroupServer,
 | 
						|
                            let roomToken: String = threadData.openGroupRoomToken
 | 
						|
                        else { return false }
 | 
						|
                        
 | 
						|
                        return dependencies[singleton: .openGroupManager].isUserModeratorOrAdmin(
 | 
						|
                            db,
 | 
						|
                            publicKey: threadData.currentUserSessionId,
 | 
						|
                            for: roomToken,
 | 
						|
                            on: server
 | 
						|
                        )
 | 
						|
                }
 | 
						|
            }()
 | 
						|
            
 | 
						|
            switch (state, isAdmin) {
 | 
						|
                /// Support local deletion only in all conversation types when all selcted messages are:
 | 
						|
                /// • Pending messages
 | 
						|
                /// • Deleted messages
 | 
						|
                /// • Control messages
 | 
						|
                case (.pendingOnly, _), (.deletedOnly, _), (.controlMessageOnly, _), (.mixedDeletedAndControlMessage, _):
 | 
						|
                    return MessageViewModel.DeletionBehaviours(
 | 
						|
                        title: "deleteMessage"
 | 
						|
                            .putNumber(cellViewModels.count)
 | 
						|
                            .localized(),
 | 
						|
                        body: (cellViewModels.count == 1 ?
 | 
						|
                            "deleteMessageDescriptionDevice".localized() :
 | 
						|
                            "deleteMessagesDescriptionDevice".localized()
 | 
						|
                        ),
 | 
						|
                        actions: .individual([
 | 
						|
                            .cancelPendingSendJobs(cellViewModels.map { $0.id }),
 | 
						|
                            .deleteFromDatabase(cellViewModels.map { $0.id })
 | 
						|
                        ])
 | 
						|
                    )
 | 
						|
                    
 | 
						|
                /// Support local "mark as deleted" in all conversation types when all selcted messages are:
 | 
						|
                /// • Incoming messages (when not an admin)
 | 
						|
                case (.incomingOnly, false):
 | 
						|
                    return MessageViewModel.DeletionBehaviours(
 | 
						|
                        title: "deleteMessage"
 | 
						|
                            .putNumber(cellViewModels.count)
 | 
						|
                            .localized(),
 | 
						|
                        body: (cellViewModels.count == 1 ?
 | 
						|
                            "deleteMessageDescriptionDevice".localized() :
 | 
						|
                            "deleteMessagesDescriptionDevice".localized()
 | 
						|
                        ),
 | 
						|
                        actions: .individual([
 | 
						|
                            .cancelPendingSendJobs(cellViewModels.map { $0.id }),
 | 
						|
                            .markAsDeleted(localOnly: true, ids: cellViewModels.map { $0.id })
 | 
						|
                        ])
 | 
						|
                    )
 | 
						|
                    
 | 
						|
                /// Support either local deletion or network deletion when:
 | 
						|
                /// • All messages were sent by the current user; or
 | 
						|
                /// • The current user is an admin
 | 
						|
                case (.outgoingOnly, _), (.incomingOnly, true), (.mixedIncomingAndOutgoing, true):
 | 
						|
                    return MessageViewModel.DeletionBehaviours(
 | 
						|
                        title: "deleteMessage"
 | 
						|
                            .putNumber(cellViewModels.count)
 | 
						|
                            .localized(),
 | 
						|
                        body: (cellViewModels.count == 1 ?
 | 
						|
                            "deleteMessageConfirm".localized() :
 | 
						|
                            "deleteMessagesConfirm".localized()
 | 
						|
                        ),
 | 
						|
                        actions: .multiple([
 | 
						|
                            NamedAction(
 | 
						|
                                title: "deleteMessageDeviceOnly".localized(),
 | 
						|
                                isDefault: !isAdmin, /// Default to "delete for me" for non-admins
 | 
						|
                                accessibility: Accessibility(identifier: "Delete for me"),
 | 
						|
                                behaviours: [
 | 
						|
                                    .cancelPendingSendJobs(cellViewModels.map { $0.id }),
 | 
						|
                                    .markAsDeleted(localOnly: true, ids: cellViewModels.map { $0.id })
 | 
						|
                                ]
 | 
						|
                            ),
 | 
						|
                            NamedAction(
 | 
						|
                                title: (threadData.threadIsNoteToSelf ?
 | 
						|
                                    "deleteMessageDevicesAll".localized() :
 | 
						|
                                    "deleteMessageEveryone".localized()
 | 
						|
                                ),
 | 
						|
                                isDefault: isAdmin, /// Default to "delete for everyone" for admins
 | 
						|
                                accessibility: Accessibility(identifier: "Delete for everyone"),
 | 
						|
                                behaviours: try {
 | 
						|
                                    /// The non-local deletion behaviours differ depending on the type of conversation
 | 
						|
                                    switch (threadData.threadVariant, isAdmin) {
 | 
						|
                                        /// **Note to Self or Contact Conversation**
 | 
						|
                                        /// Delete from all participant devices via an `UnsendRequest` (these will trigger their own sync messages)
 | 
						|
                                        /// Delete from the current users swarm
 | 
						|
                                        /// Delete from the current device
 | 
						|
                                        case (.contact, _):
 | 
						|
                                            let serverHashes: [String] = cellViewModels.compactMap { $0.serverHash }
 | 
						|
                                            let unsendRequests: [Network.PreparedRequest<Void>] = try cellViewModels.map { model in
 | 
						|
                                                try MessageSender.preparedSend(
 | 
						|
                                                    db,
 | 
						|
                                                    message: UnsendRequest(
 | 
						|
                                                        timestamp: UInt64(model.timestampMs),
 | 
						|
                                                        author: (model.variant == .standardOutgoing ?
 | 
						|
                                                            threadData.currentUserSessionId :
 | 
						|
                                                            model.authorId
 | 
						|
                                                        )
 | 
						|
                                                    )
 | 
						|
                                                    .with(
 | 
						|
                                                        expiresInSeconds: model.expiresInSeconds,
 | 
						|
                                                        expiresStartedAtMs: model.expiresStartedAtMs
 | 
						|
                                                    ),
 | 
						|
                                                    to: .contact(publicKey: model.threadId),
 | 
						|
                                                    namespace: .default,
 | 
						|
                                                    interactionId: nil,
 | 
						|
                                                    fileIds: [],
 | 
						|
                                                    using: dependencies
 | 
						|
                                                )
 | 
						|
                                            }
 | 
						|
                                            
 | 
						|
                                            /// Batch requests have a limited number of subrequests so make sure to chunk
 | 
						|
                                            /// the unsend requests accordingly
 | 
						|
                                            return [.cancelPendingSendJobs(cellViewModels.map { $0.id })]
 | 
						|
                                                .appending(
 | 
						|
                                                    contentsOf: try unsendRequests
 | 
						|
                                                        .chunked(by: Network.BatchRequest.childRequestLimit)
 | 
						|
                                                        .map { unsendRequestChunk in
 | 
						|
                                                                .preparedRequest(
 | 
						|
                                                                    try SnodeAPI.preparedBatch(
 | 
						|
                                                                        requests: unsendRequestChunk,
 | 
						|
                                                                        requireAllBatchResponses: false,
 | 
						|
                                                                        swarmPublicKey: threadData.threadId,
 | 
						|
                                                                        using: dependencies
 | 
						|
                                                                    ).map { _, _ in () }
 | 
						|
                                                                )
 | 
						|
                                                        }
 | 
						|
                                                )
 | 
						|
                                                .appending(
 | 
						|
                                                    .preparedRequest(
 | 
						|
                                                        try SnodeAPI.preparedDeleteMessages(
 | 
						|
                                                            serverHashes: serverHashes,
 | 
						|
                                                            requireSuccessfulDeletion: false,
 | 
						|
                                                            authMethod: try Authentication.with(
 | 
						|
                                                                db,
 | 
						|
                                                                swarmPublicKey: threadData.currentUserSessionId,
 | 
						|
                                                                using: dependencies
 | 
						|
                                                            ),
 | 
						|
                                                            using: dependencies
 | 
						|
                                                        )
 | 
						|
                                                        .map { _, _ in () }
 | 
						|
                                                    )
 | 
						|
                                                )
 | 
						|
                                                .appending(.markAsDeleted(localOnly: false, ids: cellViewModels.map { $0.id }))
 | 
						|
                                            
 | 
						|
                                        /// **Legacy Group Conversation**
 | 
						|
                                        /// Delete from all participant devices via an `UnsendRequest`
 | 
						|
                                        /// Delete from the current device
 | 
						|
                                        ///
 | 
						|
                                        /// **Note:** We **cannot** delete from the legacy group swarm
 | 
						|
                                        case (.legacyGroup, _):
 | 
						|
                                            let unsendRequests: [Network.PreparedRequest<Void>] = try cellViewModels.map { model in
 | 
						|
                                                try MessageSender.preparedSend(
 | 
						|
                                                    db,
 | 
						|
                                                    message: UnsendRequest(
 | 
						|
                                                        timestamp: UInt64(model.timestampMs),
 | 
						|
                                                        author: (model.variant == .standardOutgoing ?
 | 
						|
                                                            threadData.currentUserSessionId :
 | 
						|
                                                            model.authorId
 | 
						|
                                                        )
 | 
						|
                                                    )
 | 
						|
                                                    .with(
 | 
						|
                                                        expiresInSeconds: model.expiresInSeconds,
 | 
						|
                                                        expiresStartedAtMs: model.expiresStartedAtMs
 | 
						|
                                                    ),
 | 
						|
                                                    to: .closedGroup(groupPublicKey: model.threadId),
 | 
						|
                                                    namespace: .legacyClosedGroup,
 | 
						|
                                                    interactionId: nil,
 | 
						|
                                                    fileIds: [],
 | 
						|
                                                    using: dependencies
 | 
						|
                                                )
 | 
						|
                                            }
 | 
						|
                                            
 | 
						|
                                            /// Batch requests have a limited number of subrequests so make sure to chunk
 | 
						|
                                            /// the unsend requests accordingly
 | 
						|
                                            return [.cancelPendingSendJobs(cellViewModels.map { $0.id })]
 | 
						|
                                                .appending(
 | 
						|
                                                    contentsOf: try unsendRequests
 | 
						|
                                                        .chunked(by: Network.BatchRequest.childRequestLimit)
 | 
						|
                                                        .map { unsendRequestChunk in
 | 
						|
                                                                .preparedRequest(
 | 
						|
                                                                    try SnodeAPI.preparedBatch(
 | 
						|
                                                                        requests: unsendRequestChunk,
 | 
						|
                                                                        requireAllBatchResponses: false,
 | 
						|
                                                                        swarmPublicKey: threadData.threadId,
 | 
						|
                                                                        using: dependencies
 | 
						|
                                                                    ).map { _, _ in () }
 | 
						|
                                                                )
 | 
						|
                                                        }
 | 
						|
                                                )
 | 
						|
                                                .appending(.markAsDeleted(localOnly: false, ids: cellViewModels.map { $0.id }))
 | 
						|
                                            
 | 
						|
                                        /// **Group Conversation for Non Admin**
 | 
						|
                                        /// Delete from all participant devices via an `GroupUpdateDeleteMemberContentMessage`
 | 
						|
                                        /// Delete from the current device
 | 
						|
                                        case (.group, false):
 | 
						|
                                            let serverHashes: [String] = cellViewModels.compactMap { $0.serverHash }
 | 
						|
                                            
 | 
						|
                                            return [
 | 
						|
                                                .cancelPendingSendJobs(cellViewModels.map { $0.id }),
 | 
						|
                                                /// **Note:** No signature for member delete content
 | 
						|
                                                .preparedRequest(try MessageSender
 | 
						|
                                                    .preparedSend(
 | 
						|
                                                        db,
 | 
						|
                                                        message: GroupUpdateDeleteMemberContentMessage(
 | 
						|
                                                            memberSessionIds: [],
 | 
						|
                                                            messageHashes: serverHashes,
 | 
						|
                                                            sentTimestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(),
 | 
						|
                                                            authMethod: nil,
 | 
						|
                                                            using: dependencies
 | 
						|
                                                        ),
 | 
						|
                                                        to: .closedGroup(groupPublicKey: threadData.threadId),
 | 
						|
                                                        namespace: .groupMessages,
 | 
						|
                                                        interactionId: nil,
 | 
						|
                                                        fileIds: [],
 | 
						|
                                                        using: dependencies
 | 
						|
                                                    )),
 | 
						|
                                                .markAsDeleted(localOnly: false, ids: cellViewModels.map { $0.id })
 | 
						|
                                            ]
 | 
						|
                                            
 | 
						|
                                        /// **Group Conversation for Admin**
 | 
						|
                                        /// Delete from all participant devices via an `GroupUpdateDeleteMemberContentMessage`
 | 
						|
                                        /// Delete from the group swarm
 | 
						|
                                        /// Delete from the current device
 | 
						|
                                        case (.group, true):
 | 
						|
                                            guard
 | 
						|
                                                let ed25519SecretKey: Data = try? ClosedGroup
 | 
						|
                                                    .filter(id: threadData.threadId)
 | 
						|
                                                    .select(.groupIdentityPrivateKey)
 | 
						|
                                                    .asRequest(of: Data.self)
 | 
						|
                                                    .fetchOne(db)
 | 
						|
                                            else {
 | 
						|
                                                Log.error("[ConversationViewModel] Failed to retrieve groupIdentityPrivateKey when trying to delete messages from group.")
 | 
						|
                                                throw StorageError.objectNotFound
 | 
						|
                                            }
 | 
						|
                                            
 | 
						|
                                            let serverHashes: [String] = cellViewModels.compactMap { $0.serverHash }
 | 
						|
                                            
 | 
						|
                                            return [
 | 
						|
                                                .cancelPendingSendJobs(cellViewModels.map { $0.id }),
 | 
						|
                                                .preparedRequest(try MessageSender
 | 
						|
                                                    .preparedSend(
 | 
						|
                                                        db,
 | 
						|
                                                        message: GroupUpdateDeleteMemberContentMessage(
 | 
						|
                                                            memberSessionIds: [],
 | 
						|
                                                            messageHashes: serverHashes,
 | 
						|
                                                            sentTimestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(),
 | 
						|
                                                            authMethod: Authentication.groupAdmin(
 | 
						|
                                                                groupSessionId: SessionId(.group, hex: threadData.threadId),
 | 
						|
                                                                ed25519SecretKey: Array(ed25519SecretKey)
 | 
						|
                                                            ),
 | 
						|
                                                            using: dependencies
 | 
						|
                                                        ),
 | 
						|
                                                        to: .closedGroup(groupPublicKey: threadData.threadId),
 | 
						|
                                                        namespace: .groupMessages,
 | 
						|
                                                        interactionId: nil,
 | 
						|
                                                        fileIds: [],
 | 
						|
                                                        using: dependencies
 | 
						|
                                                    )),
 | 
						|
                                                .preparedRequest(try SnodeAPI
 | 
						|
                                                    .preparedDeleteMessages(
 | 
						|
                                                        serverHashes: serverHashes,
 | 
						|
                                                        requireSuccessfulDeletion: false,
 | 
						|
                                                        authMethod: Authentication.groupAdmin(
 | 
						|
                                                            groupSessionId: SessionId(.group, hex: threadData.threadId),
 | 
						|
                                                            ed25519SecretKey: Array(ed25519SecretKey)
 | 
						|
                                                        ),
 | 
						|
                                                        using: dependencies
 | 
						|
                                                    )
 | 
						|
                                                        .map { _, _ in () }),
 | 
						|
                                                .markAsDeleted(localOnly: false, ids: cellViewModels.map { $0.id })
 | 
						|
                                            ]
 | 
						|
                                            
 | 
						|
                                        /// **Community Conversation**
 | 
						|
                                        /// Delete from the SOGS
 | 
						|
                                        /// Delete from the current device
 | 
						|
                                        case (.community, _):
 | 
						|
                                            guard
 | 
						|
                                                let server: String = threadData.openGroupServer,
 | 
						|
                                                let roomToken: String = threadData.openGroupRoomToken
 | 
						|
                                            else {
 | 
						|
                                                Log.error("[ConversationViewModel] Failed to retrieve community info when trying to delete messages.")
 | 
						|
                                                throw StorageError.objectNotFound
 | 
						|
                                            }
 | 
						|
                                            
 | 
						|
                                            let deleteRequests: [Network.PreparedRequest] = try cellViewModels
 | 
						|
                                                .compactMap { $0.openGroupServerMessageId }
 | 
						|
                                                .map { messageId in
 | 
						|
                                                    try OpenGroupAPI
 | 
						|
                                                        .preparedMessageDelete(
 | 
						|
                                                            db,
 | 
						|
                                                            id: messageId,
 | 
						|
                                                            in: roomToken,
 | 
						|
                                                            on: server,
 | 
						|
                                                            using: dependencies
 | 
						|
                                                        )
 | 
						|
                                                }
 | 
						|
                                            
 | 
						|
                                            /// Batch requests have a limited number of subrequests so make sure to chunk
 | 
						|
                                            /// the unsend requests accordingly
 | 
						|
                                            return [.cancelPendingSendJobs(cellViewModels.map { $0.id })]
 | 
						|
                                                .appending(
 | 
						|
                                                    contentsOf: try deleteRequests
 | 
						|
                                                        .chunked(by: Network.BatchRequest.childRequestLimit)
 | 
						|
                                                        .map { deleteRequestsChunk in
 | 
						|
                                                                .preparedRequest(
 | 
						|
                                                                    try OpenGroupAPI.preparedBatch(
 | 
						|
                                                                        db,
 | 
						|
                                                                        server: server,
 | 
						|
                                                                        requests: deleteRequestsChunk,
 | 
						|
                                                                        using: dependencies
 | 
						|
                                                                    )
 | 
						|
                                                                    .map { _, _ in () }
 | 
						|
                                                                )
 | 
						|
                                                        }
 | 
						|
                                                )
 | 
						|
                                                .appending(.deleteFromDatabase(cellViewModels.map { $0.id }))
 | 
						|
                                    }
 | 
						|
                                }()
 | 
						|
                            )
 | 
						|
                        ])
 | 
						|
                    )
 | 
						|
                    
 | 
						|
                /// These remaining cases are not supported
 | 
						|
                case (.mixedPendingAndSent, _), (.mixed, _), (.mixedIncomingAndOutgoing, false): return nil
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |