Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Threads/TSPrivateStoryThread.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import GRDB

/// Represents a story distribution list.
public final class TSPrivateStoryThread: TSThread {
    override public class var recordType: SDSRecordType { .privateStoryThread }

    public private(set) var allowsReplies: Bool
    public private(set) var _name: String

    public enum CodingKeys: String, CodingKey, ColumnExpression {
        case allowsReplies
        case name
        case addresses
    }

    required init(inheritableDecoder decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.allowsReplies = try container.decode(Bool.self, forKey: .allowsReplies)
        self._name = try container.decode(String.self, forKey: .name)
        try super.init(inheritableDecoder: decoder)
    }

    override public func encode(to encoder: any Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.allowsReplies, forKey: .allowsReplies)
        try container.encode(self._name, forKey: .name)
        try container.encode(nil as Data?, forKey: .addresses)
    }

    init(
        id: Int64?,
        uniqueId: String,
        creationDate: Date?,
        editTargetTimestamp: UInt64?,
        isArchivedObsolete: Bool,
        isMarkedUnreadObsolete: Bool,
        lastDraftInteractionRowId: UInt64,
        lastDraftUpdateTimestamp: UInt64,
        lastInteractionRowId: UInt64,
        lastSentStoryTimestamp: UInt64?,
        mentionNotificationMode: TSThreadMentionNotificationMode,
        messageDraft: String?,
        messageDraftBodyRanges: MessageBodyRanges?,
        mutedUntilTimestampObsolete: UInt64,
        shouldThreadBeVisible: Bool,
        storyViewMode: TSThreadStoryViewMode,
        allowsReplies: Bool,
        name: String,
    ) {
        self.allowsReplies = allowsReplies
        self._name = name
        super.init(
            id: id,
            uniqueId: uniqueId,
            creationDate: creationDate,
            editTargetTimestamp: editTargetTimestamp,
            isArchivedObsolete: isArchivedObsolete,
            isMarkedUnreadObsolete: isMarkedUnreadObsolete,
            lastDraftInteractionRowId: lastDraftInteractionRowId,
            lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
            lastInteractionRowId: lastInteractionRowId,
            lastSentStoryTimestamp: lastSentStoryTimestamp,
            mentionNotificationMode: mentionNotificationMode,
            messageDraft: messageDraft,
            messageDraftBodyRanges: messageDraftBodyRanges,
            mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
            shouldThreadBeVisible: shouldThreadBeVisible,
            storyViewMode: storyViewMode,
        )
    }

    public init(uniqueId: String = UUID().uuidString, name: String, allowsReplies: Bool, viewMode: TSThreadStoryViewMode) {
        self._name = name
        self.allowsReplies = allowsReplies
        super.init(uniqueId: uniqueId)
        self.storyViewMode = viewMode
    }

    override func deepCopy() -> TSThread {
        return TSPrivateStoryThread(
            id: self.id,
            uniqueId: self.uniqueId,
            creationDate: self.creationDate,
            editTargetTimestamp: self.editTargetTimestamp,
            isArchivedObsolete: self.isArchivedObsolete,
            isMarkedUnreadObsolete: self.isMarkedUnreadObsolete,
            lastDraftInteractionRowId: self.lastDraftInteractionRowId,
            lastDraftUpdateTimestamp: self.lastDraftUpdateTimestamp,
            lastInteractionRowId: self.lastInteractionRowId,
            lastSentStoryTimestamp: self.lastSentStoryTimestamp,
            mentionNotificationMode: self.mentionNotificationMode,
            messageDraft: self.messageDraft,
            messageDraftBodyRanges: self.messageDraftBodyRanges,
            mutedUntilTimestampObsolete: self.mutedUntilTimestampObsolete,
            shouldThreadBeVisible: self.shouldThreadBeVisible,
            storyViewMode: self.storyViewMode,
            allowsReplies: self.allowsReplies,
            name: self.name,
        )
    }

    override public var hash: Int {
        var hasher = Hasher()
        hasher.combine(super.hash)
        hasher.combine(self.allowsReplies)
        hasher.combine(self.name)
        return hasher.finalize()
    }

    override public func isEqual(_ object: Any?) -> Bool {
        guard let object = object as? Self else { return false }
        guard super.isEqual(object) else { return false }
        guard self.allowsReplies == object.allowsReplies else { return false }
        guard self.name == object.name else { return false }
        return true
    }

    public class func fetchPrivateStoryThreadViaCache(uniqueId: String, transaction: DBReadTransaction) -> TSPrivateStoryThread? {
        return fetchViaCache(uniqueId: uniqueId, transaction: transaction)
    }

    public var isMyStory: Bool {
        return self.uniqueId == Self.myStoryUniqueId
    }

    public var name: String {
        if self.isMyStory {
            return OWSLocalizedString("MY_STORY_NAME", comment: "Name for the 'My Story' default story that sends to all the user's contacts.")
        }
        return _name
    }

    public typealias RowId = Int64

    @objc
    public class var myStoryUniqueId: String {
        // My Story always uses a UUID of all 0s
        "00000000-0000-0000-0000-000000000000"
    }

    public class func getMyStory(transaction: DBReadTransaction) -> TSPrivateStoryThread! {
        fetchPrivateStoryThreadViaCache(uniqueId: myStoryUniqueId, transaction: transaction)
    }

    @discardableResult
    public class func getOrCreateMyStory(transaction: DBWriteTransaction) -> TSPrivateStoryThread {
        if let myStory = getMyStory(transaction: transaction) { return myStory }

        let myStory = TSPrivateStoryThread(uniqueId: myStoryUniqueId, name: "", allowsReplies: true, viewMode: .blockList)
        myStory.anyInsert(transaction: transaction)
        return myStory
    }

    // MARK: -

    @objc
    public var distributionListIdentifier: Data? { UUID(uuidString: uniqueId)?.data }

    override public func recipientAddresses(with tx: DBReadTransaction) -> [SignalServiceAddress] {
        let storyRecipientManager = DependenciesBridge.shared.storyRecipientManager
        do {
            switch storyViewMode {
            case .default:
                throw OWSAssertionError("Unexpectedly have private story with no view mode")
            case .explicit, .disabled:
                return try storyRecipientManager.fetchRecipients(forStoryThread: self, tx: tx).map { $0.address }
            case .blockList:
                let blockedAddresses = try storyRecipientManager.fetchRecipients(forStoryThread: self, tx: tx).map { $0.address }
                let profileManager = SSKEnvironment.shared.profileManagerRef
                return profileManager.allWhitelistedRegisteredAddresses(tx: tx).filter {
                    return !blockedAddresses.contains($0) && !$0.isLocalAddress
                }
            }
        } catch {
            Logger.warn("Couldn't fetch addresses; returning []: \(error)")
            return []
        }
    }

    // MARK: - updateWith...

    public func updateWithAllowsReplies(
        _ allowsReplies: Bool,
        updateStorageService: Bool,
        transaction tx: DBWriteTransaction,
    ) {
        anyUpdate(transaction: tx) { privateStoryThread in
            privateStoryThread.allowsReplies = allowsReplies
        }

        if updateStorageService, let distributionListIdentifier {
            SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(
                updatedStoryDistributionListIds: [distributionListIdentifier],
            )
        }
    }

    public func updateWithName(
        _ name: String,
        updateStorageService: Bool,
        transaction tx: DBWriteTransaction,
    ) {
        anyUpdate(transaction: tx) { privateStoryThread in
            privateStoryThread._name = name
        }

        if updateStorageService, let distributionListIdentifier {
            SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(
                updatedStoryDistributionListIds: [distributionListIdentifier],
            )
        }
    }

    /// Update this private story thread with the given view mode and
    /// corresponding addresses.
    ///
    /// - Parameter updateStorageService
    /// Whether or not we should update the distribution list this thread
    /// represents in Storage Service.
    /// - Parameter updateHasSetMyStoryPrivacyIfNeeded
    /// Whether or not we should set the local "has set My Story privacy" flag
    /// (to `true`), assuming this thread represents "My Story". Only callers
    /// who will be managing that flag's state themselves – at the time of
    /// writing, that is exclusively Backups – should set this to `false`.
    public func updateWithStoryViewMode(
        _ storyViewMode: TSThreadStoryViewMode,
        storyRecipientIds storyRecipientIdsChange: OptionalChange<[SignalRecipient.RowId]>,
        updateStorageService: Bool,
        updateHasSetMyStoryPrivacyIfNeeded: Bool = true,
        transaction tx: DBWriteTransaction,
    ) {
        if updateHasSetMyStoryPrivacyIfNeeded, isMyStory {
            StoryManager.setHasSetMyStoriesPrivacy(
                true,
                shouldUpdateStorageService: updateStorageService,
                transaction: tx,
            )
        }

        anyUpdate(transaction: tx) { privateStoryThread in
            privateStoryThread.storyViewMode = storyViewMode
        }

        switch storyRecipientIdsChange {
        case .noChange:
            break
        case .setTo(let storyRecipientIds):
            let storyRecipientManager = DependenciesBridge.shared.storyRecipientManager
            failIfThrows {
                try storyRecipientManager.setRecipientIds(
                    storyRecipientIds,
                    for: self,
                    shouldUpdateStorageService: false, // handled below
                    tx: tx,
                )
            }
        }

        if updateStorageService, let distributionListIdentifier {
            tx.addSyncCompletion {
                let storageServiceManager = SSKEnvironment.shared.storageServiceManagerRef
                storageServiceManager.recordPendingUpdates(updatedStoryDistributionListIds: [distributionListIdentifier])
            }
        }
    }
}