Signal-iOS/SignalServiceKit/Threads/TSPrivateStoryThread.swift
2026-06-01 12:42:17 -07:00

274 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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: TSThreadType { .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])
}
}
}
}