Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/ThreemaFramework/Business/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 64 kB image not shown  

Quelle  GroupManager.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import Foundation
import ThreemaEssentials

public final class GroupManager: NSObject, GroupManagerProtocol {
    
    public enum GroupError: Error {
        case creatorIsBlocked(groupIdentity: GroupIdentity)
        case creatorNotFound
        case membersMissing
        case groupConversationNotFound
        case groupNotFound
        case decodingFailed
        case blobIDOrKeyMissing
        case photoUploadFailed
        case notCreator
        case contactForCreatorMissing
        case contactForMemberMissing
        case groupSyncNewMembersFailed
    }
    
    /// Used for error handling when fetching unknown contacts
    private enum FetchedContactOrError: Equatable {
        case added
        case revokedOrInvalid(String)
        case blocked(String)
        case localNotFound
        case error
    }

    private let myIdentityStore: MyIdentityStoreProtocol
    private let contactStore: ContactStoreProtocol
    private let taskManager: TaskManagerProtocol
    private let userSettings: UserSettingsProtocol
    private let entityManager: EntityManager
    private let groupPhotoSender: () -> (GroupPhotoSenderProtocol)
    
    init(
        myIdentityStore: MyIdentityStoreProtocol,
        contactStore: ContactStoreProtocol,
        taskManager: TaskManagerProtocol,
        userSettings: UserSettingsProtocol,
        entityManager: EntityManager,
        groupPhotoSender: @escaping (() -> (GroupPhotoSenderProtocol))
    ) {
        self.myIdentityStore = myIdentityStore
        self.contactStore = contactStore
        self.taskManager = taskManager
        self.userSettings = userSettings
        self.entityManager = entityManager
        self.groupPhotoSender = groupPhotoSender
    }
    
    convenience init(entityManager: EntityManager, taskManager: TaskManagerProtocol = TaskManager()) {
        self.init(
            myIdentityStore: MyIdentityStore.shared(),
            contactStore: ContactStore.shared(),
            taskManager: taskManager,
            userSettings: UserSettings.shared(),
            entityManager: entityManager,
            groupPhotoSender: {
                GroupPhotoSender()
            }
        )
    }

    @available(swift, obsoleted: 1.0, renamed: "init(entityManager:taskManager:)", message: "Only use from Objective-C")
    @objc convenience init(entityManager: EntityManager, taskManagerObjc: TaskManager) {
        self.init(
            myIdentityStore: MyIdentityStore.shared(),
            contactStore: ContactStore.shared(),
            taskManager: taskManagerObjc,
            userSettings: UserSettings.shared(),
            entityManager: entityManager,
            groupPhotoSender: {
                GroupPhotoSender()
            }
        )
    }
    
    // MARK: - Create or update
    
    /// Create or update group members and send group create messages to members, if I'm the creator.
    ///
    /// Also sends ballot messages to new members, if necessary.
    ///
    /// - Parameters:
    ///   - groupIdentity: Identity of the group
    ///   - members: Members (identity list) of the group
    ///   - systemMessageDate: Date for new system message(s)
    /// - Returns: Group and list of new members (identity)
    /// - Throws: ThreemaError, GroupError.notCreator, TaskManagerError
    public func createOrUpdate(
        for groupIdentity: GroupIdentity,
        members: Set<String>,
        systemMessageDate: Date
    ) async throws -> (Group, Set<String>?) {

        guard groupIdentity.creator.rawValue.elementsEqual(myIdentityStore.identity) else {
            throw GroupError.notCreator
        }
        
        // Is oldMembers nil, means the group is new and there aren't old members
        var oldMembers: [String]?
        var removedMembers = [String]()

        // If group already exists get old and removed members
        entityManager.performAndWait {
            if let oldConversation = self.getConversation(for: groupIdentity) {
                oldMembers = oldConversation.unwrappedMembers.map(\.identity)
                removedMembers = oldMembers!.filter { !members.contains($0) }
            }
        }

        guard let group = try await createOrUpdateDB(
            for: groupIdentity,
            members: members,
            systemMessageDate: systemMessageDate,
            sourceCaller: .local
        ) else {
            throw GroupError.groupNotFound
        }

        var newMembers: Set<String>?
        await entityManager.perform {
            if let oldMembers,
               let conversation = self.getConversation(for: GroupIdentity(
                   id: group.groupID,
                   creator: group.groupIdentity.creator
               )) {
                newMembers = Set(
                    conversation.unwrappedMembers
                        .filter { !oldMembers.contains($0.identity) }
                        .map(\.identity)
                )
                if newMembers?.isEmpty == true {
                    newMembers = nil
                }
            }
        }

        if groupIdentity.creator.rawValue.elementsEqual(myIdentityStore.identity) {
            // Send group create message to each active member
            let task = TaskDefinitionSendGroupCreateMessage(
                group: group,
                to: group.allActiveMemberIdentitiesWithoutCreator,
                removed: removedMembers,
                members: members
            )

            taskManager.add(taskDefinition: task)
        }
        
        // Synchronize only new members
        if let newMembers {
            do {
                try await sync(group: group, to: newMembers, withoutCreateMessage: true)
            }
            catch {
                DDLogError("Unable to sync group for new group members")
                throw GroupError.groupSyncNewMembersFailed
            }
        }

        return (group, newMembers)
    }

    /// Create or update group members in DB.
    /// - Parameters:
    ///   - groupIdentity: Identity of the group
    ///   - members: Members (identity list) of the group
    ///   - systemMessageDate: Date for new system message(s), if `nil` no message is posted
    ///   - sourceCaller: Delete member (is hidden contact) only is not `SourceCaller.sync`
    /// - Returns: Created or updated group or is Nil when group is deleted
    /// - Throws: GroupError.contactForCreatorMissing, GroupError.contactForMemberMissing
    @discardableResult
    public func createOrUpdateDB(
        for groupIdentity: GroupIdentity,
        members: Set<String>,
        systemMessageDate: Date?,
        sourceCaller: SourceCaller
    ) async throws -> Group? {
        if !groupIdentity.creator.rawValue.elementsEqual(myIdentityStore.identity) {
            // Record a pseudo sync request so we won't trigger another one if we process
            // messages in this new group while we are still processing the group create
            recordSendSyncRequest(groupIdentity)
        }

        // Am I the creator? Then Conversation.contact and GroupEntity.groupCreator have to be `nil`.
        var creatorContact: ContactEntity?
        if !groupIdentity.creator.rawValue.elementsEqual(myIdentityStore.identity) {

            // If the creator blocked and group not found, then send leave messages to sender and all provided members
            if userSettings.blacklist.contains(groupIdentity.creator.rawValue),
               getGroup(groupIdentity.id, creator: groupIdentity.creator.rawValue) == nil {
                if members.contains(myIdentityStore.identity) {
                    var toMembers = [String](members)
                    if !toMembers.contains(groupIdentity.creator.rawValue) {
                        toMembers.append(groupIdentity.creator.rawValue)
                    }

                    leave(groupIdentity: groupIdentity, toMembers: toMembers)
                }

                DDLogWarn(
                    "Group (\(groupIdentity)) not created, because creator is blocked, i sent group leave messages to its members"
                )
                throw GroupError.creatorIsBlocked(groupIdentity: groupIdentity)
            }

            creatorContact = entityManager.entityFetcher.contactEntity(for: groupIdentity.creator.rawValue)
            guard creatorContact != nil else {
                throw GroupError.contactForCreatorMissing
            }
        }

        // Adjust group members and group state
        var allMembers = Set<String>(members)
        if !allMembers.contains(groupIdentity.creator.rawValue) {
            allMembers.insert(groupIdentity.creator.rawValue)
        }

        if allMembers.contains(myIdentityStore.identity) {
            // I'm member of this group
            var groupNewCreated = false

            // First fetch all contacts
            var identitiesToFetch = [String]()
            entityManager.performAndWait {
                for member in members.filter({ $0 != self.myIdentityStore.identity }) {
                    if self.entityManager.entityFetcher.contactEntity(for: member) == nil {
                        identitiesToFetch.append(member)
                    }
                }
            }
            
            let fetchedIdentities = try await fetchContacts(for: identitiesToFetch)

            return try await entityManager.performSave {
                let conversation: ConversationEntity
                if let existingConversation = self.entityManager.entityFetcher.conversationEntity(
                    for: groupIdentity,
                    myIdentity: self.myIdentityStore.identity
                ) {
                    conversation = existingConversation
                }
                else {
                    conversation = self.entityManager.entityCreator.conversationEntity()
                    conversation.groupID = groupIdentity.id
                    conversation.contact = creatorContact
                    conversation.groupMyIdentity = self.myIdentityStore.identity
                }

                let groupEntity: GroupEntity
                if let existingGroup = self.entityManager.entityFetcher.groupEntity(for: conversation) {
                    groupEntity = existingGroup
                }
                else {
                    groupEntity = self.entityManager.entityCreator.groupEntity(
                        groupID: groupIdentity.id,
                        state: NSNumber(value: GroupEntity.GroupState.active.rawValue)
                    )
                    groupEntity.groupCreator = creatorContact != nil ? groupIdentity.creator.rawValue : nil
                    groupNewCreated = true
                }
                groupEntity.lastPeriodicSync = Date()

                let currentMembers: [String] = conversation.unwrappedMembers.map(\.identity)

                // I am member of this group, set group state active
                if groupEntity.state != NSNumber(value: GroupEntity.GroupState.active.rawValue) {
                    groupEntity.state = NSNumber(value: GroupEntity.GroupState.active.rawValue)

                    if let systemMessageDate {
                        self.postSystemMessage(
                            in: conversation,
                            type: .groupSelfAdded,
                            arg: nil,
                            date: systemMessageDate
                        )
                    }
                }

                // My ID should be set on active group conversation (could be an old ID e.g. after restored a
                // backup)
                if let groupMyIdentity = conversation.groupMyIdentity,
                   !groupMyIdentity.elementsEqual(self.myIdentityStore.identity) {
                    conversation.groupMyIdentity = self.myIdentityStore.identity
                }

                // Remove deleted members
                for memberIdentity in currentMembers {
                    guard !allMembers.contains(memberIdentity) else {
                        continue
                    }

                    if let memberContact = self.entityManager.entityFetcher.contactEntity(for: memberIdentity) {
                        conversation.members?.remove(memberContact)

                        if let systemMessageDate {
                            self.postSystemMessage(
                                in: conversation,
                                member: memberContact,
                                type: .groupMemberForcedLeave,
                                date: systemMessageDate
                            )
                        }
                    }
                }

                // Add new members
                for memberIdentity in allMembers {
                    guard !currentMembers.contains(memberIdentity),
                          !memberIdentity.elementsEqual(self.myIdentityStore.identity) else {
                        continue
                    }

                    guard let contact = self.entityManager.entityFetcher.contactEntity(for: memberIdentity) else {
                        let isIdentityRevoked = fetchedIdentities.contains { contactState in
                            if case .revokedOrInvalid(memberIdentity) = contactState {
                                return true
                            }
                            else if case .blocked(memberIdentity) = contactState {
                                return true
                            }
                            return false
                        }
                        if isIdentityRevoked {
                            // Do nothing because the contact never existed or was revoked or blocked
                            DDLogVerbose("Skip invalid, revoked or blocked contact")
                            continue
                        }
                        else {
                            throw GroupError.contactForMemberMissing
                        }
                    }

                    conversation.members?.insert(contact)

                    if let systemMessageDate {
                        self.postSystemMessage(
                            in: conversation,
                            member: contact,
                            type: .groupMemberAdd,
                            date: systemMessageDate
                        )
                    }
                }

                if groupIdentity.creator.rawValue.elementsEqual(self.myIdentityStore.identity) {
                    // Check is note group or not anymore
                    if allMembers.count == 1, allMembers.contains(self.myIdentityStore.identity) {
                        self.postSystemMessage(
                            in: conversation,
                            type: .startNoteGroupInfo,
                            arg: nil,
                            date: Date()
                        )
                    }
                    else if !groupNewCreated, allMembers.count > 1, currentMembers.isEmpty {
                        self.postSystemMessage(
                            in: conversation,
                            type: .endNoteGroupInfo,
                            arg: nil,
                            date: Date()
                        )
                    }
                }

                let lastSyncRequestSince = Date(timeIntervalSinceNow: TimeInterval(-kGroupSyncRequestInterval))
                let lastSyncRequest = self.entityManager.entityFetcher.lastGroupSyncRequestEntity(
                    for: groupIdentity,
                    since: lastSyncRequestSince
                )

                let group = Group(
                    myIdentityStore: self.myIdentityStore,
                    userSettings: self.userSettings,
                    pushSettingManager: PushSettingManager(),
                    groupEntity: groupEntity,
                    conversation: conversation,
                    lastSyncRequest: lastSyncRequest?.lastSyncRequest
                )

                self.refreshRejectedMessages(in: group)

                return group
            }
        }
        else {
            if !groupIdentity.creator.rawValue.elementsEqual(myIdentityStore.identity) {
                // I'm not member or creator of the group
                return await entityManager.performSave {
                    if let groupEntity = self.entityManager.entityFetcher.groupEntity(
                        for: groupIdentity,
                        myIdentity: self.myIdentityStore.identity
                    ) {
                        var addSystemMessage = false

                        if groupEntity.state != NSNumber(value: GroupEntity.GroupState.forcedLeft.rawValue) {
                            groupEntity.state = NSNumber(value: GroupEntity.GroupState.forcedLeft.rawValue)
                            addSystemMessage = true
                        }

                        if let conversation = self.entityManager.entityFetcher.conversationEntity(
                            for: groupIdentity,
                            myIdentity: self.myIdentityStore.identity
                        ) {
                            if addSystemMessage, let systemMessageDate {
                                self.postSystemMessage(
                                    in: conversation,
                                    type: .groupSelfRemoved,
                                    arg: nil,
                                    date: systemMessageDate
                                )
                            }

                            let lastSyncRequestSince =
                                Date(timeIntervalSinceNow: TimeInterval(-kGroupSyncRequestInterval))
                            let lastSyncRequest = self.entityManager.entityFetcher.lastGroupSyncRequestEntity(
                                for: groupIdentity,
                                since: lastSyncRequestSince
                            )
                            
                            let group = Group(
                                myIdentityStore: self.myIdentityStore,
                                userSettings: self.userSettings,
                                pushSettingManager: PushSettingManager(),
                                groupEntity: groupEntity,
                                conversation: conversation,
                                lastSyncRequest: lastSyncRequest?.lastSyncRequest
                            )
                            
                            self.refreshRejectedMessages(in: group)
                            
                            return group
                        }
                        else {
                            DDLogWarn("Conversation entity for \(groupIdentity) not found")
                        }
                    }
                    else {
                        DDLogWarn("Group entity for \(groupIdentity) not found")
                    }

                    return nil
                }
            }
            else {
                DDLogNotice("I'm creator of the group \(groupIdentity) but not member")
            }

            return nil
        }
    }

    /// Objective-C bridge
    public func createOrUpdateDBObjc(
        groupID: Data,
        creator: String,
        members: Set<String>,
        systemMessageDate: Date?,
        sourceCaller: SourceCaller
    ) async throws {
        try await createOrUpdateDB(
            for: GroupIdentity(id: groupID, creator: ThreemaIdentity(creator)),
            members: members,
            systemMessageDate: systemMessageDate,
            sourceCaller: sourceCaller
        )
    }
    
    /// Individually fetches the contacts with the listed identities from the database or requests them individually
    /// from the directory server. This function will ignore the block unknown setting, because it should add all
    /// contacts to member list.
    /// Use `fetchContacts` to fetch multiple contacts
    /// - Parameter identities: identities to fetch from the database or directory server
    /// - Returns: The fetched contact or an error
    private func fetchContacts(for identities: [String]) async throws -> [FetchedContactOrError] {
        var fetchedResults = [FetchedContactOrError]()

        let prefetchedIdentities = try await prefetchIdentities(for: identities)
        for identity in prefetchedIdentities {
            await fetchedResults.append(
                withCheckedContinuation { continuation in
                    self.contactStore.fetchPublicKey(
                        for: identity,
                        acquaintanceLevel: .groupOrDeleted,
                        entityManager: self.entityManager,
                        ignoreBlockUnknown: true
                    ) { _ in
                        guard self.entityManager.entityFetcher.contactEntity(for: identity) != nil else {
                            continuation.resume(returning: .localNotFound)
                            return
                        }
                        continuation.resume(returning: .added)
                    } onError: { error in
                        DDLogError("Error fetch public key")
                        if let nsError = error as? NSError, nsError.domain == NSURLErrorDomain,
                           nsError.code == 404 {
                            continuation.resume(returning: .revokedOrInvalid(identity))
                        }
                        else if let nsError = error as? NSError,
                                nsError.code == ThreemaProtocolError.blockUnknownContact.rawValue {
                            continuation.resume(returning: .blocked(identity))
                        }
                        else {
                            continuation.resume(returning: .error)
                        }
                    }
                }
            )
        }

        return fetchedResults
    }

    private func prefetchIdentities(for identities: [String]) async throws -> Set<String> {
        try await withCheckedThrowingContinuation { continuation in
            self.contactStore.prefetchIdentityInfo(Set(identities)) {
                continuation.resume(returning: Set(identities))
            } onError: { error in
                continuation.resume(throwing: error)
            }
        }
    }

    // MARK: - Set name
    
    public func setName(
        groupID: Data,
        creator: String,
        name: String?,
        systemMessageDate: Date,
        send: Bool
    ) async throws {
        guard let grp = getGroup(groupID, creator: creator) else {
            throw GroupError.groupNotFound
        }
        return try await setName(group: grp, name: name, systemMessageDate: systemMessageDate, send: send)
    }
    
    /// Update group name in DB. If I'm the creator send group rename message (`GroupRenameMessage`) to members.
    ///
    /// - Parameters:
    ///   - group: Group to update name
    ///   - name: New name of the group
    ///   - systemMessageDate: Date for new system message
    ///   - send: Send group rename messages if I'm the creator?
    public func setName(
        group: Group,
        name: String?,
        systemMessageDate: Date,
        send: Bool
    ) async throws {
        guard group.name != name else {
            // Name didn't change
            return
        }
        
        await entityManager.performSave {
            guard let conversation = self.entityManager.entityFetcher.conversationEntity(
                for: group.groupIdentity,
                myIdentity: self.myIdentityStore.identity
            )
            else {
                return
            }

            conversation.groupName = name

            self.postSystemMessage(
                in: conversation,
                type: .renameGroup,
                arg: name?.data(using: .utf8),
                date: systemMessageDate
            )
        }
        
        if send, group.isOwnGroup {
            let task = createGroupRenameTask(for: group, to: group.allActiveMemberIdentitiesWithoutCreator)
            try await add(task: task)
        }
    }
    
    /// Objective-C bridge
    public func setNameObjc(
        groupID: Data,
        creator: String,
        name: String?,
        systemMessageDate: Date,
        send: Bool
    ) async throws {
        try await setName(groupID: groupID, creator: creator, name: name, systemMessageDate: systemMessageDate)
    }
    
    // MARK: - Set photo
    
    public func setPhoto(
        groupID: Data,
        creator: String,
        imageData: Data,
        sentDate: Date,
        send: Bool
    ) async throws {
        guard let grp = getGroup(groupID, creator: creator) else {
            throw GroupError.groupNotFound
        }
        return try await setPhoto(group: grp, imageData: imageData, sentDate: sentDate, send: send)
    }
    
    /// Update group photo and upload photo and send group set photo message (`GroupSetPhotoMessage`) to members, if I'm
    /// the creator.
    ///
    /// - Parameters:
    ///   - group: Group to update photo
    ///   - imageData: Photo raw image data, supporting UIImage
    ///   - sentDate: Sent date of set photo message
    ///   - send: If `True` and I'm the creator: upload photo and send group set photo messages
    /// - Throws: GroupError.groupConversationNotFound, GroupError.decodingFailed
    ///           GroupError.notCreator, GroupError.photoUploadFailed, GroupError.blobIDOrKeyMissing
    public func setPhoto(
        group: Group,
        imageData: Data,
        sentDate: Date,
        send: Bool
    ) async throws {

        let (conversationObjectID, conversationGroupImageSetDate, conversationGroupImageData) = await entityManager
            .perform {
                let conversation = self.entityManager.entityFetcher.conversationEntity(
                    for: group.groupIdentity,
                    myIdentity: self.myIdentityStore.identity
                )
                return (conversation?.objectID, conversation?.groupImageSetDate, conversation?.groupImage?.data)
            }

        guard let conversationObjectID else {
            throw GroupError.groupConversationNotFound
        }

        var imageDataSend: Data?
        
        // Check if this message is older than the last set date. This ensures that we're using
        // the latest image in case multiple images arrive for the same conversation in short succession.
        // Must do the check here (main thread) to avoid race condition.
        if let imageSetDate = conversationGroupImageSetDate,
           imageSetDate.compare(sentDate) == .orderedDescending {
            
            DDLogInfo("Ignoring older group set photo message")
            imageDataSend = conversationGroupImageData
        }
        else if let image = UIImage(data: imageData) {
            do {
                try entityManager.performAndWaitSave {
                    guard let conversation = self.entityManager.entityFetcher
                        .existingObject(with: conversationObjectID) as? ConversationEntity else {
                        throw GroupError.groupConversationNotFound
                    }
                    
                    let imageEntity: ImageDataEntity
                    
                    func setImageDataEntity(_ imageDataEntity: ImageDataEntity) {
                        imageDataEntity.data = imageData
                        imageDataEntity.width = Int16(image.size.width)
                        imageDataEntity.height = Int16(image.size.height)
                    }
                    
                    if let groupImageEntity = conversation.groupImage {
                        guard groupImageEntity.data != imageData else {
                            return
                        }
                        
                        setImageDataEntity(groupImageEntity)
                        imageEntity = groupImageEntity
                    }
                    else {
                        let newImageEntity = self.entityManager.entityCreator.imageDataEntity(
                            data: imageData,
                            size: image.size
                        )
                        imageEntity = newImageEntity
                    }
                    
                    conversation.groupImageSetDate = sentDate
                    conversation.groupImage = imageEntity

                    self.postSystemMessage(
                        in: conversation,
                        type: .groupProfilePictureChanged,
                        arg: nil,
                        date: Date()
                    )
                }
            }
            catch {
                throw error
            }

            imageDataSend = imageData
        }
        else {
            throw GroupError.decodingFailed
        }
        
        if send, group.isOwnGroup,
           let imageDataSend {
            try await sendPhoto(
                to: group,
                imageData: imageDataSend,
                toMembers: group.allActiveMemberIdentitiesWithoutCreator
            )
        }
    }
    
    /// Objective-C bridge
    public func setPhotoObjc(
        groupID: Data,
        creator: String,
        imageData: Data,
        sentDate: Date,
        send: Bool
    ) async throws {
        try await setPhoto(groupID: groupID, creator: creator, imageData: imageData, sentDate: sentDate, send: send)
    }
    
    // MARK: - Delete photo
    
    /// Delete group photo and send group delete photo message (`GroupDeletePhotoMessage`) to members, if I'm the
    /// creator.
    ///
    /// - Parameters:
    ///   - groupID: ID (8 bytes) of the group, unique with creator
    ///   - creator: Creator (identity) of the group, unique with ID
    ///   - sentDate: Sent date of delete photo message
    ///   - send: True send group delete photo message if I'm the creator
    ///  - Throws: GroupError.groupNotFound, GroupError.groupConversationNotFound
    public func deletePhoto(
        groupID: Data,
        creator: String,
        sentDate: Date,
        send: Bool
    ) async throws {
        guard let grp = getGroup(groupID, creator: creator) else {
            throw GroupError.groupNotFound
        }

        try await entityManager.performSave {
            guard let conversation = self.entityManager.entityFetcher.conversationEntity(
                for: grp.groupIdentity,
                myIdentity: self.myIdentityStore.identity
            ) else {
                throw GroupError.groupConversationNotFound
            }
            conversation.groupImageSetDate = sentDate
            
            if let groupImage = conversation.groupImage {
                self.entityManager.entityDestroyer.delete(imageDataEntity: groupImage)
                conversation.groupImage = nil
                
                self.postSystemMessage(
                    in: conversation,
                    type: .groupProfilePictureChanged,
                    arg: nil,
                    date: sentDate
                )
            }
        }

        if send, grp.isOwnGroup {
            let task = createDeletePhotoTask(for: grp, to: grp.allActiveMemberIdentitiesWithoutCreator)
            try await add(task: task)
        }
    }
    
    /// Objective-C bridge
    public func deletePhotoObjc(
        groupID: Data,
        creator: String,
        sentDate: Date,
        send: Bool
    ) async throws {
        try await deletePhoto(groupID: groupID, creator: creator, sentDate: sentDate, send: send)
    }
    
    // MARK: - Leave
    
    /// Send group leave and leave the group, admin of the group may not allowed to leave the group.
    ///
    /// - Parameters:
    ///   - groupID: ID (8 bytes) of the group, unique with creator
    ///   - creator: Creator (identity) of the group, unique with ID
    ///   - toMembers: Receivers of the group leave message, if nil send to all members of existing group
    ///   - systemMessageDate: Date for new system message
    @objc public func leave(groupID: Data, creator: String, toMembers: [String]?, systemMessageDate: Date) {
        guard creator != myIdentityStore.identity else {
            DDLogWarn("Group creator can't leave the group")
            return
        }

        // Send leave group message even I'm left the group already or the group not exists
        var currentMembers = [String]()
        var hiddenContacts = [String]()
        entityManager.performAndWait {
            if let conversation = self
                .getConversation(for: GroupIdentity(id: groupID, creator: ThreemaIdentity(creator))) {
                currentMembers = conversation.unwrappedMembers.map(\.identity)
                hiddenContacts = conversation.unwrappedMembers.filter(\.isHidden).map(\.identity)
            }
        }

        var sendToMembers = toMembers ?? currentMembers
        if sendToMembers.isEmpty {
            // Add Me as receiver to reflect group leave message
            sendToMembers.append(myIdentityStore.identity)
        }

        let task = TaskDefinitionSendGroupLeaveMessage(sendContactProfilePicture: false)
        task.groupID = groupID
        task.groupCreatorIdentity = creator
        task.fromMember = myIdentityStore.identity
        task.toMembers = sendToMembers
        task.hiddenContacts = hiddenContacts
        taskManager.add(taskDefinition: task)

        if let group = getGroup(groupID, creator: creator) {
            guard group.state != .left, group.state != .forcedLeft else {
                DDLogWarn("I can't left the group, I'm not member of this group anymore")
                return
            }
        }

        leaveDB(
            groupID: groupID,
            creator: creator,
            member: myIdentityStore.identity,
            systemMessageDate: systemMessageDate
        )
    }
    
    /// Remove member from group in DB.
    ///
    /// - Parameters:
    ///   - groupID: ID (8 bytes) of the group, unique with creator
    ///   - creator: Creator (identity) of the group, unique with ID
    ///   - member: Member who left the group
    ///   - systemMessageDate: Date for new system message
    public func leaveDB(groupID: Data, creator: String, member: String, systemMessageDate: Date) {
        entityManager.performAndWait {
            guard let grp = self.getGroup(groupID, creator: creator) else {
                DDLogWarn("Group not found")
                return
            }
            guard let groupEntity = self.entityManager.entityFetcher.groupEntity(
                for: grp.groupIdentity,
                myIdentity: self.myIdentityStore.identity
            )
            else {
                DDLogWarn("Group entity not found")
                return
            }
            guard let conversation = self
                .getConversation(for: GroupIdentity(id: groupID, creator: ThreemaIdentity(creator))) else {
                DDLogWarn("Conversation not found")
                return
            }

            DDLogInfo("Member \(member) left the group \(grp.groupID.hexString) \(creator)")

            if let contact = conversation.unwrappedMembers.first(where: { contact -> Bool in
                contact.identity.elementsEqual(member)
            }) {
                self.entityManager.performAndWaitSave {
                    _ = conversation.members?.remove(contact)
                }

                self.postSystemMessage(
                    in: conversation,
                    member: contact,
                    type: .groupMemberLeave,
                    date: systemMessageDate
                )

                if creator.elementsEqual(self.myIdentityStore.identity),
                   grp.isNoteGroup {
                    self.postSystemMessage(
                        in: conversation,
                        type: .startNoteGroupInfo,
                        arg: nil,
                        date: Date()
                    )
                }
                else if creator == member, !grp.isNoteGroup {
                    self.postSystemMessage(
                        in: conversation,
                        type: .groupCreatorLeft,
                        arg: nil,
                        date: Date()
                    )
                }
            }
            else if member.elementsEqual(self.myIdentityStore.identity), !grp.didLeave {
                self.entityManager.performAndWaitSave {
                    groupEntity.state = NSNumber(value: GroupEntity.GroupState.left.rawValue)
                    if !(conversation.groupMyIdentity?.elementsEqual(self.myIdentityStore.identity) ?? false) {
                        conversation.groupMyIdentity = self.myIdentityStore.identity
                    }
                }

                self.postSystemMessage(
                    in: conversation,
                    type: .groupSelfLeft,
                    arg: nil,
                    date: systemMessageDate
                )
            }
            
            self.refreshRejectedMessages(in: grp)
        }
    }

    /// Send empty member list to every group member to dissolve the group and i left the group.
    ///
    /// - Parameters:
    ///   - groupID: Group ID to suspend (I'm the group creator)
    ///   - identities: Identities to send dissolve to, if `nil` all group members get the dissolve message
    @objc public func dissolve(
        groupID: Data,
        to identities: Set<String>?
    ) {
        entityManager.performAndWait {
            if let group = self.getGroup(groupID, creator: self.myIdentityStore.identity) {
                guard group.isSelfCreator else {
                    return
                }

                // Add task to kick all active members (and reflect left group), and left the group
                let task = TaskDefinitionGroupDissolve(group: group)
                if let identities {
                    task.toMembers = Array(identities)
                }
                else {
                    task.toMembers = group.allActiveMemberIdentitiesWithoutCreator
                }

                self.taskManager.add(taskDefinition: task)

                if group.state == .active {
                    // I leave the group
                    self.leaveDB(
                        groupID: group.groupID,
                        creator: group.groupCreatorIdentity,
                        member: group.groupCreatorIdentity,
                        systemMessageDate: Date()
                    )
                }
            }
            else {
                // Group not found (means conversation was deleted), kick identities except me if I'm group creator and
                // has left the group
                guard let groupEntity = self.entityManager.entityFetcher.groupEntity(
                    for: groupID
                ),
                    groupEntity.didLeave,
                    let identities
                else {
                    return
                }

                let removeMembers = identities.filter { $0 != self.myIdentityStore.identity }
                if !removeMembers.isEmpty {
                    self.taskManager.add(
                        taskDefinition: TaskDefinitionSendGroupCreateMessage(
                            groupID: groupID,
                            groupCreatorIdentity: self.myIdentityStore.identity,
                            groupName: nil,
                            allGroupMembers: nil,
                            isNoteGroup: nil,
                            to: [],
                            removed: Array(removeMembers),
                            members: Set<String>(),
                            sendContactProfilePicture: false
                        )
                    )
                }
            }
        }
    }
       
    /// Send empty member list to the receivers, if I'm the group creator
    ///
    /// - Parameters:
    ///   - groupID: Group Identity
    ///   - identities: Identities to send empty member list to
    public func sendEmptyMemberList(
        groupIdentity: GroupIdentity,
        to identities: Set<ThreemaIdentity>
    ) {
        guard let group = getGroup(groupIdentity.id, creator: groupIdentity.creator.rawValue),
              group.isSelfCreator else {
            let errorMessage =
                "Could not send empty member list because the group does not exist or I am not the creator of the group (id: \(groupIdentity.id.hexString), creator: \(groupIdentity.creator.rawValue))"
            assertionFailure(errorMessage)
            DDLogError("\(errorMessage)")
            return
        }
        
        taskManager.add(
            taskDefinition: TaskDefinitionSendGroupCreateMessage(
                groupID: groupIdentity.id,
                groupCreatorIdentity: groupIdentity.creator.rawValue,
                groupName: nil,
                allGroupMembers: nil,
                isNoteGroup: nil,
                to: Array(identities.map(\.rawValue)),
                removed: nil,
                members: Set<String>(),
                sendContactProfilePicture: false
            )
        )
    }
    
    // MARK: - Sync
    
    /// Sync group information to identities.
    ///
    /// Send group create, rename and set photo message to each identity that is a member. Send a group create to
    /// non-members with an empty
    /// members list.
    ///
    /// - Parameters:
    ///   - group: Group to sync
    ///   - identities: Identities to send sync to, if `nil` all group members get the sync messages
    ///   - withoutCreateMessage: Should the create message also be sent to group members?
    ///                             (This is useful to send all group information to recently added members.)
    public func sync(
        group: Group,
        to identities: Set<String>?,
        withoutCreateMessage: Bool
    ) async throws {
        // Ensure that we are the creator
        guard group.isOwnGroup else {
            throw GroupError.notCreator
        }
        
        guard let identities else {
            // Sync to all members
            try await sync(
                group: group,
                to: group.allActiveMemberIdentitiesWithoutCreator,
                withoutCreateMessage: withoutCreateMessage
            )
            return
        }
        
        // If we have a list of receivers filter between members and non-members.
        var activeMembers = [String]()
        var removedMembers = [String]()
        for identity in identities {
            if group.allMemberIdentities.contains(identity) {
                activeMembers.append(identity)
            }
            else {
                removedMembers.append(identity)
            }
        }
        
        try await sync(group: group, to: activeMembers, withoutCreateMessage: withoutCreateMessage)
        try await syncToRemovedMembers(group: group, to: removedMembers)
    }
    
    public func syncObjc(
        group: Group,
        to identities: Set<String>?,
        withoutCreateMessage: Bool
    ) async throws {
        try await sync(group: group, to: identities, withoutCreateMessage: withoutCreateMessage)
    }
    
    /// Send sync request for this group, is not already requested in the last 7 days (see `kGroupSyncRequestInterval`).
    ///
    /// - Parameters:
    ///   - groupID: ID 8 Bytes
    ///   - creator: Creator of group
    ///   - force: If the minimum time interval should be ignored
    @objc public func sendSyncRequest(groupID: Data, creator: String, force: Bool) {
        
        let lastSyncRequestSince = Date(timeIntervalSinceNow: TimeInterval(-kGroupSyncRequestInterval))
        let groupIdentity = GroupIdentity(id: groupID, creator: ThreemaIdentity(creator))
        guard entityManager.entityFetcher.lastGroupSyncRequestEntity(
            for: groupIdentity,
            since: lastSyncRequestSince
        ) == nil || force else {
            DDLogInfo(
                "Sync for Group ID \(groupID.hexString) (creator \(creator)) already requested in the last \(kGroupSyncRequestInterval) s."
            )
            return
        }
        
        DDLogWarn("Group ID \(groupID.hexString) (creator \(creator)) not found. Requesting sync from creator.")
        
        // Fetch creator first, contact could be missing
        contactStore.fetchPublicKey(
            for: creator,
            acquaintanceLevel: .groupOrDeleted,
            entityManager: entityManager,
            ignoreBlockUnknown: false
        ) { _ in
            if self.entityManager.entityFetcher.contactEntity(for: creator) != nil {
                self.recordSendSyncRequest(GroupIdentity(id: groupID, creator: ThreemaIdentity(creator)))
                self.sendGroupSyncRequest(groupID, creator)
            }
            else {
                DDLogError("Could not send group request sync, because of missing group creator \(creator) contact")
            }
        } onError: { error in
            if let error = error as? NSError {
                DDLogError("Could not fetch public key for \(creator); Error: \(error.description) \(error.code) ")
            }
            else {
                DDLogError("Could not fetch public key for \(creator)")
            }
        }
    }
    
    /// Start periodic group sync if needed
    ///
    /// Sync tasks are guaranteed to be enqueued when this function returns, except of a potential set photo message
    ///
    /// - Parameter group: Group conversation to sync
    public func periodicSyncIfNeeded(for group: Group) {
        // Check if we are the creator
        guard group.isOwnGroup else {
            return
        }
        
        // Check if sync is needed
        if let lastPeriodicSync = group.lastPeriodicSync {
            let timeSinceLastSync = lastPeriodicSync.timeIntervalSinceNow
            if timeSinceLastSync > TimeInterval(-kGroupPeriodicSyncInterval) {
                // Last sync is shorter than `kGroupPeriodicSyncInterval` in the past
                return
            }
        }

        let toMembers = group.allActiveMemberIdentitiesWithoutCreator
        
        // Do these step synchronously to ensure they are enqueued before a potential outgoing
        // message leading to this call.
        // We don't care if the task execution fails eventually as the likelihood is low.
                
        guard let conversation =
            getConversation(for: GroupIdentity(id: group.groupID, creator: group.groupIdentity.creator))
        else {
            DDLogWarn("Coversation not found")
            return
        }

        // 5. Send a group-setup message with the current group members, ...
        let createTask = createGroupCreateSyncTask(for: group, conversation: conversation, to: toMembers)
        taskManager.add(taskDefinition: createTask)

        // ...followed by a group-name message to the sender.
        let sendNameTask = createGroupRenameTask(for: group, to: toMembers)
        taskManager.add(taskDefinition: sendNameTask)

        // 7. If the group has no profile picture, send a `delete-profile-picture` group control message to the sender.
        if group.old_ProfilePicture == nil {
            let deletePhotoTask = createDeletePhotoTask(for: group, to: toMembers)
            taskManager.add(taskDefinition: deletePhotoTask)
        }
                
        entityManager.performAndWaitSave {
            if let groupEntity = self.entityManager.entityFetcher.groupEntity(
                for: group.groupIdentity,
                myIdentity: self.myIdentityStore.identity
            ) {
                groupEntity.lastPeriodicSync = Date()
            }
        }
        
        // As sending a profile picture might take a while to upload we do it asynchronously and
        // don't guarantee it to be sent out before the a group message is sent
        
        // 6. If the group has a profile picture, send a `set-profile-picture` group control message to the sender.
        if let profilePicture = group.old_ProfilePicture {
            Task {
                do {
                    try await self.sendPhoto(to: group, imageData: profilePicture, toMembers: toMembers)
                }
                catch {
                    // Note: This might never be called if the task was persisted at some point in
                    // the meantime
                    DDLogError("Periodic group sync photo sending failed: \(error)")
                    // Reset last periodic sync date if photo sending failed
                    await self.entityManager.performSave {
                        if let groupEntity = self.entityManager.entityFetcher.groupEntity(
                            for: group.groupIdentity,
                            myIdentity: self.myIdentityStore.identity
                        ) {
                            groupEntity.lastPeriodicSync = nil
                        }
                    }
                }
            }
        }
    }
    
    // MARK: - Get group / conversation

    public func getConversation(for groupIdentity: GroupIdentity) -> ConversationEntity? {
        entityManager.entityFetcher.conversationEntity(for: groupIdentity, myIdentity: myIdentityStore.identity)
    }

    /// Loads group, conversation and LastGroupSyncRequest from DB.
    ///
    /// - Parameters:
    ///   - groupID: ID 8 Bytes
    ///   - creator: Creator of group
    /// - Returns: The group or nil
    @objc public func getGroup(_ groupID: Data, creator: String) -> Group? {
        var group: Group?

        entityManager.performAndWait {
            let groupIdentity = GroupIdentity(id: groupID, creator: ThreemaIdentity(creator))
            guard let conversation = self.entityManager.entityFetcher.conversationEntity(
                for: groupIdentity, myIdentity: self.myIdentityStore.identity
            ) else {
                return
            }
            guard let groupEntity = self.entityManager.entityFetcher.groupEntity(for: conversation) else {
                return
            }
            group = self.getGroup(groupEntity: groupEntity, conversation: conversation)
        }

        return group
    }
    
    /// Loads group for conversation.
    ///
    /// - Parameter conversation: Conversation for group
    /// - Returns: The group or nil
    @objc public func getGroup(conversation: ConversationEntity) -> Group? {
        var group: Group?

        entityManager.performAndWait {
            guard let groupEntity = self.entityManager.entityFetcher.groupEntity(for: conversation) else {
                return
            }
            group = self.getGroup(groupEntity: groupEntity, conversation: conversation)
        }

        return group
    }
    
    public func getAllActiveGroups() async -> [Group] {
        await entityManager.perform {
            let allActiveGroupEntities = self.entityManager.entityFetcher.activeGroupEntities() ?? []
            
            return allActiveGroupEntities.compactMap { groupEntity in
                // Because the entity fetcher for group conversations needs a creator identity we set them to our own
                // identity. If our ID changed in the meantime (e.g. through a restore were the data is restored, but a
                // new ID created) we will still fetch the correct group, that we're not really part of anymore, but
                // might be still marked as active...
                guard let creatorIdentity = groupEntity.groupCreator ?? self.myIdentityStore.identity else {
                    return nil
                }
                
                let groupIdentity = GroupIdentity(id: groupEntity.groupID, creator: ThreemaIdentity(creatorIdentity))

                guard let conversation = self.entityManager.entityFetcher.conversationEntity(
                    for: groupIdentity, myIdentity: self.myIdentityStore.identity
                ) else {
                    return nil
                }

                return self.getGroup(groupEntity: groupEntity, conversation: conversation)
            }
        }
    }

    private func getGroup(groupEntity: GroupEntity, conversation: ConversationEntity) -> Group {
        let creator: String = groupEntity.groupCreator ?? myIdentityStore.identity
        let groupIdentity = GroupIdentity(id: groupEntity.groupID, creator: ThreemaIdentity(creator))

        let lastSyncRequestSince = Date(timeIntervalSinceNow: TimeInterval(-kGroupSyncRequestInterval))
        
        let lastSyncRequest = entityManager.entityFetcher.lastGroupSyncRequestEntity(
            for: groupIdentity,
            since: lastSyncRequestSince
        )

        return Group(
            myIdentityStore: myIdentityStore,
            userSettings: userSettings,
            pushSettingManager: PushSettingManager(),
            groupEntity: groupEntity,
            conversation: conversation,
            lastSyncRequest: lastSyncRequest?.lastSyncRequest
        )
    }
    
    /// Get all members of existing group except me. It will remove the hidden flag for all members.
    ///
    /// - Parameters:
    ///   - groupID: ID 8 Bytes
    ///   - creator: Creator of group
    /// - Returns: Members of the group
    func getGroupMembersForClone(_ groupID: Data, creator: String) -> Set<ContactEntity>? {
        guard let conversation =
            getConversation(for: GroupIdentity(id: groupID, creator: ThreemaIdentity(creator))) else {
            DDLogError("Group conversation not found")
            return nil
        }
                
        // Mark all contacts as visible
        removeContactHiddenFlags(for: conversation)

        return conversation.unwrappedMembers.filter { member -> Bool in
            !member.identity.elementsEqual(myIdentityStore.identity)
        }
    }
    
    // MARK: - Private functions
    
    private func postSystemMessage(
        in conversation: ConversationEntity,
        member: ContactEntity,
        type: SystemMessageEntity.SystemMessageEntityType,
        date: Date
    ) {
        postSystemMessage(in: conversation, type: type, arg: Data(member.displayName.utf8), date: date)
    }
    
    private func postSystemMessage(
        in conversation: ConversationEntity,
        type: SystemMessageEntity.SystemMessageEntityType,
        arg: Data?,
        date: Date
    ) {
        entityManager.performAndWaitSave {
            // Insert system message to document this change
            let sysMsg = self.entityManager.entityCreator.systemMessageEntity(
                for: type,
                in: conversation
            )
            sysMsg.arg = arg
            sysMsg.date = date
            sysMsg.remoteSentDate = date
        }
    }
    
    // MARK: Sync
    
    private func sync(
        group: Group,
        to toMembers: [String],
        withoutCreateMessage: Bool
    ) async throws {
        /// Sync group if `toMembers` empty to reflect note groups if multi device activated.
        guard !(toMembers.isEmpty && !userSettings.enableMultiDevice) else {
            return
        }

        // Prepare sync tasks
    
        func runGroupCreateTask() async throws {
            guard !withoutCreateMessage else {
                return
            }
            
            guard let conversation =
                getConversation(for: GroupIdentity(id: group.groupID, creator: group.groupIdentity.creator))
            else {
                throw GroupError.groupConversationNotFound
            }

            // 5. Send a group-setup message with the current group members, ...
            let task = createGroupCreateSyncTask(for: group, conversation: conversation, to: toMembers)
            try await add(task: task)
        }
        
        // ...followed by a group-name message to the sender.
        func runGroupNameTask() async throws {
            let task = createGroupRenameTask(for: group, to: toMembers)
            try await add(task: task)
        }
        
        func runGroupPhotoTask() async throws {
            var profilePicture: Data?
            await entityManager.perform {
                profilePicture = group.old_ProfilePicture
            }

            guard let data = profilePicture else {
                // 7. If the group has no profile picture, send a `delete-profile-picture` group
                // control message to the sender.
                let task = createDeletePhotoTask(for: group, to: toMembers)
                try await add(task: task)
                return
            }
            
            // 6. If the group has a profile picture, send a `set-profile-picture` group control
            // message to the sender.
            try await sendPhoto(to: group, imageData: data, toMembers: toMembers)
        }
        
        try await runGroupCreateTask()
        try await runGroupNameTask()
        try await runGroupPhotoTask()
    }
    
    private func syncToRemovedMembers(
        group: Group,
        to removedMembers: [String]
    ) async throws {
        guard !removedMembers.isEmpty else {
            return
        }
        guard let conversation =
            getConversation(for: GroupIdentity(id: group.groupID, creator: group.groupIdentity.creator))
        else {
            return
        }
        
        let members = Set(conversation.unwrappedMembers.map(\.identity))
        let emptyCreateTask = TaskDefinitionSendGroupCreateMessage(
            group: group,
            to: [],
            removed: removedMembers,
            members: members
        )
        
        try await add(task: emptyCreateTask)
    }
    
    private func sendPhoto(
        to group: Group,
        imageData: Data,
        toMembers: [String]
    ) async throws {
        guard group.isOwnGroup else {
            throw GroupError.notCreator
        }
        
        // Only capture blob data inside the callback — no CoreData access.
        let (blobID, encryptionKey): (Data, Data) = try await withCheckedThrowingContinuation { continuation in
            groupPhotoSender().start(
                withImageData: imageData,
                isNoteGroup: group.isNoteGroup
            ) { blobID, encryptionKey in
                guard let blobID, let encryptionKey else {
                    continuation.resume(throwing: GroupError.blobIDOrKeyMissing)
                    return
                }
                continuation.resume(returning: (blobID, encryptionKey))
            } onError: { _ in
                continuation.resume(throwing: GroupError.photoUploadFailed)
            }
        }

        let task = TaskDefinitionSendGroupSetPhotoMessage(
            group: group,
            from: myIdentityStore.identity,
            to: toMembers,
            size: UInt32(imageData.count),
            blobID: blobID,
            encryptionKey: encryptionKey
        )

        try await add(task: task)
    }
        
    private func createGroupCreateSyncTask(
        for group: Group,
        conversation: ConversationEntity,
        to toMembers: [String]
    ) -> TaskDefinitionSendGroupCreateMessage {
        let members = entityManager.performAndWait {
            Set(conversation.unwrappedMembers.map(\.identity))
        }
        
        return TaskDefinitionSendGroupCreateMessage(
            group: group,
            to: toMembers,
            members: members
        )
    }
    
    private func createGroupRenameTask(
        for group: Group,
        to toMembers: [String]
    ) -> TaskDefinitionSendGroupRenameMessage {
        TaskDefinitionSendGroupRenameMessage(
            group: group,
            from: myIdentityStore.identity,
            to: toMembers,
            newName: group.name
        )
    }
    
    private func createDeletePhotoTask(
        for group: Group,
        to toMembers: [String]
    ) -> TaskDefinitionSendGroupDeletePhotoMessage {
        TaskDefinitionSendGroupDeletePhotoMessage(
            group: group,
            from: myIdentityStore.identity,
            to: toMembers,
            sendContactProfilePicture: false
        )
    }
    
    private func add(task: TaskDefinitionProtocol) async throws {
        // swiftformat:disable:next all
        return try await withCheckedThrowingContinuation { continuation in
            taskManager.add(taskDefinition: task) { _, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }
                continuation.resume()
            }
        }
    }
    
    private func recordSendSyncRequest(_ groupIdentity: GroupIdentity) {
        entityManager.performAndWaitSave {
            // Record this sync request
            _ = self.entityManager.entityCreator.lastGroupSyncRequestEntity(
                groupIdentity: groupIdentity,
                lastSyncRequest: .now
            )
        }
    }
    
    /// Caution: Use this only for testing!
    public func deleteAllSyncRequestRecords() {
        if let entities = entityManager.entityFetcher.lastGroupSyncRequestEntities() {
            for entity in entities {
                entityManager.entityDestroyer.delete(lastGroupSyncRequestEntity: entity)
            }
        }
    }
    
    private func sendGroupSyncRequest(_ groupID: Data, _ creator: String) {
        let msg = GroupRequestSyncMessage()
        msg.groupID = groupID
        msg.groupCreator = creator
        msg.toIdentity = creator
        
        let task = TaskDefinitionSendAbstractMessage(message: msg)
        taskManager.add(taskDefinition: task)
    }
    
    private func removeContactHiddenFlags(for conversation: ConversationEntity) {
        // Get all hidden contacts and mark them as visible
        let hiddenMembers = conversation.unwrappedMembers.filter { member -> Bool in
            !member.identity.elementsEqual(myIdentityStore.identity)
                && member.isHidden
        }
        
        if !hiddenMembers.isEmpty {
            entityManager.performAndWaitSave {
                for member in hiddenMembers {
                    member.isHidden = false
                }
            }
        }
    }

    // MARK: Rejected Messages Refresh Steps

    /// Implementation of `Rejected Messages Refresh Steps`
    ///
    /// From the protocol documentation:
    /// > [...] run every time the group members are being updated
    ///
    /// - Parameter group: Group to run Rejected Messages Refresh Steps on
    private func refreshRejectedMessages(in group: Group) {
        // 2. If `group` is marked as _left_:
        if group.state == .left || group.state == .forcedLeft {
            resetAllRejectedMessages(in: group)
        }
        // 3. If `group` is not marked as _left_:
        else {
            updateAllRejectedMessages(in: group)
        }
    }
    
    /// Don't call this directly. Use `refreshRejectedMessages(in:)` instead
    private func resetAllRejectedMessages(in group: Group) {
        //    1. For each `message` of `group` that has a _re-send requested_ mark,
        //       remove the mark and the list of receivers requiring a re-send.
        entityManager.performAndWaitSave {
            let messageFetcher = MessageFetcher(for: group.conversation, with: self.entityManager)
            let allRejectedMessages = messageFetcher.rejectedGroupMessages()
            
            for rejectedMessage in allRejectedMessages {
                rejectedMessage.rejectedBy = Set()
                // We only reset `sendFailed` if the message was sent successfully before. As `sendFailed` might not
                // have been set because of rejections. In theory this should never happen as an unsent message should
                // never have been rejected.
                if rejectedMessage.sent.boolValue {
                    rejectedMessage.sendFailed = false
                }
            }
        }
    }
    
    /// Don't call this directly. Use `refreshRejectedMessages(in:)` instead
    private func updateAllRejectedMessages(in group: Group) {
        
        entityManager.performAndWaitSave {
            //    1. Let `members` be the current list of members for `group`.
            let members = group.members.map(\.identity)
            
            //    2. For each `message` of `group` that has a _re-send requested_ mark:
            let messageFetcher = MessageFetcher(for: group.conversation, with: self.entityManager)
            let allRejectedMessages = messageFetcher.rejectedGroupMessages()
            for rejectedMessage in allRejectedMessages {
                self.updateRejectedMessage(rejectedMessage, with: members)
            }
        }
    }
    
    private func updateRejectedMessage(_ rejectedMessage: BaseMessageEntity, with groupMembers: [ThreemaIdentity]) {
        
        //       1. Let `receivers` be the list of receivers requiring a re-send for
        //          `message`.
        
        guard let rejectedByContactsList = rejectedMessage.rejectedBy?.map({
            ThreemaIdentity($0.identity)
        }) else {
            return
        }
        let rejectedByContacts = Set(rejectedByContactsList)

        //       2. Remove all entries from `receivers` that are not present in
        //          `members`.
        
        let contactsToRemove = rejectedByContacts.subtracting(groupMembers)
        for contactToRemove in contactsToRemove {
            guard let contact = entityManager.entityFetcher.contactEntity(for: contactToRemove.rawValue) else {
                continue
            }
            rejectedMessage.removeFromRejectedBy(contact)
        }
        
        //       3. If `receivers` is now empty, remove the _re-send requested_ mark on
        //          `message`.
        
        // We only reset `sendFailed` if the message was sent successfully before. As `sendFailed` might not
        // have been set because of rejections. In theory this should never happen as an unsent message should
        // never have been rejected.
        if rejectedMessage.rejectedBy?.isEmpty ?? true, rejectedMessage.sent.boolValue {
            rejectedMessage.sendFailed = false
        }
    }
}

Messung
CHZG
87949993

¤ Dauer der Verarbeitung: 0.31 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.