548 lines
23 KiB
Swift
548 lines
23 KiB
Swift
//
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import GRDBCipher
|
|
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 var tableMetadata: SDSTableMetadata {
|
|
return TSThreadSerializer.table
|
|
}
|
|
|
|
public static let 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
|
|
|
|
// Base class properties
|
|
public let archivalDate: Date?
|
|
public let archivedAsOfMessageSortId: Bool?
|
|
public let conversationColorName: String
|
|
public let creationDate: Date?
|
|
public let isArchivedByLegacyTimestampForSorting: Bool
|
|
public let lastMessageDate: Date?
|
|
public let messageDraft: String?
|
|
public let mutedUntilDate: Date?
|
|
public let shouldThreadBeVisible: Bool
|
|
|
|
// Subclass properties
|
|
public let groupModel: Data?
|
|
public let hasDismissedOffers: Bool?
|
|
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
|
|
case id
|
|
case recordType
|
|
case uniqueId
|
|
case archivalDate
|
|
case archivedAsOfMessageSortId
|
|
case conversationColorName
|
|
case creationDate
|
|
case isArchivedByLegacyTimestampForSorting
|
|
case lastMessageDate
|
|
case messageDraft
|
|
case mutedUntilDate
|
|
case shouldThreadBeVisible
|
|
case groupModel
|
|
case hasDismissedOffers
|
|
}
|
|
|
|
public static func columnName(_ column: ThreadRecord.CodingKeys, fullyQualified: Bool = false) -> String {
|
|
return fullyQualified ? "\(databaseTableName).\(column.rawValue)" : column.rawValue
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
// TODO: Rework metadata to not include, for example, columns, column indices.
|
|
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 archivalDate: Date? = record.archivalDate
|
|
let archivedAsOfMessageSortId: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.archivedAsOfMessageSortId, name: "archivedAsOfMessageSortId", conversion: { NSNumber(value: $0) })
|
|
let conversationColorName: ConversationColorName = ConversationColorName(rawValue: record.conversationColorName)
|
|
let creationDate: Date? = record.creationDate
|
|
let isArchivedByLegacyTimestampForSorting: Bool = record.isArchivedByLegacyTimestampForSorting
|
|
let lastMessageDate: Date? = record.lastMessageDate
|
|
let messageDraft: String? = record.messageDraft
|
|
let mutedUntilDate: Date? = record.mutedUntilDate
|
|
let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible
|
|
let hasDismissedOffers: Bool = try SDSDeserialization.required(record.hasDismissedOffers, name: "hasDismissedOffers")
|
|
|
|
return TSContactThread(uniqueId: uniqueId,
|
|
archivalDate: archivalDate,
|
|
archivedAsOfMessageSortId: archivedAsOfMessageSortId,
|
|
conversationColorName: conversationColorName,
|
|
creationDate: creationDate,
|
|
isArchivedByLegacyTimestampForSorting: isArchivedByLegacyTimestampForSorting,
|
|
lastMessageDate: lastMessageDate,
|
|
messageDraft: messageDraft,
|
|
mutedUntilDate: mutedUntilDate,
|
|
shouldThreadBeVisible: shouldThreadBeVisible,
|
|
hasDismissedOffers: hasDismissedOffers)
|
|
|
|
case .groupThread:
|
|
|
|
let uniqueId: String = record.uniqueId
|
|
let archivalDate: Date? = record.archivalDate
|
|
let archivedAsOfMessageSortId: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.archivedAsOfMessageSortId, name: "archivedAsOfMessageSortId", conversion: { NSNumber(value: $0) })
|
|
let conversationColorName: ConversationColorName = ConversationColorName(rawValue: record.conversationColorName)
|
|
let creationDate: Date? = record.creationDate
|
|
let isArchivedByLegacyTimestampForSorting: Bool = record.isArchivedByLegacyTimestampForSorting
|
|
let lastMessageDate: Date? = record.lastMessageDate
|
|
let messageDraft: String? = record.messageDraft
|
|
let mutedUntilDate: Date? = record.mutedUntilDate
|
|
let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible
|
|
let groupModelSerialized: Data? = record.groupModel
|
|
let groupModel: TSGroupModel = try SDSDeserialization.unarchive(groupModelSerialized, name: "groupModel")
|
|
|
|
return TSGroupThread(uniqueId: uniqueId,
|
|
archivalDate: archivalDate,
|
|
archivedAsOfMessageSortId: archivedAsOfMessageSortId,
|
|
conversationColorName: conversationColorName,
|
|
creationDate: creationDate,
|
|
isArchivedByLegacyTimestampForSorting: isArchivedByLegacyTimestampForSorting,
|
|
lastMessageDate: lastMessageDate,
|
|
messageDraft: messageDraft,
|
|
mutedUntilDate: mutedUntilDate,
|
|
shouldThreadBeVisible: shouldThreadBeVisible,
|
|
groupModel: groupModel)
|
|
|
|
case .thread:
|
|
|
|
let uniqueId: String = record.uniqueId
|
|
let archivalDate: Date? = record.archivalDate
|
|
let archivedAsOfMessageSortId: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.archivedAsOfMessageSortId, name: "archivedAsOfMessageSortId", conversion: { NSNumber(value: $0) })
|
|
let conversationColorName: ConversationColorName = ConversationColorName(rawValue: record.conversationColorName)
|
|
let creationDate: Date? = record.creationDate
|
|
let isArchivedByLegacyTimestampForSorting: Bool = record.isArchivedByLegacyTimestampForSorting
|
|
let lastMessageDate: Date? = record.lastMessageDate
|
|
let messageDraft: String? = record.messageDraft
|
|
let mutedUntilDate: Date? = record.mutedUntilDate
|
|
let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible
|
|
|
|
return TSThread(uniqueId: uniqueId,
|
|
archivalDate: archivalDate,
|
|
archivedAsOfMessageSortId: archivedAsOfMessageSortId,
|
|
conversationColorName: conversationColorName,
|
|
creationDate: creationDate,
|
|
isArchivedByLegacyTimestampForSorting: isArchivedByLegacyTimestampForSorting,
|
|
lastMessageDate: lastMessageDate,
|
|
messageDraft: messageDraft,
|
|
mutedUntilDate: mutedUntilDate,
|
|
shouldThreadBeVisible: shouldThreadBeVisible)
|
|
|
|
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 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 {
|
|
return try serializer.asRecord()
|
|
}
|
|
}
|
|
|
|
// MARK: - Table Metadata
|
|
|
|
extension TSThreadSerializer {
|
|
|
|
// This defines all of the columns used in the table
|
|
// where this model (and any subclasses) are persisted.
|
|
static let recordTypeColumn = SDSColumnMetadata(columnName: "recordType", columnType: .int, columnIndex: 0)
|
|
static let idColumn = SDSColumnMetadata(columnName: "id", columnType: .primaryKey, columnIndex: 1)
|
|
static let uniqueIdColumn = SDSColumnMetadata(columnName: "uniqueId", columnType: .unicodeString, columnIndex: 2)
|
|
// Base class properties
|
|
static let archivalDateColumn = SDSColumnMetadata(columnName: "archivalDate", columnType: .int64, isOptional: true, columnIndex: 3)
|
|
static let archivedAsOfMessageSortIdColumn = SDSColumnMetadata(columnName: "archivedAsOfMessageSortId", columnType: .int, isOptional: true, columnIndex: 4)
|
|
static let conversationColorNameColumn = SDSColumnMetadata(columnName: "conversationColorName", columnType: .unicodeString, columnIndex: 5)
|
|
static let creationDateColumn = SDSColumnMetadata(columnName: "creationDate", columnType: .int64, isOptional: true, columnIndex: 6)
|
|
static let isArchivedByLegacyTimestampForSortingColumn = SDSColumnMetadata(columnName: "isArchivedByLegacyTimestampForSorting", columnType: .int, columnIndex: 7)
|
|
static let lastMessageDateColumn = SDSColumnMetadata(columnName: "lastMessageDate", columnType: .int64, isOptional: true, columnIndex: 8)
|
|
static let messageDraftColumn = SDSColumnMetadata(columnName: "messageDraft", columnType: .unicodeString, isOptional: true, columnIndex: 9)
|
|
static let mutedUntilDateColumn = SDSColumnMetadata(columnName: "mutedUntilDate", columnType: .int64, isOptional: true, columnIndex: 10)
|
|
static let shouldThreadBeVisibleColumn = SDSColumnMetadata(columnName: "shouldThreadBeVisible", columnType: .int, columnIndex: 11)
|
|
// Subclass properties
|
|
static let groupModelColumn = SDSColumnMetadata(columnName: "groupModel", columnType: .blob, isOptional: true, columnIndex: 12)
|
|
static let hasDismissedOffersColumn = SDSColumnMetadata(columnName: "hasDismissedOffers", columnType: .int, isOptional: true, columnIndex: 13)
|
|
|
|
// TODO: We should decide on a naming convention for
|
|
// tables that store models.
|
|
public static let table = SDSTableMetadata(tableName: "model_TSThread", columns: [
|
|
recordTypeColumn,
|
|
idColumn,
|
|
uniqueIdColumn,
|
|
archivalDateColumn,
|
|
archivedAsOfMessageSortIdColumn,
|
|
conversationColorNameColumn,
|
|
creationDateColumn,
|
|
isArchivedByLegacyTimestampForSortingColumn,
|
|
lastMessageDateColumn,
|
|
messageDraftColumn,
|
|
mutedUntilDateColumn,
|
|
shouldThreadBeVisibleColumn,
|
|
groupModelColumn,
|
|
hasDismissedOffersColumn
|
|
])
|
|
}
|
|
|
|
// MARK: - Save/Remove/Update
|
|
|
|
@objc
|
|
public extension TSThread {
|
|
func anyInsert(transaction: SDSAnyWriteTransaction) {
|
|
sdsSave(saveMode: .insert, transaction: transaction)
|
|
}
|
|
|
|
// This method is private; we should never use it directly.
|
|
// Instead, use anyUpdate(transaction:block:), so that we
|
|
// use the "update with" pattern.
|
|
private func anyUpdate(transaction: SDSAnyWriteTransaction) {
|
|
sdsSave(saveMode: .update, transaction: transaction)
|
|
}
|
|
|
|
@available(*, deprecated, message: "Use anyInsert() or anyUpdate() instead.")
|
|
func anyUpsert(transaction: SDSAnyWriteTransaction) {
|
|
sdsSave(saveMode: .upsert, 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) {
|
|
guard let uniqueId = uniqueId else {
|
|
owsFailDebug("Missing uniqueId.")
|
|
return
|
|
}
|
|
|
|
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.anyUpdate(transaction: transaction)
|
|
}
|
|
|
|
func anyRemove(transaction: SDSAnyWriteTransaction) {
|
|
anyWillRemove(with: transaction)
|
|
|
|
switch transaction.writeTransaction {
|
|
case .yapWrite(let ydbTransaction):
|
|
ydb_remove(with: ydbTransaction)
|
|
case .grdbWrite(let grdbTransaction):
|
|
do {
|
|
let record = try asRecord()
|
|
record.sdsRemove(transaction: grdbTransaction)
|
|
} catch {
|
|
owsFail("Remove failed: \(error)")
|
|
}
|
|
}
|
|
|
|
anyDidRemove(with: transaction)
|
|
}
|
|
|
|
func anyReload(transaction: SDSAnyReadTransaction) {
|
|
anyReload(transaction: transaction, ignoreMissing: false)
|
|
}
|
|
|
|
func anyReload(transaction: SDSAnyReadTransaction, ignoreMissing: Bool) {
|
|
guard let uniqueId = self.uniqueId else {
|
|
owsFailDebug("uniqueId was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
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 {
|
|
private let cursor: RecordCursor<ThreadRecord>?
|
|
|
|
init(cursor: RecordCursor<ThreadRecord>?) {
|
|
self.cursor = cursor
|
|
}
|
|
|
|
public func next() throws -> TSThread? {
|
|
guard let cursor = cursor else {
|
|
return nil
|
|
}
|
|
guard let record = try cursor.next() else {
|
|
return nil
|
|
}
|
|
return try TSThread.fromRecord(record)
|
|
}
|
|
|
|
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
|
|
|
|
// TODO: We may eventually want to define some combination of:
|
|
//
|
|
// * fetchCursor, fetchOne, fetchAll, etc. (ala GRDB)
|
|
// * Optional "where clause" parameters for filtering.
|
|
// * Async flavors with completions.
|
|
//
|
|
// TODO: I've defined flavors that take a read transaction.
|
|
// Or we might take a "connection" if we end up having that class.
|
|
@objc
|
|
public extension TSThread {
|
|
class func grdbFetchCursor(transaction: GRDBReadTransaction) -> TSThreadCursor {
|
|
let database = transaction.database
|
|
do {
|
|
let cursor = try ThreadRecord.fetchCursor(database)
|
|
return TSThreadCursor(cursor: cursor)
|
|
} catch {
|
|
owsFailDebug("Read failed: \(error)")
|
|
return TSThreadCursor(cursor: nil)
|
|
}
|
|
}
|
|
|
|
// Fetches a single model by "unique id".
|
|
class func anyFetch(uniqueId: String,
|
|
transaction: SDSAnyReadTransaction) -> TSThread? {
|
|
assert(uniqueId.count > 0)
|
|
|
|
switch transaction.readTransaction {
|
|
case .yapRead(let ydbTransaction):
|
|
return TSThread.ydb_fetch(uniqueId: uniqueId, transaction: ydbTransaction)
|
|
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.
|
|
// Traversal aborts if the visitor returns false.
|
|
class func anyEnumerate(transaction: SDSAnyReadTransaction, block: @escaping (TSThread, UnsafeMutablePointer<ObjCBool>) -> Void) {
|
|
switch transaction.readTransaction {
|
|
case .yapRead(let ydbTransaction):
|
|
TSThread.ydb_enumerateCollectionObjects(with: ydbTransaction) { (object, stop) in
|
|
guard let value = object as? TSThread else {
|
|
owsFailDebug("unexpected object: \(type(of: object))")
|
|
return
|
|
}
|
|
block(value, stop)
|
|
}
|
|
case .grdbRead(let grdbTransaction):
|
|
do {
|
|
let cursor = TSThread.grdbFetchCursor(transaction: grdbTransaction)
|
|
var stop: ObjCBool = false
|
|
while let value = try cursor.next() {
|
|
block(value, &stop)
|
|
guard !stop.boolValue else {
|
|
break
|
|
}
|
|
}
|
|
} catch let error as NSError {
|
|
owsFailDebug("Couldn't fetch models: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Does not order the results.
|
|
class func anyFetchAll(transaction: SDSAnyReadTransaction) -> [TSThread] {
|
|
var result = [TSThread]()
|
|
anyEnumerate(transaction: transaction) { (model, _) in
|
|
result.append(model)
|
|
}
|
|
return result
|
|
}
|
|
|
|
class func anyCount(transaction: SDSAnyReadTransaction) -> UInt {
|
|
switch transaction.readTransaction {
|
|
case .yapRead(let ydbTransaction):
|
|
return ydbTransaction.numberOfKeys(inCollection: TSThread.collection())
|
|
case .grdbRead(let grdbTransaction):
|
|
return ThreadRecord.ows_fetchCount(grdbTransaction.database)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Swift Fetch
|
|
|
|
public extension TSThread {
|
|
class func grdbFetchCursor(sql: String,
|
|
arguments: [DatabaseValueConvertible]?,
|
|
transaction: GRDBReadTransaction) -> TSThreadCursor {
|
|
var statementArguments: StatementArguments?
|
|
if let arguments = arguments {
|
|
guard let statementArgs = StatementArguments(arguments) else {
|
|
owsFailDebug("Could not convert arguments.")
|
|
return TSThreadCursor(cursor: nil)
|
|
}
|
|
statementArguments = statementArgs
|
|
}
|
|
let database = transaction.database
|
|
do {
|
|
let statement: SelectStatement = try database.cachedSelectStatement(sql: sql)
|
|
let cursor = try ThreadRecord.fetchCursor(statement, arguments: statementArguments)
|
|
return TSThreadCursor(cursor: cursor)
|
|
} catch {
|
|
Logger.error("sql: \(sql)")
|
|
owsFailDebug("Read failed: \(error)")
|
|
return TSThreadCursor(cursor: nil)
|
|
}
|
|
}
|
|
|
|
class func grdbFetchOne(sql: String,
|
|
arguments: StatementArguments,
|
|
transaction: GRDBReadTransaction) -> TSThread? {
|
|
assert(sql.count > 0)
|
|
|
|
do {
|
|
guard let record = try ThreadRecord.fetchOne(transaction.database, sql: sql, arguments: arguments) else {
|
|
return nil
|
|
}
|
|
|
|
return try TSThread.fromRecord(record)
|
|
} 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 required init(model: TSThread) {
|
|
self.model = model
|
|
}
|
|
|
|
// MARK: - Record
|
|
|
|
func asRecord() throws -> SDSRecord {
|
|
let id: Int64? = nil
|
|
|
|
let recordType: SDSRecordType = .thread
|
|
guard let uniqueId: String = model.uniqueId else {
|
|
owsFailDebug("Missing uniqueId.")
|
|
throw SDSError.missingRequiredField
|
|
}
|
|
|
|
// Base class properties
|
|
let archivalDate: Date? = model.archivalDate
|
|
let archivedAsOfMessageSortId: Bool? = archiveOptionalNSNumber(model.archivedAsOfMessageSortId, conversion: { $0.boolValue })
|
|
let conversationColorName: String = model.conversationColorName.rawValue
|
|
let creationDate: Date? = model.creationDate
|
|
let isArchivedByLegacyTimestampForSorting: Bool = model.isArchivedByLegacyTimestampForSorting
|
|
let lastMessageDate: Date? = model.lastMessageDate
|
|
let messageDraft: String? = model.messageDraft
|
|
let mutedUntilDate: Date? = model.mutedUntilDate
|
|
let shouldThreadBeVisible: Bool = model.shouldThreadBeVisible
|
|
|
|
// Subclass properties
|
|
let groupModel: Data? = nil
|
|
let hasDismissedOffers: Bool? = nil
|
|
|
|
return ThreadRecord(id: id, recordType: recordType, uniqueId: uniqueId, archivalDate: archivalDate, archivedAsOfMessageSortId: archivedAsOfMessageSortId, conversationColorName: conversationColorName, creationDate: creationDate, isArchivedByLegacyTimestampForSorting: isArchivedByLegacyTimestampForSorting, lastMessageDate: lastMessageDate, messageDraft: messageDraft, mutedUntilDate: mutedUntilDate, shouldThreadBeVisible: shouldThreadBeVisible, groupModel: groupModel, hasDismissedOffers: hasDismissedOffers)
|
|
}
|
|
}
|