// // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import GRDB import SignalCoreKit // NOTE: This file is generated by /Scripts/sds_codegen/sds_generate.py. // Do not manually edit it, instead run `sds_codegen.sh`. // MARK: - Record public struct ThreadRecord: SDSRecord { public weak var delegate: SDSRecordDelegate? public var tableMetadata: SDSTableMetadata { TSThreadSerializer.table } public static var databaseTableName: String { TSThreadSerializer.table.tableName } public var id: Int64? // This defines all of the columns used in the table // where this model (and any subclasses) are persisted. public let recordType: SDSRecordType public let uniqueId: String // Properties public let conversationColorName: String public let creationDate: Double? public let isArchived: Bool public let lastInteractionRowId: UInt64 public let messageDraft: String? public let mutedUntilDate: Double? public let shouldThreadBeVisible: Bool public let contactPhoneNumber: String? public let contactUUID: String? public let groupModel: Data? public let hasDismissedOffers: Bool? public let isMarkedUnread: Bool public let lastVisibleSortIdOnScreenPercentage: Double public let lastVisibleSortId: UInt64 public let messageDraftBodyRanges: Data? public let mentionNotificationMode: UInt public let mutedUntilTimestamp: UInt64 public let allowsReplies: Bool? public let lastSentStoryTimestamp: UInt64? public let name: String? public let addresses: Data? public let storyViewMode: UInt public let editTargetTimestamp: UInt64? public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { case id case recordType case uniqueId case conversationColorName case creationDate case isArchived case lastInteractionRowId case messageDraft case mutedUntilDate case shouldThreadBeVisible case contactPhoneNumber case contactUUID case groupModel case hasDismissedOffers case isMarkedUnread case lastVisibleSortIdOnScreenPercentage case lastVisibleSortId case messageDraftBodyRanges case mentionNotificationMode case mutedUntilTimestamp case allowsReplies case lastSentStoryTimestamp case name case addresses case storyViewMode case editTargetTimestamp } public static func columnName(_ column: ThreadRecord.CodingKeys, fullyQualified: Bool = false) -> String { fullyQualified ? "\(databaseTableName).\(column.rawValue)" : column.rawValue } public func didInsert(with rowID: Int64, for column: String?) { guard let delegate = delegate else { owsFailDebug("Missing delegate.") return } delegate.updateRowId(rowID) } } // MARK: - Row Initializer public extension ThreadRecord { static var databaseSelection: [SQLSelectable] { CodingKeys.allCases } init(row: Row) { id = row[0] recordType = row[1] uniqueId = row[2] conversationColorName = row[3] creationDate = row[4] isArchived = row[5] lastInteractionRowId = row[6] messageDraft = row[7] mutedUntilDate = row[8] shouldThreadBeVisible = row[9] contactPhoneNumber = row[10] contactUUID = row[11] groupModel = row[12] hasDismissedOffers = row[13] isMarkedUnread = row[14] lastVisibleSortIdOnScreenPercentage = row[15] lastVisibleSortId = row[16] messageDraftBodyRanges = row[17] mentionNotificationMode = row[18] mutedUntilTimestamp = row[19] allowsReplies = row[20] lastSentStoryTimestamp = row[21] name = row[22] addresses = row[23] storyViewMode = row[24] editTargetTimestamp = row[25] } } // MARK: - StringInterpolation public extension String.StringInterpolation { mutating func appendInterpolation(threadColumn column: ThreadRecord.CodingKeys) { appendLiteral(ThreadRecord.columnName(column)) } mutating func appendInterpolation(threadColumnFullyQualified column: ThreadRecord.CodingKeys) { appendLiteral(ThreadRecord.columnName(column, fullyQualified: true)) } } // MARK: - Deserialization extension TSThread { // This method defines how to deserialize a model, given a // database row. The recordType column is used to determine // the corresponding model class. class func fromRecord(_ record: ThreadRecord) throws -> TSThread { guard let recordId = record.id else { throw SDSError.invalidValue() } switch record.recordType { case .contactThread: let uniqueId: String = record.uniqueId let conversationColorNameObsolete: String = record.conversationColorName let creationDateInterval: Double? = record.creationDate let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate") let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) }) let isArchivedObsolete: Bool = record.isArchived let isMarkedUnreadObsolete: Bool = record.isMarkedUnread let lastInteractionRowId: UInt64 = record.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) }) let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default let messageDraft: String? = record.messageDraft let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges") let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete") let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default let contactPhoneNumber: String? = record.contactPhoneNumber let contactUUID: String? = record.contactUUID let hasDismissedOffers: Bool = try SDSDeserialization.required(record.hasDismissedOffers, name: "hasDismissedOffers") return TSContactThread(grdbId: recordId, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode, contactPhoneNumber: contactPhoneNumber, contactUUID: contactUUID, hasDismissedOffers: hasDismissedOffers) case .groupThread: let uniqueId: String = record.uniqueId let conversationColorNameObsolete: String = record.conversationColorName let creationDateInterval: Double? = record.creationDate let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate") let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) }) let isArchivedObsolete: Bool = record.isArchived let isMarkedUnreadObsolete: Bool = record.isMarkedUnread let lastInteractionRowId: UInt64 = record.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) }) let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default let messageDraft: String? = record.messageDraft let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges") let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete") let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default let groupModelSerialized: Data? = record.groupModel let groupModel: TSGroupModel = try SDSDeserialization.unarchive(groupModelSerialized, name: "groupModel") return TSGroupThread(grdbId: recordId, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode, groupModel: groupModel) case .privateStoryThread: let uniqueId: String = record.uniqueId let conversationColorNameObsolete: String = record.conversationColorName let creationDateInterval: Double? = record.creationDate let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate") let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) }) let isArchivedObsolete: Bool = record.isArchived let isMarkedUnreadObsolete: Bool = record.isMarkedUnread let lastInteractionRowId: UInt64 = record.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) }) let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default let messageDraft: String? = record.messageDraft let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges") let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete") let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default let addressesSerialized: Data? = record.addresses let addresses: [SignalServiceAddress] = try SDSDeserialization.unarchive(addressesSerialized, name: "addresses") let allowsReplies: Bool = try SDSDeserialization.required(record.allowsReplies, name: "allowsReplies") let name: String = try SDSDeserialization.required(record.name, name: "name") return TSPrivateStoryThread(grdbId: recordId, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode, addresses: addresses, allowsReplies: allowsReplies, name: name) case .thread: let uniqueId: String = record.uniqueId let conversationColorNameObsolete: String = record.conversationColorName let creationDateInterval: Double? = record.creationDate let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate") let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) }) let isArchivedObsolete: Bool = record.isArchived let isMarkedUnreadObsolete: Bool = record.isMarkedUnread let lastInteractionRowId: UInt64 = record.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) }) let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default let messageDraft: String? = record.messageDraft let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges") let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete") let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default return TSThread(grdbId: recordId, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode) default: owsFailDebug("Unexpected record type: \(record.recordType)") throw SDSError.invalidValue() } } } // MARK: - SDSModel extension TSThread: SDSModel { public var serializer: SDSSerializer { // Any subclass can be cast to it's superclass, // so the order of this switch statement matters. // We need to do a "depth first" search by type. switch self { case let model as TSPrivateStoryThread: assert(type(of: model) == TSPrivateStoryThread.self) return TSPrivateStoryThreadSerializer(model: model) case let model as TSGroupThread: assert(type(of: model) == TSGroupThread.self) return TSGroupThreadSerializer(model: model) case let model as TSContactThread: assert(type(of: model) == TSContactThread.self) return TSContactThreadSerializer(model: model) default: return TSThreadSerializer(model: self) } } public func asRecord() throws -> SDSRecord { try serializer.asRecord() } public var sdsTableName: String { ThreadRecord.databaseTableName } public static var table: SDSTableMetadata { TSThreadSerializer.table } } // MARK: - DeepCopyable extension TSThread: DeepCopyable { public func deepCopy() throws -> AnyObject { // Any subclass can be cast to it's superclass, // so the order of this switch statement matters. // We need to do a "depth first" search by type. guard let id = self.grdbId?.int64Value else { throw OWSAssertionError("Model missing grdbId.") } if let modelToCopy = self as? TSPrivateStoryThread { assert(type(of: modelToCopy) == TSPrivateStoryThread.self) let uniqueId: String = modelToCopy.uniqueId let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete let creationDate: Date? = modelToCopy.creationDate let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode let messageDraft: String? = modelToCopy.messageDraft // NOTE: If this generates build errors, you made need to // modify DeepCopy.swift to support this type. // // That might mean: // // * Implement DeepCopyable for this type (e.g. a model). // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection). let messageDraftBodyRanges: MessageBodyRanges? if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges { messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy) } else { messageDraftBodyRanges = nil } let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode // NOTE: If this generates build errors, you made need to // implement DeepCopyable for this type in DeepCopy.swift. let addresses: [SignalServiceAddress] = try DeepCopies.deepCopy(modelToCopy.addresses) let allowsReplies: Bool = modelToCopy.allowsReplies let name: String = modelToCopy.name return TSPrivateStoryThread(grdbId: id, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode, addresses: addresses, allowsReplies: allowsReplies, name: name) } if let modelToCopy = self as? TSGroupThread { assert(type(of: modelToCopy) == TSGroupThread.self) let uniqueId: String = modelToCopy.uniqueId let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete let creationDate: Date? = modelToCopy.creationDate let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode let messageDraft: String? = modelToCopy.messageDraft // NOTE: If this generates build errors, you made need to // modify DeepCopy.swift to support this type. // // That might mean: // // * Implement DeepCopyable for this type (e.g. a model). // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection). let messageDraftBodyRanges: MessageBodyRanges? if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges { messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy) } else { messageDraftBodyRanges = nil } let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode // NOTE: If this generates build errors, you made need to // implement DeepCopyable for this type in DeepCopy.swift. let groupModel: TSGroupModel = try DeepCopies.deepCopy(modelToCopy.groupModel) return TSGroupThread(grdbId: id, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode, groupModel: groupModel) } if let modelToCopy = self as? TSContactThread { assert(type(of: modelToCopy) == TSContactThread.self) let uniqueId: String = modelToCopy.uniqueId let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete let creationDate: Date? = modelToCopy.creationDate let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode let messageDraft: String? = modelToCopy.messageDraft // NOTE: If this generates build errors, you made need to // modify DeepCopy.swift to support this type. // // That might mean: // // * Implement DeepCopyable for this type (e.g. a model). // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection). let messageDraftBodyRanges: MessageBodyRanges? if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges { messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy) } else { messageDraftBodyRanges = nil } let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode let contactPhoneNumber: String? = modelToCopy.contactPhoneNumber let contactUUID: String? = modelToCopy.contactUUID let hasDismissedOffers: Bool = modelToCopy.hasDismissedOffers return TSContactThread(grdbId: id, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode, contactPhoneNumber: contactPhoneNumber, contactUUID: contactUUID, hasDismissedOffers: hasDismissedOffers) } do { let modelToCopy = self assert(type(of: modelToCopy) == TSThread.self) let uniqueId: String = modelToCopy.uniqueId let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete let creationDate: Date? = modelToCopy.creationDate let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode let messageDraft: String? = modelToCopy.messageDraft // NOTE: If this generates build errors, you made need to // modify DeepCopy.swift to support this type. // // That might mean: // // * Implement DeepCopyable for this type (e.g. a model). // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection). let messageDraftBodyRanges: MessageBodyRanges? if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges { messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy) } else { messageDraftBodyRanges = nil } let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode return TSThread(grdbId: id, uniqueId: uniqueId, conversationColorNameObsolete: conversationColorNameObsolete, creationDate: creationDate, editTargetTimestamp: editTargetTimestamp, isArchivedObsolete: isArchivedObsolete, isMarkedUnreadObsolete: isMarkedUnreadObsolete, lastInteractionRowId: lastInteractionRowId, lastSentStoryTimestamp: lastSentStoryTimestamp, lastVisibleSortIdObsolete: lastVisibleSortIdObsolete, lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete, mentionNotificationMode: mentionNotificationMode, messageDraft: messageDraft, messageDraftBodyRanges: messageDraftBodyRanges, mutedUntilDateObsolete: mutedUntilDateObsolete, mutedUntilTimestampObsolete: mutedUntilTimestampObsolete, shouldThreadBeVisible: shouldThreadBeVisible, storyViewMode: storyViewMode) } } } // MARK: - Table Metadata extension TSThreadSerializer { // This defines all of the columns used in the table // where this model (and any subclasses) are persisted. static var idColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "id", columnType: .primaryKey) } static var recordTypeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "recordType", columnType: .int64) } static var uniqueIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "uniqueId", columnType: .unicodeString, isUnique: true) } // Properties static var conversationColorNameObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "conversationColorNameObsolete", columnType: .unicodeString) } static var creationDateColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "creationDate", columnType: .double, isOptional: true) } static var isArchivedObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isArchivedObsolete", columnType: .int) } static var lastInteractionRowIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastInteractionRowId", columnType: .int64) } static var messageDraftColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "messageDraft", columnType: .unicodeString, isOptional: true) } static var mutedUntilDateObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "mutedUntilDateObsolete", columnType: .double, isOptional: true) } static var shouldThreadBeVisibleColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "shouldThreadBeVisible", columnType: .int) } static var contactPhoneNumberColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "contactPhoneNumber", columnType: .unicodeString, isOptional: true) } static var contactUUIDColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "contactUUID", columnType: .unicodeString, isOptional: true) } static var groupModelColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "groupModel", columnType: .blob, isOptional: true) } static var hasDismissedOffersColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "hasDismissedOffers", columnType: .int, isOptional: true) } static var isMarkedUnreadObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isMarkedUnreadObsolete", columnType: .int) } static var lastVisibleSortIdOnScreenPercentageObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastVisibleSortIdOnScreenPercentageObsolete", columnType: .double) } static var lastVisibleSortIdObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastVisibleSortIdObsolete", columnType: .int64) } static var messageDraftBodyRangesColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "messageDraftBodyRanges", columnType: .blob, isOptional: true) } static var mentionNotificationModeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "mentionNotificationMode", columnType: .int) } static var mutedUntilTimestampObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "mutedUntilTimestampObsolete", columnType: .int64) } static var allowsRepliesColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "allowsReplies", columnType: .int, isOptional: true) } static var lastSentStoryTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastSentStoryTimestamp", columnType: .int64, isOptional: true) } static var nameColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "name", columnType: .unicodeString, isOptional: true) } static var addressesColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "addresses", columnType: .blob, isOptional: true) } static var storyViewModeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "storyViewMode", columnType: .int) } static var editTargetTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "editTargetTimestamp", columnType: .int64, isOptional: true) } public static var table: SDSTableMetadata { SDSTableMetadata(collection: TSThread.collection(), tableName: "model_TSThread", columns: [ idColumn, recordTypeColumn, uniqueIdColumn, conversationColorNameObsoleteColumn, creationDateColumn, isArchivedObsoleteColumn, lastInteractionRowIdColumn, messageDraftColumn, mutedUntilDateObsoleteColumn, shouldThreadBeVisibleColumn, contactPhoneNumberColumn, contactUUIDColumn, groupModelColumn, hasDismissedOffersColumn, isMarkedUnreadObsoleteColumn, lastVisibleSortIdOnScreenPercentageObsoleteColumn, lastVisibleSortIdObsoleteColumn, messageDraftBodyRangesColumn, mentionNotificationModeColumn, mutedUntilTimestampObsoleteColumn, allowsRepliesColumn, lastSentStoryTimestampColumn, nameColumn, addressesColumn, storyViewModeColumn, editTargetTimestampColumn ]) } } // MARK: - Save/Remove/Update @objc public extension TSThread { func anyInsert(transaction: SDSAnyWriteTransaction) { sdsSave(saveMode: .insert, transaction: transaction) } // Avoid this method whenever feasible. // // If the record has previously been saved, this method does an overwriting // update of the corresponding row, otherwise if it's a new record, this // method inserts a new row. // // For performance, when possible, you should explicitly specify whether // you are inserting or updating rather than calling this method. func anyUpsert(transaction: SDSAnyWriteTransaction) { let isInserting: Bool if TSThread.anyFetch(uniqueId: uniqueId, transaction: transaction) != nil { isInserting = false } else { isInserting = true } sdsSave(saveMode: isInserting ? .insert : .update, transaction: transaction) } // This method is used by "updateWith..." methods. // // This model may be updated from many threads. We don't want to save // our local copy (this instance) since it may be out of date. We also // want to avoid re-saving a model that has been deleted. Therefore, we // use "updateWith..." methods to: // // a) Update a property of this instance. // b) If a copy of this model exists in the database, load an up-to-date copy, // and update and save that copy. // b) If a copy of this model _DOES NOT_ exist in the database, do _NOT_ save // this local instance. // // After "updateWith...": // // a) Any copy of this model in the database will have been updated. // b) The local property on this instance will always have been updated. // c) Other properties on this instance may be out of date. // // All mutable properties of this class have been made read-only to // prevent accidentally modifying them directly. // // This isn't a perfect arrangement, but in practice this will prevent // data loss and will resolve all known issues. func anyUpdate(transaction: SDSAnyWriteTransaction, block: (TSThread) -> Void) { block(self) guard let dbCopy = type(of: self).anyFetch(uniqueId: uniqueId, transaction: transaction) else { return } // Don't apply the block twice to the same instance. // It's at least unnecessary and actually wrong for some blocks. // e.g. `block: { $0 in $0.someField++ }` if dbCopy !== self { block(dbCopy) } dbCopy.sdsSave(saveMode: .update, transaction: transaction) } // This method is an alternative to `anyUpdate(transaction:block:)` methods. // // We should generally use `anyUpdate` to ensure we're not unintentionally // clobbering other columns in the database when another concurrent update // has occurred. // // There are cases when this doesn't make sense, e.g. when we know we've // just loaded the model in the same transaction. In those cases it is // safe and faster to do a "overwriting" update func anyOverwritingUpdate(transaction: SDSAnyWriteTransaction) { sdsSave(saveMode: .update, transaction: transaction) } func anyReload(transaction: SDSAnyReadTransaction) { anyReload(transaction: transaction, ignoreMissing: false) } func anyReload(transaction: SDSAnyReadTransaction, ignoreMissing: Bool) { guard let latestVersion = type(of: self).anyFetch(uniqueId: uniqueId, transaction: transaction) else { if !ignoreMissing { owsFailDebug("`latest` was unexpectedly nil") } return } setValuesForKeys(latestVersion.dictionaryValue) } } // MARK: - TSThreadCursor @objc public class TSThreadCursor: NSObject, SDSCursor { private let transaction: GRDBReadTransaction private let cursor: RecordCursor? init(transaction: GRDBReadTransaction, cursor: RecordCursor?) { self.transaction = transaction self.cursor = cursor } public func next() throws -> TSThread? { guard let cursor = cursor else { return nil } guard let record = try cursor.next() else { return nil } let value = try TSThread.fromRecord(record) Self.modelReadCaches.threadReadCache.didReadThread(value, transaction: transaction.asAnyRead) return value } public func all() throws -> [TSThread] { var result = [TSThread]() while true { guard let model = try next() else { break } result.append(model) } return result } } // MARK: - Obj-C Fetch @objc public extension TSThread { class func grdbFetchCursor(transaction: GRDBReadTransaction) -> TSThreadCursor { let database = transaction.database do { let cursor = try ThreadRecord.fetchCursor(database) return TSThreadCursor(transaction: transaction, cursor: cursor) } catch { DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFailDebug("Read failed: \(error)") return TSThreadCursor(transaction: transaction, cursor: nil) } } // Fetches a single model by "unique id". class func anyFetch(uniqueId: String, transaction: SDSAnyReadTransaction) -> TSThread? { assert(!uniqueId.isEmpty) return anyFetch(uniqueId: uniqueId, transaction: transaction, ignoreCache: false) } // Fetches a single model by "unique id". class func anyFetch(uniqueId: String, transaction: SDSAnyReadTransaction, ignoreCache: Bool) -> TSThread? { assert(!uniqueId.isEmpty) if !ignoreCache, let cachedCopy = Self.modelReadCaches.threadReadCache.getThread(uniqueId: uniqueId, transaction: transaction) { return cachedCopy } switch transaction.readTransaction { case .grdbRead(let grdbTransaction): let sql = "SELECT * FROM \(ThreadRecord.databaseTableName) WHERE \(threadColumn: .uniqueId) = ?" return grdbFetchOne(sql: sql, arguments: [uniqueId], transaction: grdbTransaction) } } // Traverses all records. // Records are not visited in any particular order. class func anyEnumerate( transaction: SDSAnyReadTransaction, block: (TSThread, UnsafeMutablePointer) -> Void ) { anyEnumerate(transaction: transaction, batched: false, block: block) } // Traverses all records. // Records are not visited in any particular order. class func anyEnumerate( transaction: SDSAnyReadTransaction, batched: Bool = false, block: (TSThread, UnsafeMutablePointer) -> Void ) { let batchSize = batched ? Batching.kDefaultBatchSize : 0 anyEnumerate(transaction: transaction, batchSize: batchSize, block: block) } // Traverses all records. // Records are not visited in any particular order. // // If batchSize > 0, the enumeration is performed in autoreleased batches. class func anyEnumerate( transaction: SDSAnyReadTransaction, batchSize: UInt, block: (TSThread, UnsafeMutablePointer) -> Void ) { switch transaction.readTransaction { case .grdbRead(let grdbTransaction): let cursor = TSThread.grdbFetchCursor(transaction: grdbTransaction) Batching.loop(batchSize: batchSize, loopBlock: { stop in do { guard let value = try cursor.next() else { stop.pointee = true return } block(value, stop) } catch let error { owsFailDebug("Couldn't fetch model: \(error)") } }) } } // Traverses all records' unique ids. // Records are not visited in any particular order. class func anyEnumerateUniqueIds( transaction: SDSAnyReadTransaction, block: (String, UnsafeMutablePointer) -> Void ) { anyEnumerateUniqueIds(transaction: transaction, batched: false, block: block) } // Traverses all records' unique ids. // Records are not visited in any particular order. class func anyEnumerateUniqueIds( transaction: SDSAnyReadTransaction, batched: Bool = false, block: (String, UnsafeMutablePointer) -> Void ) { let batchSize = batched ? Batching.kDefaultBatchSize : 0 anyEnumerateUniqueIds(transaction: transaction, batchSize: batchSize, block: block) } // Traverses all records' unique ids. // Records are not visited in any particular order. // // If batchSize > 0, the enumeration is performed in autoreleased batches. class func anyEnumerateUniqueIds( transaction: SDSAnyReadTransaction, batchSize: UInt, block: (String, UnsafeMutablePointer) -> Void ) { switch transaction.readTransaction { case .grdbRead(let grdbTransaction): grdbEnumerateUniqueIds(transaction: grdbTransaction, sql: """ SELECT \(threadColumn: .uniqueId) FROM \(ThreadRecord.databaseTableName) """, batchSize: batchSize, block: block) } } // Does not order the results. class func anyFetchAll(transaction: SDSAnyReadTransaction) -> [TSThread] { var result = [TSThread]() anyEnumerate(transaction: transaction) { (model, _) in result.append(model) } return result } // Does not order the results. class func anyAllUniqueIds(transaction: SDSAnyReadTransaction) -> [String] { var result = [String]() anyEnumerateUniqueIds(transaction: transaction) { (uniqueId, _) in result.append(uniqueId) } return result } class func anyCount(transaction: SDSAnyReadTransaction) -> UInt { switch transaction.readTransaction { case .grdbRead(let grdbTransaction): return ThreadRecord.ows_fetchCount(grdbTransaction.database) } } class func anyExists( uniqueId: String, transaction: SDSAnyReadTransaction ) -> Bool { assert(!uniqueId.isEmpty) switch transaction.readTransaction { case .grdbRead(let grdbTransaction): let sql = "SELECT EXISTS ( SELECT 1 FROM \(ThreadRecord.databaseTableName) WHERE \(threadColumn: .uniqueId) = ? )" let arguments: StatementArguments = [uniqueId] do { return try Bool.fetchOne(grdbTransaction.database, sql: sql, arguments: arguments) ?? false } catch { DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFail("Missing instance.") } } } } // MARK: - Swift Fetch public extension TSThread { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: GRDBReadTransaction) -> TSThreadCursor { do { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) let cursor = try ThreadRecord.fetchCursor(transaction.database, sqlRequest) return TSThreadCursor(transaction: transaction, cursor: cursor) } catch { DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFailDebug("Read failed: \(error)") return TSThreadCursor(transaction: transaction, cursor: nil) } } class func grdbFetchOne(sql: String, arguments: StatementArguments = StatementArguments(), transaction: GRDBReadTransaction) -> TSThread? { assert(!sql.isEmpty) do { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) guard let record = try ThreadRecord.fetchOne(transaction.database, sqlRequest) else { return nil } let value = try TSThread.fromRecord(record) Self.modelReadCaches.threadReadCache.didReadThread(value, transaction: transaction.asAnyRead) return value } catch { owsFailDebug("error: \(error)") return nil } } } // MARK: - SDSSerializer // The SDSSerializer protocol specifies how to insert and update the // row that corresponds to this model. class TSThreadSerializer: SDSSerializer { private let model: TSThread public init(model: TSThread) { self.model = model } // MARK: - Record func asRecord() throws -> SDSRecord { let id: Int64? = model.grdbId?.int64Value let recordType: SDSRecordType = .thread let uniqueId: String = model.uniqueId // Properties let conversationColorName: String = model.conversationColorNameObsolete let creationDate: Double? = archiveOptionalDate(model.creationDate) let isArchived: Bool = model.isArchivedObsolete let lastInteractionRowId: UInt64 = model.lastInteractionRowId let messageDraft: String? = model.messageDraft let mutedUntilDate: Double? = archiveOptionalDate(model.mutedUntilDateObsolete) let shouldThreadBeVisible: Bool = model.shouldThreadBeVisible let contactPhoneNumber: String? = nil let contactUUID: String? = nil let groupModel: Data? = nil let hasDismissedOffers: Bool? = nil let isMarkedUnread: Bool = model.isMarkedUnreadObsolete let lastVisibleSortIdOnScreenPercentage: Double = model.lastVisibleSortIdOnScreenPercentageObsolete let lastVisibleSortId: UInt64 = model.lastVisibleSortIdObsolete let messageDraftBodyRanges: Data? = optionalArchive(model.messageDraftBodyRanges) let mentionNotificationMode: UInt = model.mentionNotificationMode.rawValue let mutedUntilTimestamp: UInt64 = model.mutedUntilTimestampObsolete let allowsReplies: Bool? = nil let lastSentStoryTimestamp: UInt64? = archiveOptionalNSNumber(model.lastSentStoryTimestamp, conversion: { $0.uint64Value }) let name: String? = nil let addresses: Data? = nil let storyViewMode: UInt = model.storyViewMode.rawValue let editTargetTimestamp: UInt64? = archiveOptionalNSNumber(model.editTargetTimestamp, conversion: { $0.uint64Value }) return ThreadRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, conversationColorName: conversationColorName, creationDate: creationDate, isArchived: isArchived, lastInteractionRowId: lastInteractionRowId, messageDraft: messageDraft, mutedUntilDate: mutedUntilDate, shouldThreadBeVisible: shouldThreadBeVisible, contactPhoneNumber: contactPhoneNumber, contactUUID: contactUUID, groupModel: groupModel, hasDismissedOffers: hasDismissedOffers, isMarkedUnread: isMarkedUnread, lastVisibleSortIdOnScreenPercentage: lastVisibleSortIdOnScreenPercentage, lastVisibleSortId: lastVisibleSortId, messageDraftBodyRanges: messageDraftBodyRanges, mentionNotificationMode: mentionNotificationMode, mutedUntilTimestamp: mutedUntilTimestamp, allowsReplies: allowsReplies, lastSentStoryTimestamp: lastSentStoryTimestamp, name: name, addresses: addresses, storyViewMode: storyViewMode, editTargetTimestamp: editTargetTimestamp) } } // MARK: - Deep Copy #if TESTABLE_BUILD @objc public extension TSThread { // We're not using this method at the moment, // but we might use it for validation of // other deep copy methods. func deepCopyUsingRecord() throws -> TSThread { guard let record = try asRecord() as? ThreadRecord else { throw OWSAssertionError("Could not convert to record.") } return try TSThread.fromRecord(record) } } #endif