diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -594,8 +594,9 @@ "(" " id, type, name, description, color, creation_time, parent_thread_id," " containing_thread_id, community, members, roles, current_user," - " source_message_id, replies_count, avatar, pinned_count, timestamps) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; + " source_message_id, replies_count, avatar, pinned_count, timestamps," + " pinned_message_ids) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; replaceEntity(this->getConnection(), replaceThreadSQL, thread); }; diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchema.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchema.cpp --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchema.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchema.cpp @@ -88,7 +88,8 @@ " replies_count INTEGER NOT NULL," " avatar TEXT," " pinned_count INTEGER NOT NULL DEFAULT 0," - " timestamps TEXT" + " timestamps TEXT," + " pinned_message_ids TEXT" ");" "CREATE TABLE IF NOT EXISTS backup_threads (" @@ -108,7 +109,8 @@ " replies_count INTEGER NOT NULL," " avatar TEXT," " pinned_count INTEGER NOT NULL DEFAULT 0," - " timestamps TEXT" + " timestamps TEXT," + " pinned_message_ids TEXT" ");" "CREATE TABLE IF NOT EXISTS metadata (" diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchemaMigrations.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchemaMigrations.cpp --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchemaMigrations.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteSchemaMigrations.cpp @@ -935,6 +935,31 @@ return SQLiteSchema::createTable(db, query, "queued_dm_operations"); } +bool add_pinned_message_ids_column_to_threads(sqlite3 *db) { + char *error; + sqlite3_exec( + db, + "ALTER TABLE threads" + " ADD COLUMN pinned_message_ids TEXT;" + "ALTER TABLE backup_threads" + " ADD COLUMN pinned_message_ids TEXT;", + nullptr, + nullptr, + &error); + + if (!error) { + return true; + } + + std::ostringstream stringStream; + stringStream << "Error adding pinned_message_ids column to threads table: " + << error; + Logger::log(stringStream.str()); + + sqlite3_free(error); + return false; +} + SQLiteMigrations SQLiteSchema::migrations{ {{1, {create_drafts_table, true}}, {2, {rename_threadID_to_key, true}}, @@ -983,6 +1008,7 @@ {55, {convert_target_message_to_standard_column, true}}, {56, {create_backup_tables, true}}, {57, {create_holders_table, true}}, - {58, {create_queued_dm_operations_table, true}}}}; + {58, {create_queued_dm_operations_table, true}}, + {59, {add_pinned_message_ids_column_to_threads, true}}}}; } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/entities/Thread.h b/native/cpp/CommonCpp/DatabaseManagers/entities/Thread.h --- a/native/cpp/CommonCpp/DatabaseManagers/entities/Thread.h +++ b/native/cpp/CommonCpp/DatabaseManagers/entities/Thread.h @@ -27,6 +27,7 @@ std::optional avatar; int pinned_count; std::optional timestamps; + std::optional pinned_message_ids; static Thread fromSQLResult(sqlite3_stmt *sqlRow, int idx) { return Thread{ @@ -47,6 +48,7 @@ getOptionalStringFromSQLRow(sqlRow, idx + 14), getIntFromSQLRow(sqlRow, idx + 15), getOptionalStringFromSQLRow(sqlRow, idx + 16), + getOptionalStringFromSQLRow(sqlRow, idx + 17), }; } @@ -67,7 +69,8 @@ bindIntToSQL(replies_count, sql, idx + 13); bindOptionalStringToSQL(avatar, sql, idx + 14); bindIntToSQL(pinned_count, sql, idx + 15); - return bindOptionalStringToSQL(timestamps, sql, idx + 16); + bindOptionalStringToSQL(timestamps, sql, idx + 16); + return bindOptionalStringToSQL(pinned_message_ids, sql, idx + 17); } }; diff --git a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/DataStores/ThreadStore.cpp b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/DataStores/ThreadStore.cpp --- a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/DataStores/ThreadStore.cpp +++ b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/DataStores/ThreadStore.cpp @@ -82,6 +82,12 @@ jsiThread.setProperty(rt, "timestamps", timestamps); } + if (thread.pinned_message_ids.has_value()) { + auto pinnedMessageIDs = + jsi::String::createFromUtf8(rt, thread.pinned_message_ids.value()); + jsiThread.setProperty(rt, "pinnedMessageIDs", pinnedMessageIDs); + } + jsiThreads.setValueAtIndex(rt, writeIdx++, jsiThread); } return jsiThreads; @@ -181,6 +187,15 @@ std::optional timestamps = maybeTimestamps.isString() ? std::optional(maybeTimestamps.asString(rt).utf8(rt)) : std::nullopt; + + jsi::Value maybePinnedMessageIDs = + threadObj.getProperty(rt, "pinnedMessageIDs"); + std::optional pinnedMessageIDs = + maybePinnedMessageIDs.isString() + ? std::optional( + maybePinnedMessageIDs.asString(rt).utf8(rt)) + : std::nullopt; + bool isBackedUp = op.getProperty(rt, "isBackedUp").asBool(); Thread thread{ @@ -200,7 +215,8 @@ repliesCount, avatar, pinnedCount, - timestamps}; + timestamps, + pinnedMessageIDs}; threadStoreOps.push_back(std::make_unique( std::move(thread), isBackedUp)); diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp --- a/web/cpp/SQLiteQueryExecutorBindings.cpp +++ b/web/cpp/SQLiteQueryExecutorBindings.cpp @@ -125,7 +125,8 @@ .field("repliesCount", &Thread::replies_count) .field("avatar", &Thread::avatar) .field("pinnedCount", &Thread::pinned_count) - .field("timestamps", &Thread::timestamps); + .field("timestamps", &Thread::timestamps) + .field("pinnedMessageIDs", &Thread::pinned_message_ids); value_object("WebMessage") .field("id", &Message::id) diff --git a/web/shared-worker/_generated/comm-query-executor.wasm b/web/shared-worker/_generated/comm-query-executor.wasm index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ { + const pinnedMessageIDs = '["msg1","msg2","msg3"]'; + + queryExecutor.replaceThread( + { + id: 'thread_with_pinned', + type: 1, + name: 'Thread with Pinned Messages', + avatar: undefined, + description: undefined, + color: 'aabbcc', + creationTime: BigInt(6000), + parentThreadID: undefined, + containingThreadID: undefined, + community: undefined, + members: '1', + roles: '1', + currentUser: '1', + sourceMessageID: undefined, + repliesCount: 0, + pinnedCount: 3, + timestamps: undefined, + pinnedMessageIDs, + }, + false, + ); + + const threads = queryExecutor.getAllThreads(); + const threadWithPinned = threads.find(t => t.id === 'thread_with_pinned'); + + expect(threadWithPinned).toBeDefined(); + expect(threadWithPinned?.pinnedMessageIDs).toBe(pinnedMessageIDs); + expect(threadWithPinned?.pinnedCount).toBe(3); + }); + + it('should write and read pinnedMessageIDs correctly for backup threads', () => { + const pinnedMessageIDs = '["backup_msg1","backup_msg2"]'; + + queryExecutor.replaceThread( + { + id: 'backup_thread_with_pinned', + type: 1, + name: 'Backup Thread with Pinned Messages', + avatar: undefined, + description: undefined, + color: 'ddeeff', + creationTime: BigInt(7000), + parentThreadID: undefined, + containingThreadID: undefined, + community: undefined, + members: '2', + roles: '2', + currentUser: '2', + sourceMessageID: undefined, + repliesCount: 0, + pinnedCount: 2, + timestamps: undefined, + pinnedMessageIDs, + }, + true, + ); + + const threads = queryExecutor.getAllThreads(); + const backupThreadWithPinned = threads.find( + t => t.id === 'backup_thread_with_pinned', + ); + + expect(backupThreadWithPinned).toBeDefined(); + expect(backupThreadWithPinned?.pinnedMessageIDs).toBe(pinnedMessageIDs); + expect(backupThreadWithPinned?.pinnedCount).toBe(2); + }); + + it('should handle undefined pinnedMessageIDs correctly', () => { + queryExecutor.replaceThread( + { + id: 'thread_no_pinned', + type: 1, + name: 'Thread without Pinned Messages', + avatar: undefined, + description: undefined, + color: '112233', + creationTime: BigInt(8000), + parentThreadID: undefined, + containingThreadID: undefined, + community: undefined, + members: '1', + roles: '1', + currentUser: '1', + sourceMessageID: undefined, + repliesCount: 0, + pinnedCount: 0, + timestamps: undefined, + pinnedMessageIDs: undefined, + }, + false, + ); + + const threads = queryExecutor.getAllThreads(); + const threadNoPinned = threads.find(t => t.id === 'thread_no_pinned'); + + expect(threadNoPinned).toBeDefined(); + expect(threadNoPinned?.pinnedMessageIDs).toBeUndefined(); + expect(threadNoPinned?.pinnedCount).toBe(0); + }); + + it('should update pinnedMessageIDs when replacing existing thread', () => { + const initialPinnedMessageIDs = '["msg1","msg2"]'; + const updatedPinnedMessageIDs = '["msg1","msg2","msg3","msg4"]'; + + // Create initial thread + queryExecutor.replaceThread( + { + id: 'updatable_thread', + type: 1, + name: 'Updatable Thread', + avatar: undefined, + description: undefined, + color: '445566', + creationTime: BigInt(9000), + parentThreadID: undefined, + containingThreadID: undefined, + community: undefined, + members: '1', + roles: '1', + currentUser: '1', + sourceMessageID: undefined, + repliesCount: 0, + pinnedCount: 2, + timestamps: undefined, + pinnedMessageIDs: initialPinnedMessageIDs, + }, + false, + ); + + let threads = queryExecutor.getAllThreads(); + let thread = threads.find(t => t.id === 'updatable_thread'); + expect(thread?.pinnedMessageIDs).toBe(initialPinnedMessageIDs); + expect(thread?.pinnedCount).toBe(2); + + // Update thread with new pinned messages + queryExecutor.replaceThread( + { + id: 'updatable_thread', + type: 1, + name: 'Updatable Thread', + avatar: undefined, + description: undefined, + color: '445566', + creationTime: BigInt(9000), + parentThreadID: undefined, + containingThreadID: undefined, + community: undefined, + members: '1', + roles: '1', + currentUser: '1', + sourceMessageID: undefined, + repliesCount: 0, + pinnedCount: 4, + timestamps: undefined, + pinnedMessageIDs: updatedPinnedMessageIDs, + }, + false, + ); + + threads = queryExecutor.getAllThreads(); + thread = threads.find(t => t.id === 'updatable_thread'); + expect(thread?.pinnedMessageIDs).toBe(updatedPinnedMessageIDs); + expect(thread?.pinnedCount).toBe(4); + }); }); diff --git a/web/shared-worker/types/entities.js b/web/shared-worker/types/entities.js --- a/web/shared-worker/types/entities.js +++ b/web/shared-worker/types/entities.js @@ -27,6 +27,7 @@ +repliesCount: number, +pinnedCount: number, +timestamps: string | void, + +pinnedMessageIDs: string | void, }; function clientDBThreadInfoToWebThread( @@ -50,6 +51,7 @@ repliesCount: info.repliesCount, pinnedCount: info.pinnedCount || 0, timestamps: nullToUndefined(info.timestamps), + pinnedMessageIDs: nullToUndefined(info.pinnedMessageIDs), }; } @@ -80,6 +82,12 @@ sourceMessageID: thread.sourceMessageID, }; } + if (thread.pinnedMessageIDs) { + result = { + ...result, + pinnedMessageIDs: thread.pinnedMessageIDs, + }; + } return result; }