251 lines
9.8 KiB
Swift
251 lines
9.8 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import GRDB
|
|
import LibSignalClient
|
|
|
|
open class TSContactThread: TSThread {
|
|
override public class var recordType: SDSRecordType { .contactThread }
|
|
|
|
/// Represents the uppercase ServiceId string for this contact.
|
|
/// - Note
|
|
/// This property name includes `UUID` for compatibility with SDS (to match the
|
|
/// SQLite column), but **may not contain a valid UUID string**.
|
|
public internal(set) var contactUUID: String?
|
|
public internal(set) var contactPhoneNumber: String?
|
|
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
case contactPhoneNumber
|
|
case contactUUID
|
|
case hasDismissedOffers
|
|
}
|
|
|
|
public required init(inheritableDecoder decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.contactUUID = try container.decodeIfPresent(String.self, forKey: .contactUUID)
|
|
self.contactPhoneNumber = try container.decodeIfPresent(String.self, forKey: .contactPhoneNumber)
|
|
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.contactUUID, forKey: .contactUUID)
|
|
try container.encode(self.contactPhoneNumber, forKey: .contactPhoneNumber)
|
|
try container.encode(false, forKey: .hasDismissedOffers)
|
|
}
|
|
|
|
override public var hash: Int {
|
|
var hasher = Hasher()
|
|
hasher.combine(super.hash)
|
|
hasher.combine(self.contactPhoneNumber)
|
|
hasher.combine(self.contactUUID)
|
|
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.contactPhoneNumber == object.contactPhoneNumber else { return false }
|
|
guard self.contactUUID == object.contactUUID else { return false }
|
|
return true
|
|
}
|
|
|
|
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,
|
|
contactUUID: String?,
|
|
contactPhoneNumber: String?,
|
|
) {
|
|
self.contactUUID = contactUUID
|
|
self.contactPhoneNumber = contactPhoneNumber
|
|
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,
|
|
contactUUID: String?,
|
|
contactPhoneNumber: String?,
|
|
) {
|
|
self.contactUUID = contactUUID
|
|
self.contactPhoneNumber = contactPhoneNumber
|
|
super.init(uniqueId: uniqueId)
|
|
}
|
|
|
|
override func deepCopy() -> TSThread {
|
|
return TSContactThread(
|
|
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,
|
|
contactUUID: self.contactUUID,
|
|
contactPhoneNumber: self.contactPhoneNumber,
|
|
)
|
|
}
|
|
|
|
class func fetchContactThreadViaCache(uniqueId: String, transaction: DBReadTransaction) -> TSContactThread? {
|
|
return fetchViaCache(uniqueId: uniqueId, transaction: transaction)
|
|
}
|
|
|
|
public var contactAddress: SignalServiceAddress {
|
|
return SignalServiceAddress(serviceIdString: self.contactUUID, phoneNumber: self.contactPhoneNumber)
|
|
}
|
|
|
|
override public func recipientAddresses(with tx: DBReadTransaction) -> [SignalServiceAddress] {
|
|
return [self.contactAddress]
|
|
}
|
|
|
|
override public var isNoteToSelf: Bool { self.contactAddress.isLocalAddress }
|
|
|
|
override public func hasSafetyNumbers() -> Bool {
|
|
return OWSIdentityManagerObjCBridge.identityKey(forAddress: self.contactAddress) != nil
|
|
}
|
|
|
|
static func contactAddress(fromThreadId threadUniqueId: String, transaction tx: DBReadTransaction) -> SignalServiceAddress? {
|
|
return (TSThread.fetchViaCache(uniqueId: threadUniqueId, transaction: tx) as? TSContactThread)?.contactAddress
|
|
}
|
|
|
|
override public func anyDidInsert(transaction: DBWriteTransaction) {
|
|
super.anyDidInsert(transaction: transaction)
|
|
Logger.info("Inserted contact thread: \(self.contactAddress)")
|
|
}
|
|
|
|
@objc
|
|
public convenience init(contactAddress: SignalServiceAddress) {
|
|
let normalizedAddress = NormalizedDatabaseRecordAddress(address: contactAddress)
|
|
owsAssertDebug(normalizedAddress != nil)
|
|
self.init(
|
|
contactUUID: normalizedAddress?.serviceId?.serviceIdUppercaseString,
|
|
contactPhoneNumber: normalizedAddress?.phoneNumber,
|
|
)
|
|
}
|
|
|
|
@objc
|
|
public static func getOrCreateLocalThread(transaction: DBWriteTransaction) -> TSContactThread? {
|
|
guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction)?.aciAddress else {
|
|
owsFailDebug("Missing localAddress.")
|
|
return nil
|
|
}
|
|
return TSContactThread.getOrCreateThread(withContactAddress: localAddress, transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public static func getOrCreateLocalThreadWithSneakyTransaction() -> TSContactThread? {
|
|
assert(!Thread.isMainThread)
|
|
|
|
let thread: TSContactThread? = SSKEnvironment.shared.databaseStorageRef.read { tx in
|
|
guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx)?.aciAddress else {
|
|
owsFailDebug("Missing localAddress.")
|
|
return nil
|
|
}
|
|
return TSContactThread.getWithContactAddress(localAddress, transaction: tx)
|
|
}
|
|
if let thread {
|
|
return thread
|
|
}
|
|
|
|
return SSKEnvironment.shared.databaseStorageRef.write { transaction in
|
|
return getOrCreateLocalThread(transaction: transaction)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public static func getOrCreateThread(
|
|
withContactAddress contactAddress: SignalServiceAddress,
|
|
transaction: DBWriteTransaction,
|
|
) -> TSContactThread {
|
|
owsAssertDebug(contactAddress.isValid)
|
|
|
|
let existingThread = ContactThreadFinder().contactThread(for: contactAddress, tx: transaction)
|
|
if let existingThread {
|
|
return existingThread
|
|
}
|
|
|
|
let insertedThread = TSContactThread(contactAddress: contactAddress)
|
|
insertedThread.anyInsert(transaction: transaction)
|
|
return insertedThread
|
|
}
|
|
|
|
public static func getOrCreateThread(contactAddress: SignalServiceAddress) -> TSContactThread {
|
|
owsAssertDebug(contactAddress.isValid)
|
|
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
|
|
|
let existingThread = databaseStorage.read { tx in
|
|
return ContactThreadFinder().contactThread(for: contactAddress, tx: tx)
|
|
}
|
|
if let existingThread {
|
|
return existingThread
|
|
}
|
|
|
|
return databaseStorage.write { tx in
|
|
return self.getOrCreateThread(withContactAddress: contactAddress, transaction: tx)
|
|
}
|
|
}
|
|
|
|
// Unlike getOrCreateThreadWithContactAddress, this will _NOT_ create a thread if one does not already exist.
|
|
@objc
|
|
public static func getWithContactAddress(
|
|
_ contactAddress: SignalServiceAddress,
|
|
transaction: DBReadTransaction,
|
|
) -> TSContactThread? {
|
|
return ContactThreadFinder().contactThread(for: contactAddress, tx: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: - StringInterpolation
|
|
|
|
public extension String.StringInterpolation {
|
|
mutating func appendInterpolation(contactThreadColumn column: TSContactThread.CodingKeys) {
|
|
appendLiteral(column.rawValue)
|
|
}
|
|
|
|
mutating func appendInterpolation(contactThreadColumnFullyQualified column: TSContactThread.CodingKeys) {
|
|
appendLiteral("\(TSThread.databaseTableName).\(column.rawValue)")
|
|
}
|
|
}
|