Promote chats with drafts to the top of the Chat List

This commit is contained in:
kate-signal
2025-07-14 12:21:38 -07:00
committed by GitHub
parent f83d3548d1
commit aa36739ee9
25 changed files with 600 additions and 17 deletions

View File

@@ -157,6 +157,8 @@
"ThreadRecord.hasDismissedOffers": 11,
"ThreadRecord.isArchivedObsolete": 3,
"ThreadRecord.isMarkedUnreadObsolete": 12,
"ThreadRecord.lastDraftInteractionRowId": 24,
"ThreadRecord.lastDraftUpdateTimestamp": 25,
"ThreadRecord.lastInteractionRowId": 4,
"ThreadRecord.lastReceivedStoryTimestamp": 24,
"ThreadRecord.lastSentStoryTimestamp": 19,
@@ -171,4 +173,4 @@
"ThreadRecord.name": 20,
"ThreadRecord.shouldThreadBeVisible": 7,
"ThreadRecord.storyViewMode": 22
}
}

View File

@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */; };
041C24ED2DF782AF0065B685 /* OutgoingGroupUpdateMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BC9C6428B7C00A0077D442 /* OutgoingGroupUpdateMessageTest.swift */; };
043CC30E2E1EF79C00D9002E /* ThreadFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043CC30D2E1EF79300D9002E /* ThreadFinderTests.swift */; };
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */; };
04AA85BC2E0EFC5C0051C0A7 /* BackupEnablementReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */; };
04BC94D22E061D8300446C52 /* BackupArchiveAttachmentByteCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BC94D12E061D7500446C52 /* BackupArchiveAttachmentByteCounter.swift */; };
@@ -3836,6 +3837,7 @@
/* Begin PBXFileReference section */
041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementMegaphone.swift; sourceTree = "<group>"; };
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementReminderMegaphoneTests.swift; sourceTree = "<group>"; };
043CC30D2E1EF79300D9002E /* ThreadFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadFinderTests.swift; sourceTree = "<group>"; };
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphoneTests.swift; sourceTree = "<group>"; };
04BC94D12E061D7500446C52 /* BackupArchiveAttachmentByteCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveAttachmentByteCounter.swift; sourceTree = "<group>"; };
04E66D3E2DFC81BC0059DBAC /* BackupKeyReminderMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphone.swift; sourceTree = "<group>"; };
@@ -13373,6 +13375,7 @@
F942622E289B1B5500460798 /* SMKTestUtils.swift */,
F9426230289B1B5500460798 /* SMKUDAccessKeyTest.swift */,
F942621E289B1B5500460798 /* TestProtocolRunnerTest.swift */,
043CC30D2E1EF79300D9002E /* ThreadFinderTests.swift */,
D9AD1D9428B9955C00B42E6F /* TSInfoMessage+GroupUpdateType+NSAttributedStringTest.swift */,
F9426227289B1B5500460798 /* TypingIndicatorMessageTest.swift */,
502346742DB017420029DB97 /* ValidatedIncomingEnvelopeTest.swift */,
@@ -19011,6 +19014,7 @@
663B9CB12C9DF55D0055DC7D /* TaskQueueLoaderTest.swift in Sources */,
F9426288289B1B5600460798 /* TestProtocolRunnerTest.swift in Sources */,
6600F38B299016BC00B1EDB7 /* TestSchedulerTest.swift in Sources */,
043CC30E2E1EF79C00D9002E /* ThreadFinderTests.swift in Sources */,
5011D9702A0429B6000FE8E5 /* ThreadMergerTest.swift in Sources */,
50C38CAD2A8EB2610030A731 /* TimeGatedBatchTest.swift in Sources */,
50D1EEAE2D95D95A00ADF921 /* TimeIntervalTest.swift in Sources */,

View File

@@ -78,14 +78,26 @@ public class CLVLoader {
.pinnedThreadIds(tx: transaction)
let visibleThreadUniqueIds: [String]
if isViewingArchive {
visibleThreadUniqueIds = try threadFinder.visibleArchivedThreadIds(transaction: transaction)
if FeatureFlags.moveDraftsUpChatList {
if isViewingArchive {
visibleThreadUniqueIds = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
} else {
visibleThreadUniqueIds = try threadFinder.internal_visibleInboxThreadIds(
filteredBy: viewInfo.inboxFilter,
requiredVisibleThreadIds: viewInfo.requiredVisibleThreadIds,
transaction: transaction
)
}
} else {
visibleThreadUniqueIds = try threadFinder.visibleInboxThreadIds(
filteredBy: viewInfo.inboxFilter,
requiredVisibleThreadIds: viewInfo.requiredVisibleThreadIds,
transaction: transaction
)
if isViewingArchive {
visibleThreadUniqueIds = try threadFinder.visibleArchivedThreadIds(transaction: transaction)
} else {
visibleThreadUniqueIds = try threadFinder.visibleInboxThreadIds(
filteredBy: viewInfo.inboxFilter,
requiredVisibleThreadIds: viewInfo.requiredVisibleThreadIds,
transaction: transaction
)
}
}
var pinnedThreadUniqueIdsToRender = Set<String>()

View File

@@ -53,6 +53,8 @@ public struct ThreadRecord: SDSRecord {
public let addresses: Data?
public let storyViewMode: UInt
public let editTargetTimestamp: UInt64?
public let lastDraftInteractionRowId: UInt64
public let lastDraftUpdateTimestamp: UInt64
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case id
@@ -81,6 +83,8 @@ public struct ThreadRecord: SDSRecord {
case addresses
case storyViewMode
case editTargetTimestamp
case lastDraftInteractionRowId
case lastDraftUpdateTimestamp
}
public static func columnName(_ column: ThreadRecord.CodingKeys, fullyQualified: Bool = false) -> String {
@@ -130,6 +134,8 @@ public extension ThreadRecord {
addresses = row[23]
storyViewMode = row[24]
editTargetTimestamp = row[25]
lastDraftInteractionRowId = row[26]
lastDraftUpdateTimestamp = row[27]
}
}
@@ -166,6 +172,8 @@ extension TSThread {
let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
let isArchivedObsolete: Bool = record.isArchived
let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
let lastDraftInteractionRowId: UInt64 = record.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = record.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = record.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
@@ -190,6 +198,8 @@ extension TSThread {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -214,6 +224,8 @@ extension TSThread {
let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
let isArchivedObsolete: Bool = record.isArchived
let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
let lastDraftInteractionRowId: UInt64 = record.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = record.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = record.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
@@ -237,6 +249,8 @@ extension TSThread {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -259,6 +273,8 @@ extension TSThread {
let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
let isArchivedObsolete: Bool = record.isArchived
let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
let lastDraftInteractionRowId: UInt64 = record.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = record.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = record.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
@@ -283,6 +299,8 @@ extension TSThread {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -307,6 +325,8 @@ extension TSThread {
let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
let isArchivedObsolete: Bool = record.isArchived
let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
let lastDraftInteractionRowId: UInt64 = record.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = record.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = record.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
@@ -328,6 +348,8 @@ extension TSThread {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -402,6 +424,8 @@ extension TSThread: DeepCopyable {
let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
let lastDraftInteractionRowId: UInt64 = modelToCopy.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = modelToCopy.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
@@ -429,6 +453,8 @@ extension TSThread: DeepCopyable {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -453,6 +479,8 @@ extension TSThread: DeepCopyable {
let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
let lastDraftInteractionRowId: UInt64 = modelToCopy.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = modelToCopy.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
@@ -478,6 +506,8 @@ extension TSThread: DeepCopyable {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -500,6 +530,8 @@ extension TSThread: DeepCopyable {
let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
let lastDraftInteractionRowId: UInt64 = modelToCopy.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = modelToCopy.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
@@ -527,6 +559,8 @@ extension TSThread: DeepCopyable {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -552,6 +586,8 @@ extension TSThread: DeepCopyable {
let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
let lastDraftInteractionRowId: UInt64 = modelToCopy.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = modelToCopy.lastDraftUpdateTimestamp
let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
@@ -576,6 +612,8 @@ extension TSThread: DeepCopyable {
editTargetTimestamp: editTargetTimestamp,
isArchivedObsolete: isArchivedObsolete,
isMarkedUnreadObsolete: isMarkedUnreadObsolete,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp,
lastInteractionRowId: lastInteractionRowId,
lastSentStoryTimestamp: lastSentStoryTimestamp,
lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
@@ -625,6 +663,8 @@ extension TSThreadSerializer {
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) }
static var lastDraftInteractionRowIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastDraftInteractionRowId", columnType: .int64) }
static var lastDraftUpdateTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastDraftUpdateTimestamp", columnType: .int64) }
public static var table: SDSTableMetadata {
SDSTableMetadata(
@@ -656,6 +696,8 @@ extension TSThreadSerializer {
addressesColumn,
storyViewModeColumn,
editTargetTimestampColumn,
lastDraftInteractionRowIdColumn,
lastDraftUpdateTimestampColumn,
]
)
}
@@ -1036,7 +1078,9 @@ class TSThreadSerializer: SDSSerializer {
let addresses: Data? = nil
let storyViewMode: UInt = model.storyViewMode.rawValue
let editTargetTimestamp: UInt64? = archiveOptionalNSNumber(model.editTargetTimestamp, conversion: { $0.uint64Value })
let lastDraftInteractionRowId: UInt64 = model.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = model.lastDraftUpdateTimestamp
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)
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, lastDraftInteractionRowId: lastDraftInteractionRowId, lastDraftUpdateTimestamp: lastDraftUpdateTimestamp)
}
}

View File

@@ -64,6 +64,14 @@ typedef NS_CLOSED_ENUM(NSUInteger, TSThreadStoryViewMode) {
// The corresponding interaction may have been deleted.
@property (nonatomic) uint64_t lastInteractionRowId;
// These are used to maintain the ordering of drafts in the chat list.
// When a draft is saved, the lastDraftInteractionRowId for that thread
// should be set to the max lastInteractionRowId across all threads to
// prioritize it in the chat list. lastDraftUpdateTimestamp
// can be used to break ties between threads with the same lastDraftInteractionRowId.
@property (nonatomic) uint64_t lastDraftInteractionRowId;
@property (nonatomic) uint64_t lastDraftUpdateTimestamp;
@property (nonatomic, nullable) NSNumber *editTargetTimestamp;
@property (atomic, readonly) uint64_t mutedUntilTimestampObsolete;
@@ -89,6 +97,8 @@ typedef NS_CLOSED_ENUM(NSUInteger, TSThreadStoryViewMode) {
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -100,7 +110,7 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
mutedUntilTimestampObsolete:(uint64_t)mutedUntilTimestampObsolete
shouldThreadBeVisible:(BOOL)shouldThreadBeVisible
storyViewMode:(TSThreadStoryViewMode)storyViewMode
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:));
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastDraftInteractionRowId:lastDraftUpdateTimestamp:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:));
// clang-format on

View File

@@ -62,6 +62,8 @@ NS_ASSUME_NONNULL_BEGIN
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -86,6 +88,8 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
_editTargetTimestamp = editTargetTimestamp;
_isArchivedObsolete = isArchivedObsolete;
_isMarkedUnreadObsolete = isMarkedUnreadObsolete;
_lastDraftInteractionRowId = lastDraftInteractionRowId;
_lastDraftUpdateTimestamp = lastDraftUpdateTimestamp;
_lastInteractionRowId = lastInteractionRowId;
_lastSentStoryTimestamp = lastSentStoryTimestamp;
_lastVisibleSortIdObsolete = lastVisibleSortIdObsolete;
@@ -497,6 +501,8 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
if (self.messageDraft == nil) {
self.messageDraft = otherThread.messageDraft;
self.messageDraftBodyRanges = otherThread.messageDraftBodyRanges;
self.lastDraftInteractionRowId = otherThread.lastDraftInteractionRowId;
self.lastDraftUpdateTimestamp = otherThread.lastDraftUpdateTimestamp;
}
}

View File

@@ -18,10 +18,22 @@ extension TSThread {
editTargetTimestamp: UInt64?,
transaction tx: DBWriteTransaction
) {
let mostRecentInteractionID = InteractionFinder.maxInteractionRowId(transaction: tx)
anyUpdate(transaction: tx) { thread in
thread.messageDraft = draftMessageBody?.text
thread.messageDraftBodyRanges = draftMessageBody?.ranges
thread.editTargetTimestamp = editTargetTimestamp.map { NSNumber(value: $0) }
if draftMessageBody?.text.nilIfEmpty == nil {
// 0 makes these values effectively irrelevant since they will
// be compared to the lastInteractionRowId, which will always be > 0.
thread.lastDraftInteractionRowId = 0
thread.lastDraftUpdateTimestamp = 0
} else {
thread.lastDraftInteractionRowId = mostRecentInteractionID
thread.lastDraftUpdateTimestamp = Date().ows_millisecondsSince1970
}
}
if let replyInfo {

View File

@@ -86,7 +86,9 @@ class TSPrivateStoryThreadSerializer: SDSSerializer {
let addresses: Data? = model.addresses
let storyViewMode: UInt = model.storyViewMode.rawValue
let editTargetTimestamp: UInt64? = archiveOptionalNSNumber(model.editTargetTimestamp, conversion: { $0.uint64Value })
let lastDraftInteractionRowId: UInt64 = model.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = model.lastDraftUpdateTimestamp
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)
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, lastDraftInteractionRowId: lastDraftInteractionRowId, lastDraftUpdateTimestamp: lastDraftUpdateTimestamp)
}
}

View File

@@ -38,6 +38,8 @@ NS_ASSUME_NONNULL_BEGIN
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -64,6 +66,8 @@ NS_ASSUME_NONNULL_BEGIN
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -78,7 +82,7 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
addresses:(nullable NSData *)addresses
allowsReplies:(BOOL)allowsReplies
name:(NSString *)name
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:addresses:allowsReplies:name:));
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastDraftInteractionRowId:lastDraftUpdateTimestamp:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:addresses:allowsReplies:name:));
// clang-format on

View File

@@ -48,6 +48,8 @@
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -70,6 +72,8 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
editTargetTimestamp:editTargetTimestamp
isArchivedObsolete:isArchivedObsolete
isMarkedUnreadObsolete:isMarkedUnreadObsolete
lastDraftInteractionRowId:lastDraftInteractionRowId
lastDraftUpdateTimestamp:lastDraftUpdateTimestamp
lastInteractionRowId:lastInteractionRowId
lastSentStoryTimestamp:lastSentStoryTimestamp
lastVisibleSortIdObsolete:lastVisibleSortIdObsolete

View File

@@ -86,7 +86,9 @@ class TSContactThreadSerializer: SDSSerializer {
let addresses: Data? = nil
let storyViewMode: UInt = model.storyViewMode.rawValue
let editTargetTimestamp: UInt64? = archiveOptionalNSNumber(model.editTargetTimestamp, conversion: { $0.uint64Value })
let lastDraftInteractionRowId: UInt64 = model.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = model.lastDraftUpdateTimestamp
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)
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, lastDraftInteractionRowId: lastDraftInteractionRowId, lastDraftUpdateTimestamp: lastDraftUpdateTimestamp)
}
}

View File

@@ -34,6 +34,8 @@ NS_ASSUME_NONNULL_BEGIN
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -60,6 +62,8 @@ NS_ASSUME_NONNULL_BEGIN
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -74,7 +78,7 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
contactPhoneNumber:(nullable NSString *)contactPhoneNumber
contactUUID:(nullable NSString *)contactUUID
hasDismissedOffers:(BOOL)hasDismissedOffers
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:contactPhoneNumber:contactUUID:hasDismissedOffers:));
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastDraftInteractionRowId:lastDraftUpdateTimestamp:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:contactPhoneNumber:contactUUID:hasDismissedOffers:));
// clang-format on

View File

@@ -44,6 +44,8 @@ NSUInteger const TSContactThreadSchemaVersion = 1;
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -66,6 +68,8 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
editTargetTimestamp:editTargetTimestamp
isArchivedObsolete:isArchivedObsolete
isMarkedUnreadObsolete:isMarkedUnreadObsolete
lastDraftInteractionRowId:lastDraftInteractionRowId
lastDraftUpdateTimestamp:lastDraftUpdateTimestamp
lastInteractionRowId:lastInteractionRowId
lastSentStoryTimestamp:lastSentStoryTimestamp
lastVisibleSortIdObsolete:lastVisibleSortIdObsolete

View File

@@ -86,7 +86,9 @@ class TSGroupThreadSerializer: SDSSerializer {
let addresses: Data? = nil
let storyViewMode: UInt = model.storyViewMode.rawValue
let editTargetTimestamp: UInt64? = archiveOptionalNSNumber(model.editTargetTimestamp, conversion: { $0.uint64Value })
let lastDraftInteractionRowId: UInt64 = model.lastDraftInteractionRowId
let lastDraftUpdateTimestamp: UInt64 = model.lastDraftUpdateTimestamp
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)
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, lastDraftInteractionRowId: lastDraftInteractionRowId, lastDraftUpdateTimestamp: lastDraftUpdateTimestamp)
}
}

View File

@@ -33,6 +33,8 @@ extern NSString *const TSGroupThread_NotificationKey_UniqueId;
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -59,6 +61,8 @@ extern NSString *const TSGroupThread_NotificationKey_UniqueId;
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -71,7 +75,7 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
shouldThreadBeVisible:(BOOL)shouldThreadBeVisible
storyViewMode:(TSThreadStoryViewMode)storyViewMode
groupModel:(TSGroupModel *)groupModel
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:groupModel:));
NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNameObsolete:creationDate:editTargetTimestamp:isArchivedObsolete:isMarkedUnreadObsolete:lastDraftInteractionRowId:lastDraftUpdateTimestamp:lastInteractionRowId:lastSentStoryTimestamp:lastVisibleSortIdObsolete:lastVisibleSortIdOnScreenPercentageObsolete:mentionNotificationMode:messageDraft:messageDraftBodyRanges:mutedUntilDateObsolete:mutedUntilTimestampObsolete:shouldThreadBeVisible:storyViewMode:groupModel:));
// clang-format on

View File

@@ -29,6 +29,8 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastDraftInteractionRowId:(uint64_t)lastDraftInteractionRowId
lastDraftUpdateTimestamp:(uint64_t)lastDraftUpdateTimestamp
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
@@ -49,6 +51,8 @@ lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPer
editTargetTimestamp:editTargetTimestamp
isArchivedObsolete:isArchivedObsolete
isMarkedUnreadObsolete:isMarkedUnreadObsolete
lastDraftInteractionRowId:lastDraftInteractionRowId
lastDraftUpdateTimestamp:lastDraftUpdateTimestamp
lastInteractionRowId:lastInteractionRowId
lastSentStoryTimestamp:lastSentStoryTimestamp
lastVisibleSortIdObsolete:lastVisibleSortIdObsolete

View File

@@ -206,6 +206,8 @@ extension TSGroupThread {
editTargetTimestamp: nil,
isArchivedObsolete: false,
isMarkedUnreadObsolete: false,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0,
lastInteractionRowId: 1,
lastSentStoryTimestamp: nil,
lastVisibleSortIdObsolete: 0,

View File

@@ -221,6 +221,8 @@ final class ThreadSoftDeleteManagerImpl: ThreadSoftDeleteManager {
/// properties on the thread.
thread.anyUpdate(transaction: sdsTx) { thread in
thread.lastInteractionRowId = 0
thread.lastDraftInteractionRowId = 0
thread.lastDraftUpdateTimestamp = 0
}
}
}

View File

@@ -41,6 +41,8 @@ CREATE
,"addresses" BLOB
,"storyViewMode" INTEGER DEFAULT 0
,"editTargetTimestamp" INTEGER
,"lastDraftInteractionRowId" INTEGER DEFAULT 0
,"lastDraftUpdateTimestamp" INTEGER DEFAULT 0
)
;

View File

@@ -332,6 +332,7 @@ public class GRDBSchemaMigrator {
case removeAttachmentMediaTierDigestColumn
case addListMediaTable
case recomputeAttachmentMediaNames
case lastDraftInteractionRowID
// NOTE: Every time we add a migration id, consider
// incrementing grdbSchemaVersionLatest.
@@ -395,7 +396,7 @@ public class GRDBSchemaMigrator {
}
public static let grdbSchemaVersionDefault: UInt = 0
public static let grdbSchemaVersionLatest: UInt = 117
public static let grdbSchemaVersionLatest: UInt = 118
// An optimization for new users, we have the first migration import the latest schema
// and mark any other migrations as "already run".
@@ -4125,6 +4126,18 @@ public class GRDBSchemaMigrator {
return .success(())
}
migrator.registerMigration(.lastDraftInteractionRowID) { tx in
try tx.database.alter(table: "model_TSThread") { table in
table.add(column: "lastDraftInteractionRowId", .integer).defaults(to: 0)
}
try tx.database.alter(table: "model_TSThread") { table in
table.add(column: "lastDraftUpdateTimestamp", .integer).defaults(to: 0)
}
return .success(())
}
// MARK: - Schema Migration Insertion Point
}

View File

@@ -1589,4 +1589,20 @@ extension InteractionFinder {
return "AND \(columnPrefix)\(interactionColumn: .editState) IS \(TSEditState.none.rawValue)"
}
}
public class func maxInteractionRowId(transaction: DBReadTransaction) -> UInt64 {
let sql = """
SELECT MAX(\(interactionColumn: .id))
FROM \(InteractionRecord.databaseTableName)
"""
do {
return try UInt64.fetchOne(
transaction.database,
sql: sql
) ?? 0
} catch {
owsFailDebug("Failed to find max transaction ID: \(error)")
return 0
}
}
}

View File

@@ -496,4 +496,74 @@ public class ThreadFinder {
AND \(ThreadAssociatedData.databaseTableName).isArchived = \(isArchived ? "1" : "0")
"""
}
public func internal_visibleInboxThreadIds(
filteredBy inboxFilter: InboxFilter? = nil,
requiredVisibleThreadIds: Set<String> = [],
transaction: DBReadTransaction
) throws -> [String] {
if inboxFilter == .unread {
let sql = """
SELECT
\(threadColumnFullyQualified: .uniqueId) AS thread_uniqueId,
\(ThreadAssociatedData.databaseTableName).isMarkedUnread AS thread_isMarkedUnread,
COUNT(i.\(interactionColumn: .uniqueId)) AS interactions_unreadCount
FROM \(ThreadRecord.databaseTableName)
INNER JOIN \(ThreadAssociatedData.databaseTableName)
ON \(ThreadAssociatedData.databaseTableName).threadUniqueId = \(threadColumnFullyQualified: .uniqueId)
AND \(ThreadAssociatedData.databaseTableName).isArchived = 0
LEFT OUTER JOIN \(InteractionRecord.databaseTableName) AS i
\(DEBUG_INDEXED_BY("index_model_TSInteraction_UnreadMessages"))
ON i.\(interactionColumn: .threadUniqueId) = thread_uniqueId
AND \(InteractionFinder.sqlClauseForUnreadInteractionCounts(interactionsAlias: "i"))
WHERE \(threadColumnFullyQualified: .shouldThreadBeVisible) = 1
GROUP BY thread_uniqueId
HAVING (
thread_isMarkedUnread = 1
OR interactions_unreadCount > 0
\(requiredVisibleThreadsClause(forThreadIds: requiredVisibleThreadIds))
)
ORDER BY
CASE WHEN \(threadColumn: .lastDraftInteractionRowId) > \(threadColumn: .lastInteractionRowId)
THEN \(threadColumn: .lastDraftInteractionRowId) ELSE \(threadColumn: .lastInteractionRowId)
END DESC,
\(threadColumn: .lastDraftUpdateTimestamp) DESC
"""
return try String.fetchAll(transaction.database, sql: sql, adapter: RangeRowAdapter(0..<1))
} else {
let sql = """
SELECT \(threadColumn: .uniqueId)
FROM \(ThreadRecord.databaseTableName)
INNER JOIN \(ThreadAssociatedData.databaseTableName)
ON \(ThreadAssociatedData.databaseTableName).threadUniqueId = \(threadColumnFullyQualified: .uniqueId)
AND \(ThreadAssociatedData.databaseTableName).isArchived = 0
WHERE \(threadColumn: .shouldThreadBeVisible) = 1
ORDER BY
CASE WHEN \(threadColumn: .lastDraftInteractionRowId) > \(threadColumn: .lastInteractionRowId)
THEN \(threadColumn: .lastDraftInteractionRowId) ELSE \(threadColumn: .lastInteractionRowId)
END DESC,
\(threadColumn: .lastDraftUpdateTimestamp) DESC
"""
return try String.fetchAll(transaction.database, sql: sql)
}
}
public func internal_visibleArchivedThreadIds(
transaction: DBReadTransaction
) throws -> [String] {
let sql = """
SELECT \(threadColumn: .uniqueId)
FROM \(ThreadRecord.databaseTableName)
\(threadAssociatedDataJoinClause(isArchived: true))
WHERE \(threadColumn: .shouldThreadBeVisible) = 1
ORDER BY
CASE WHEN \(threadColumn: .lastDraftInteractionRowId) > \(threadColumn: .lastInteractionRowId)
THEN \(threadColumn: .lastDraftInteractionRowId) ELSE \(threadColumn: .lastInteractionRowId)
END DESC,
\(threadColumn: .lastDraftUpdateTimestamp) DESC
"""
return try String.fetchAll(transaction.database, sql: sql)
}
}

View File

@@ -64,6 +64,8 @@ public enum FeatureFlags {
public static let libsignalEnforceMinTlsVersion = false
public static let notificationServiceWebSocket = build.includes(.internal)
public static let moveDraftsUpChatList = build.includes(.internal)
}
// MARK: -

View File

@@ -25,6 +25,8 @@ class EditManagerTests: SSKBaseTest {
editTargetTimestamp: nil,
isArchivedObsolete: false,
isMarkedUnreadObsolete: false,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0,
lastInteractionRowId: 0,
lastSentStoryTimestamp: nil,
lastVisibleSortIdObsolete: 0,

View File

@@ -0,0 +1,354 @@
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import XCTest
import LibSignalClient
@testable import SignalServiceKit
class ThreadFinderTests: XCTestCase {
private var db = InMemoryDB()
private let threadFinder = ThreadFinder()
private var contactThread1: TSContactThread!
private var contactThread2: TSContactThread!
enum ChatListType: CaseIterable {
case inbox
case unread
case archive
}
override func setUp() {
super.setUp()
let testPhone1 = E164("+16505550101")!
let testACI1 = Aci.constantForTesting("00000000-0000-4000-8000-00000000000A")
contactThread1 = TSContactThread(contactAddress: SignalServiceAddress(
serviceId: testACI1,
phoneNumber: testPhone1.stringValue,
cache: SignalServiceAddressCache()
))
let testPhone2 = E164("+16505550100")!
let testACI2 = Aci.constantForTesting("00000000-0000-4000-A000-00000000000B")
contactThread2 = TSContactThread(contactAddress: SignalServiceAddress(
serviceId: testACI2,
phoneNumber: testPhone2.stringValue,
cache: SignalServiceAddressCache()
))
}
func buildThreadRecord(
uniqueID: String,
contactThread: TSContactThread,
draft: String?,
lastInteractionRowID: UInt64,
lastDraftInteractionRowId: UInt64,
lastDraftUpdateTimestamp: UInt64
) -> ThreadRecord {
ThreadRecord(
delegate: contactThread,
recordType: .contactThread,
uniqueId: uniqueID,
conversationColorName: "Obsolete",
creationDate: Date.now.timeIntervalSince1970,
isArchived: false,
lastInteractionRowId: lastInteractionRowID,
messageDraft: draft,
mutedUntilDate: nil,
shouldThreadBeVisible: true,
contactPhoneNumber: nil,
contactUUID: nil,
groupModel: nil,
hasDismissedOffers: false,
isMarkedUnread: false,
lastVisibleSortIdOnScreenPercentage: 0.0,
lastVisibleSortId: 0,
messageDraftBodyRanges: nil,
mentionNotificationMode: 0,
mutedUntilTimestamp: 0,
allowsReplies: nil,
lastSentStoryTimestamp: nil,
name: nil,
addresses: nil,
storyViewMode: 0,
editTargetTimestamp: nil,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp)
}
func buildThreadAssociatedData(
uniqueID: String,
isMarkedUnread: Bool,
isArchived: Bool
) -> ThreadAssociatedData {
return ThreadAssociatedData(
threadUniqueId: uniqueID,
isArchived: isArchived,
isMarkedUnread: isMarkedUnread,
mutedUntilTimestamp: 0,
audioPlaybackRate: 1
)
}
func newDraftGoesToTop(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// New draft.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 0,
lastDraftInteractionRowId: 1,
lastDraftUpdateTimestamp: 1
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Non-draft that has more recent lastInteractionRowID.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: nil,
lastInteractionRowID: 1,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let messages = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(messages.count, 2)
XCTAssertTrue(messages.first == "UUID1", "First message should be the draft")
}
case .archive:
try db.read { transaction in
let messages = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(messages.count, 2)
XCTAssertTrue(messages.first == "UUID1", "First message should be the draft")
}
}
}
func testNewDraftGoesToTop_inbox() throws {
try newDraftGoesToTop(chatListType: .inbox)
}
func testNewDraftGoesToTop_unread() throws {
try newDraftGoesToTop(chatListType: .unread)
}
func testNewDraftGoesToTop_archive() throws {
try newDraftGoesToTop(chatListType: .archive)
}
func ignoreDraftRowIdThatIsNotMostRecent(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// New draft that is not the latest activity on the thread.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 3,
lastDraftInteractionRowId: 1,
lastDraftUpdateTimestamp: 1
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Non-draft that has less recent lastInteractionRowID.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: nil,
lastInteractionRowID: 2,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let threads = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID1", "First thread should be the draft thread, even though the draft is not the most recent activity")
}
case .archive:
try db.read { transaction in
let threads = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID1", "First thread should be the draft thread, even though the draft is not the most recent activity")
}
}
}
func testIgnoreDraftRowIdThatIsNotMostRecent_inbox() throws {
try ignoreDraftRowIdThatIsNotMostRecent(chatListType: .inbox)
}
func testIgnoreDraftRowIdThatIsNotMostRecent_unread() throws {
try ignoreDraftRowIdThatIsNotMostRecent(chatListType: .unread)
}
func testIgnoreDraftRowIdThatIsNotMostRecent_archive() throws {
try ignoreDraftRowIdThatIsNotMostRecent(chatListType: .archive)
}
func sameDraftRowIdFallsBackToTimestamp(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// Thread 1, has a draft after latest TSInteraction, but less recent than UUID2.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 2,
lastDraftInteractionRowId: 2,
lastDraftUpdateTimestamp: 1
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Thread 2, has a more recent draft based on timestamp.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: "test draft",
lastInteractionRowID: 1,
lastDraftInteractionRowId: 2,
lastDraftUpdateTimestamp: 2
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let threads = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be the one with latest timestamp")
}
case .archive:
try db.read { transaction in
let threads = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be the one with latest timestamp")
}
}
}
func testSameDraftRowIdFallsBackToTimestamp_inbox() throws {
try sameDraftRowIdFallsBackToTimestamp(chatListType: .inbox)
}
func testSameDraftRowIdFallsBackToTimestamp_unread() throws {
try sameDraftRowIdFallsBackToTimestamp(chatListType: .unread)
}
func testSameDraftRowIdFallsBackToTimestamp_archive() throws {
try sameDraftRowIdFallsBackToTimestamp(chatListType: .archive)
}
func draftNotMostRecent(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// New draft that is not the latest activity on the thread.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 1,
lastDraftInteractionRowId: 2,
lastDraftUpdateTimestamp: 100
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Non-draft that has less recent lastInteractionRowID.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: nil,
lastInteractionRowID: 3,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let threads = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be most recent thread, the non-draft")
}
case .archive:
try db.read { transaction in
let threads = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be most recent thread, the non-draft")
}
}
}
func testDraftNotMostRecent_inbox() throws {
try draftNotMostRecent(chatListType: .inbox)
}
func testDraftNotMostRecent_unread() throws {
try draftNotMostRecent(chatListType: .unread)
}
func testDraftNotMostRecent_archive() throws {
try draftNotMostRecent(chatListType: .archive)
}
}