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

Quelle  Group.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import Foundation
import GroupCalls
import Intents
import ThreemaEssentials
import ThreemaMacros
import ThreemaProtocols
import UIKit

/// Business representation of a Threema group
public final class Group: NSObject {
    
    // These strings are used as static properties for performance reasons
    public static let maxGroupMembers = BundleUtil.object(forInfoDictionaryKey: "ThreemaMaxGroupMembers") as? Int ?? 0
    private static let meString = #localize("me")
    private static let unknownString = #localize("(unknown)")
    private static let oneMemberTitleString = #localize("group_one_member_title")
    private static let multipleMemberTitleString = #localize("group_multiple_members_title")
    
    // MARK: - Internal types
    
    /// A member in a group
    public enum Member: CustomStringConvertible, Equatable {
        case me(String)
        case contact(Contact)
        case unknown
        
        public var shortDisplayName: String {
            switch self {
            case .me:
                meString
            case let .contact(contact):
                contact.shortDisplayName
            case .unknown:
                unknownString
            }
        }
        
        // CustomStringConvertible
        public var description: String {
            switch self {
            case .me:
                meString
            case let .contact(contact):
                contact.displayName
            case .unknown:
                unknownString
            }
        }

        public var identity: String? {
            switch self {
            case let .contact(contact): contact.identity.rawValue
            case let .me(identity): identity
            default: nil
            }
        }
    }
    
    /// Creator of a group (same as `Member`)
    public typealias Creator = Member
    
    // MARK: - Observation
    
    /// This will be set to `true` when a group is in the process to be deleted.
    ///
    /// This can be used to detect deletion in KVO-observers
    @objc public private(set) dynamic var willBeDeleted = false
    
    // Tokens for entity subscriptions, will be removed when is deallocated
    private var subscriptionTokens = [EntityObserver.SubscriptionToken]()

    @objc public private(set) dynamic var state: GroupEntity.GroupState
    @objc public private(set) dynamic var members: Set<Contact>
    @objc public private(set) dynamic var name: String?
    @objc public private(set) dynamic var old_ProfilePicture: Data?
    @objc public private(set) dynamic lazy var profilePicture: UIImage = resolveProfilePicture()
    
    // MARK: - Public properties

    @available(*, deprecated, message: "Do not use anymore, load CoreData conversation separated")
    @objc public let conversation: ConversationEntity

    public let conversationObjectID: NSManagedObjectID

    public let groupIdentity: GroupIdentity
    
    public var pushSetting: PushSetting {
        pushSettingManager.find(forGroup: groupIdentity)
    }
    
    public private(set) var lastSyncRequest: Date?
    public private(set) var lastUpdate: Date?
    public private(set) var lastMessageDate: Date?
    
    /// True if group is created by a gateway ID and if group is message storing
    public var isMessageStoringGatewayGroup: Bool {
        isGatewayGroup && name?.hasPrefix(Constants.messageStoringGatewayGroupPrefix) ?? false
    }
    
    /// Returns true if me is group creator
    @objc public var isSelfCreator: Bool {
        (conversationGroupMyIdentity?.elementsEqual(myIdentityStore.identity) ?? false)
            && conversationContact == nil
    }

    /// Returns true, if I'm in the group
    @objc public var isSelfMember: Bool {
        isOwnGroup ||
            (
                (conversationGroupMyIdentity?.elementsEqual(myIdentityStore.identity) ?? false)
                    && state == .active
            )
    }
    
    /// Returns true, if me is group creator and group is active/editable.
    public var isOwnGroup: Bool {
        isSelfCreator && state == .active
    }
    
    public var attributedDisplayName: NSAttributedString {
        let attributeString = NSMutableAttributedString(string: conversation.displayName)
        attributeString.addAttribute(
            .foregroundColor,
            value: UIColor.label,
            range: NSMakeRange(0, attributeString.length)
        )
        
        guard isSelfMember else {
            attributeString.addAttribute(
                .strikethroughStyle,
                value: 2,
                range: NSMakeRange(0, attributeString.length)
            )
            
            return attributeString
        }
        
        return attributeString
    }
    
    public var canLeave: Bool {
        isSelfMember && !isOwnGroup
    }

    public var canDissolve: Bool {
        isOwnGroup
    }
    
    /// Has the creator left the group?
    public var didCreatorLeave: Bool {
        !isMember(identity: groupCreatorIdentity)
    }
    
    /// Only `true` if you left the group. `false` otherwise.
    /// This includes if you were removed from the group by the creator.
    ///
    /// Is that really what you want? Probably you're looking for `isSelfMember`.
    @objc public var didLeave: Bool {
        state == .left
    }

    var didForcedLeave: Bool {
        state == .forcedLeft
    }
    
    @objc public var groupID: Data {
        groupIdentity.id
    }
    
    @objc public var groupCreatorIdentity: String {
        if let identity = conversationContact?.identity.rawValue {
            return identity
        }
        return myIdentityStore.identity
    }

    /// Creator of the group. It might not be part of the group anymore.
    public var creator: Creator {
        if conversationGroupMyIdentity?.elementsEqual(myIdentityStore.identity) ?? false,
           conversationContact == nil {
            .me(myIdentityStore.identity)
        }
        else if let contact = conversationContact {
            .contact(contact)
        }
        else {
            .unknown
        }
    }
    
    /// Number of members including me
    public var numberOfMembers: Int {
        var membersCount = members.count
        
        if state == .active {
            membersCount += 1
        }
        
        return membersCount
    }
    
    public var membersTitleSummary: String {
        if numberOfMembers == 1 {
            Group.oneMemberTitleString
        }
        else {
            String.localizedStringWithFormat(
                Group.multipleMemberTitleString,
                numberOfMembers
            )
        }
    }
    
    @objc public var isNoteGroup: Bool {
        allMemberIdentities.count == 1 && isSelfMember
    }

    public private(set) var conversationCategory: ConversationEntity.Category
    
    public private(set) var conversationVisibility: ConversationEntity.Visibility
    
    /// A string with all members as comma separated list
    ///
    /// The order is the same as produced by `sortedMembers`
    public private(set) var membersList = ""
    
    /// Sorted list of group members
    public private(set) var sortedMembers = [Member]()

    /// All group member identities including me
    public var allMemberIdentities: Set<String> {
        var identities = Set(members.map(\.identity.rawValue))
        if state == .active {
            identities.insert(myIdentityStore.identity)
        }
        return identities
    }
    
    /// All members that are active in this group
    ///
    /// This is useful to get all members that should receive a group message
    var allActiveMemberIdentitiesWithoutCreator: [String] {
        members
            .filter { $0.state != .invalid }
            .map(\.identity.rawValue)
    }
    
    public private(set) var usesNonGeneratedProfilePicture = false

    private(set) var lastPeriodicSync: Date?

    // MARK: - Private properties

    private let myIdentityStore: MyIdentityStoreProtocol
    private let userSettings: UserSettingsProtocol
    private let pushSettingManager: PushSettingManagerProtocol

    private var conversationGroupMyIdentity: String?

    /// `nil` if we the creator of this group
    private var conversationContact: Contact?
    
    private var groupImageData: Data? {
        didSet {
            updateProfilePicture()
        }
    }

    private lazy var idColor: UIColor = {
        let creatorIDData = Data(groupIdentity.creator.rawValue.utf8)
        let combinedID = creatorIDData + groupIdentity.id
        return IDColor.forData(combinedID)
    }()

    /// True if group is created by a gateway ID
    private var isGatewayGroup: Bool {
        groupCreatorIdentity.hasPrefix("*")
    }
    
    /// Can I add new members
    private var canAddMembers: Bool {
        let maxGroupMembers: Int = Group.maxGroupMembers
        
        return isOwnGroup && members.count < maxGroupMembers
    }
    
    private var didSyncRequest: Bool {
        lastSyncRequest != nil
    }
    
    private var groupCreatorNickname: String? {
        if let nickname = conversationContact?.publicNickname {
            return nickname
        }
        return myIdentityStore.pushFromName
    }
    
    // MARK: - Lifecycle
    
    /// Initialize Group properties, subscribe GroupEntity and Conversation on EntityObserver for updates.
    /// Note: Group properties will be only refreshed if it's ContactEntity and Conversation object already saved in
    /// Core Data.
    ///
    /// - Parameters:
    ///   - myIdentityStore: MyIdentityStore
    ///   - userSettings: UserSettings
    ///   - groupEntity: Core Data object
    ///   - conversation: Core Data object
    ///   - lastSyncRequest: From Core Data object `LastGroupSyncRequest.lastSyncRequest` (TODO: should be Core Data
    /// itself and subscribe on EntityObserver too)
    init(
        myIdentityStore: MyIdentityStoreProtocol,
        userSettings: UserSettingsProtocol,
        pushSettingManager: PushSettingManagerProtocol,
        groupEntity: GroupEntity,
        conversation: ConversationEntity,
        lastSyncRequest: Date?
    ) {
        if let conversationContact = conversation.contact {
            assert(conversationContact.identity == groupEntity.groupCreator)
        }
        else {
            assert(groupEntity.groupCreator == nil)
        }

        self.myIdentityStore = myIdentityStore
        self.userSettings = userSettings
        self.pushSettingManager = pushSettingManager
        self.conversation = conversation
        self.conversationObjectID = conversation.objectID
        self.conversationGroupMyIdentity = conversation.groupMyIdentity
        if let contactEntity = conversation.contact {
            self.conversationContact = Contact(contactEntity: contactEntity)
        }
        self.groupIdentity = GroupIdentity(
            id: groupEntity.groupID,
            creator: ThreemaIdentity(groupEntity.groupCreator ?? myIdentityStore.identity)
        )
        self.state = GroupEntity.GroupState(rawValue: groupEntity.state.intValue)!
        self.name = conversation.groupName
        
        self.old_ProfilePicture = conversation.groupImage?.data
        self.groupImageData = conversation.groupImage?.data
        self.lastSyncRequest = lastSyncRequest
        self.lastUpdate = conversation.lastUpdate
        self.lastMessageDate = conversation.lastMessage?.date
        self.conversationCategory = conversation.conversationCategory
        self.conversationVisibility = conversation.conversationVisibility
        self.lastPeriodicSync = groupEntity.lastPeriodicSync

        self.members = Set(conversation.unwrappedMembers.map { Contact(contactEntity: $0) })

        super.init()
        
        self.sortedMembers = allSortedMembers()
        self.membersList = sortedMembers.map(\.shortDisplayName)
            .joined(separator: ", ")
        
        // Subscribe group entity for DB updates or deletion
        subscribeForGroupEntityChanges(groupEntity: groupEntity)

        // Subscribe conversation entity for DB updates or deletion
        subscribeForConversationEntityChanges(conversationEntity: conversation)
    }

    // MARK: - Public functions

    @objc func isMember(identity: String) -> Bool {
        allMemberIdentities.contains(identity)
    }
    
    /// Checks if at least one member of the group supports the given mask
    /// - Parameter mask: `Common_CspFeatureMaskFlag` to check members for
    /// - Returns: `true` if at least one member supports the given `mask`, `false` otherwise
    public func hasAtLeastOneMemberSupporting(_ mask: ThreemaProtocols.Common_CspFeatureMaskFlag) -> Bool {
        !membersSupporting(mask).isEmpty
    }
    
    /// Checks if *all* member of the group support the given mask
    /// - Parameter mask: `Common_CspFeatureMaskFlag` to check members for
    /// - Returns: `true` all member supports the given `mask`, `false` otherwise
    public func allMembersSupport(_ mask: ThreemaProtocols.Common_CspFeatureMaskFlag) -> Bool {
        membersSupporting(mask).count == members.count
    }
    
    public func generatedProfilePicture() -> UIImage {
        ProfilePictureGenerator.generateImage(for: isNoteGroup ? .noteGroup : .group, color: idColor)
    }
    
    // MARK: - Private functions
    
    private func subscribeForGroupEntityChanges(groupEntity: GroupEntity) {
        subscriptionTokens.append(
            EntityObserver.shared.subscribe(
                managedObject: groupEntity
            ) { [weak self] managedObject, reason in
               
                guard let self, let groupEntity = managedObject as? GroupEntity else {
                    DDLogError("Wrong type, should be GroupEntity")
                    return
                }

                switch reason {
                case .deleted:
                    if !willBeDeleted {
                        willBeDeleted = true
                    }
                    
                case .updated:
                    guard groupIdentity == GroupIdentity(
                        id: groupEntity.groupID,
                        creator: ThreemaIdentity(groupEntity.groupCreator ?? myIdentityStore.identity)
                    ) else {
                        DDLogError("Group identity mismatch")
                        return
                    }

                    if let newState = GroupEntity.GroupState(rawValue: groupEntity.state.intValue) {
                        if state != newState {
                            state = newState
                        }
                    }
                    else {
                        DDLogError("Unknown group state")
                    }
                    if lastPeriodicSync != groupEntity.lastPeriodicSync {
                        lastPeriodicSync = groupEntity.lastPeriodicSync
                    }
                }
            }
        )
    }
    
    private func subscribeForConversationEntityChanges(conversationEntity: ConversationEntity) {
        subscriptionTokens.append(
            EntityObserver.shared.subscribe(
                managedObject: conversation
            ) { [weak self] managedObject, reason in
                guard let self, let conversation = managedObject as? ConversationEntity else {
                    DDLogError("Wrong type, should be Conversation")
                    return
                }

                switch reason {
                case .deleted:
                    if !willBeDeleted {
                        willBeDeleted = true
                    }
                case .updated:
                    guard conversation.isGroup, groupID == conversation.groupID else {
                        DDLogError("Group ID mismatch")
                        return
                    }

                    if conversationGroupMyIdentity != conversation.groupMyIdentity {
                        conversationGroupMyIdentity = conversation.groupMyIdentity
                    }
                    if let contactEntity = conversation.contact {
                        let newConversationContact = Contact(contactEntity: contactEntity)
                        if conversationContact != newConversationContact {
                            conversationContact = newConversationContact
                        }
                    }
                    else if conversationContact != nil {
                        conversationContact = nil
                    }
                    if name != conversation.groupName {
                        name = conversation.groupName
                    }
                    if old_ProfilePicture != conversation.groupImage?.data {
                        old_ProfilePicture = conversation.groupImage?.data
                    }
                    if groupImageData != conversation.groupImage?.data {
                        groupImageData = conversation.groupImage?.data
                    }
                    if lastUpdate != conversation.lastUpdate {
                        lastUpdate = conversation.lastUpdate
                    }
                    if lastMessageDate != conversation.lastMessage?.date {
                        lastMessageDate = conversation.lastMessage?.date
                    }
                    if conversationCategory != conversation.conversationCategory {
                        conversationCategory = conversation.conversationCategory
                    }
                    if conversationVisibility != conversation.conversationVisibility {
                        conversationVisibility = conversation.conversationVisibility
                    }

                    // Check has members composition changed
                    let newMembers = Set(conversation.unwrappedMembers.map { Contact(contactEntity: $0) })

                    if !members.contactIdentitiesEqual(to: newMembers) {
                        members = newMembers
                        sortedMembers = allSortedMembers()
                        membersList = sortedMembers.map(\.shortDisplayName).joined(separator: ", ")
                    }
                    else if !members.contactNameEqual(to: newMembers) {
                        membersList = sortedMembers.map(\.shortDisplayName).joined(separator: ", ")
                    }
                }
            }
        )
    }

    /// The order is as follows: creator (always), me (if I'm in the group and not the creator), all other members
    /// sorted (w/o creator)
    ///
    /// - Returns: Sorted group members list
    private func allSortedMembers() -> [Member] {
        var allSortedMembers = [Member]()
        
        // Always add creator
        allSortedMembers.append(creator)
        
        // Add everybody else expect the creator
        allSortedMembers.append(
            contentsOf: members.sorted(with: userSettings)
                .map(Member.contact)
                .filter { $0.identity != creator.identity }
        )

        // Add me if I'm a member and not the creator
        if isSelfMember, !isOwnGroup {
            allSortedMembers.append(.me(myIdentityStore.identity))
        }
        
        return allSortedMembers
    }
    
    /// Returns an array of `Contacts` containing the members of this group supporting a given
    /// `Common_CspFeatureMaskFlag`
    /// - Parameter mask: `Common_CspFeatureMaskFlag` to check for
    /// - Returns: `Contacts` supporting the `mask`
    private func membersSupporting(_ mask: ThreemaProtocols.Common_CspFeatureMaskFlag) -> [Contact] {
        var supportingMembers = [Contact]()
        for member in members {
            if FeatureMask.check(contact: member, for: mask) {
                supportingMembers.append(member)
            }
        }
        return supportingMembers
    }
    
    private func updateProfilePicture() {
        profilePicture = resolveProfilePicture()
    }
    
    private func resolveProfilePicture() -> UIImage {
        if let groupImageData, let image = UIImage(data: groupImageData) {
            usesNonGeneratedProfilePicture = true
            return image
        }
        
        usesNonGeneratedProfilePicture = false
        return ProfilePictureGenerator.generateImage(for: isNoteGroup ? .noteGroup : .group, color: idColor)
    }

    // MARK: Comparing function

    public func isEqual(to object: Any?) -> Bool {
        guard let object = object as? Group else {
            return false
        }

        return willBeDeleted == object.willBeDeleted &&
            groupIdentity == object.groupIdentity &&
            state == object.state &&
            lastPeriodicSync == object.lastPeriodicSync &&
            conversationGroupMyIdentity == object.conversationGroupMyIdentity &&
            conversationContact == object.conversationContact &&
            name == object.name &&
            old_ProfilePicture == object.old_ProfilePicture &&
            profilePicture.pngData() == object.profilePicture.pngData() &&
            groupImageData == object.groupImageData &&
            lastUpdate == object.lastUpdate &&
            lastMessageDate == object.lastMessageDate &&
            conversationCategory == object.conversationCategory &&
            conversationVisibility == object.conversationVisibility &&
            members.contactsEqual(to: object.members)
    }
}

Messung
CHZG
869510093

¤ Dauer der Verarbeitung: 0.2 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© 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.