diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h @@ -42,6 +42,8 @@ virtual void removeDrafts(const std::vector &ids) const = 0; virtual void removeAllMessages() const = 0; virtual std::vector getInitialMessages() const = 0; + virtual std::vector + fetchMessages(std::string threadID, int limit, int offset) const = 0; virtual void removeMessages(const std::vector &ids) const = 0; virtual void removeMessagesForThreads(const std::vector &threadIDs) const = 0; @@ -190,6 +192,8 @@ virtual std::vector getAllThreadsWeb() const = 0; virtual void replaceThreadWeb(const WebThread &thread) const = 0; virtual std::vector getInitialMessagesWeb() const = 0; + virtual std::vector + fetchMessagesWeb(std::string threadID, int limit, int offset) const = 0; virtual void replaceMessageWeb(const WebMessage &message) const = 0; virtual NullableString getOlmPersistAccountDataWeb(int accountID) const = 0; virtual std::vector diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -67,6 +67,8 @@ void removeDrafts(const std::vector &ids) const override; void removeAllMessages() const override; std::vector getInitialMessages() const override; + std::vector + fetchMessages(std::string threadID, int limit, int offset) const override; void removeMessages(const std::vector &ids) const override; void removeMessagesForThreads( const std::vector &threadIDs) const override; @@ -207,6 +209,8 @@ std::vector getAllThreadsWeb() const override; void replaceThreadWeb(const WebThread &thread) const override; std::vector getInitialMessagesWeb() const override; + std::vector + fetchMessagesWeb(std::string threadID, int limit, int offset) const override; void replaceMessageWeb(const WebMessage &message) const override; NullableString getOlmPersistAccountDataWeb(int accountID) const override; std::vector 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 @@ -1535,6 +1535,31 @@ return this->processMessagesResults(preparedSQL); } +std::vector SQLiteQueryExecutor::fetchMessages( + std::string threadID, + int limit, + int offset) const { + static std::string query = + "SELECT " + " m.id, m.local_id, m.thread, m.user, m.type, m.future_type, " + " m.content, m.time, media.id, media.container, media.thread, " + " media.uri, media.type, media.extras " + "FROM messages AS m " + "LEFT JOIN media " + " ON m.id = media.container " + "WHERE m.thread = ? " + "ORDER BY m.time DESC, m.id DESC " + "LIMIT ? OFFSET ?;"; + SQLiteStatementWrapper preparedSQL( + SQLiteQueryExecutor::getConnection(), query, "Failed to fetch messages."); + + bindStringToSQL(threadID.c_str(), preparedSQL, 1); + bindIntToSQL(limit, preparedSQL, 2); + bindIntToSQL(offset, preparedSQL, 3); + + return this->processMessagesResults(preparedSQL); +} + std::vector SQLiteQueryExecutor::processMessagesResults( SQLiteStatementWrapper &preparedSQL) const { std::string prevMsgIdx{}; @@ -2852,6 +2877,14 @@ return this->transformToWebMessages(messages); } +std::vector SQLiteQueryExecutor::fetchMessagesWeb( + std::string threadID, + int limit, + int offset) const { + auto messages = this->fetchMessages(threadID, limit, offset); + return this->transformToWebMessages(messages); +} + void SQLiteQueryExecutor::replaceMessageWeb(const WebMessage &message) const { this->replaceMessage(message.toMessage()); }; diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp --- a/web/cpp/SQLiteQueryExecutorBindings.cpp +++ b/web/cpp/SQLiteQueryExecutorBindings.cpp @@ -325,7 +325,8 @@ .function( "updateMessageSearchIndex", &SQLiteQueryExecutor::updateMessageSearchIndex) - .function("searchMessages", &SQLiteQueryExecutor::searchMessagesWeb); + .function("searchMessages", &SQLiteQueryExecutor::searchMessagesWeb) + .function("fetchMessagesWeb", &SQLiteQueryExecutor::fetchMessagesWeb); } } // namespace comm 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$@ { + let queryExecutor; + let dbModule; + const threadID = '123'; + const userID = '124'; + + beforeAll(async () => { + dbModule = getDatabaseModule(); + + if (!dbModule) { + throw new Error('Database module is missing'); + } + queryExecutor = new dbModule.SQLiteQueryExecutor(FILE_PATH); + if (!queryExecutor) { + throw new Error('SQLiteQueryExecutor is missing'); + } + + for (let i = 0; i < 50; i++) { + const message: WebMessage = { + id: i.toString(), + localID: createNullableString(), + thread: threadID, + user: userID, + type: messageTypes.TEXT, + futureType: createNullableInt(), + content: createNullableString(`text-${i}`), + time: i.toString(), + }; + queryExecutor.replaceMessageWeb(message); + } + }); + + afterAll(() => { + clearSensitiveData(dbModule, FILE_PATH, queryExecutor); + }); + + function assertMessageEqual(message: MessageEntity, id: number) { + const expected: WebMessage = { + id: id.toString(), + localID: createNullableString(), + thread: threadID, + user: userID, + type: messageTypes.TEXT, + futureType: createNullableInt(), + content: createNullableString(`text-${id}`), + time: id.toString(), + }; + expect(message.message).toEqual(expected); + } + + it('should fetch the first messages', () => { + const result = queryExecutor.fetchMessagesWeb(threadID, 5, 0); + expect(result.length).toBe(5); + for (let i = 0; i < 5; i++) { + assertMessageEqual(result[i], 49 - i); + } + }); + + it('should fetch the following messages', () => { + const result = queryExecutor.fetchMessagesWeb(threadID, 5, 5); + expect(result.length).toBe(5); + for (let i = 0; i < 5; i++) { + assertMessageEqual(result[i], 44 - i); + } + }); + + it('should fetch the last messages', () => { + const result = queryExecutor.fetchMessagesWeb(threadID, 5, 45); + expect(result.length).toBe(5); + for (let i = 0; i < 5; i++) { + assertMessageEqual(result[i], 4 - i); + } + }); + + it('should check if thread ID matches', () => { + const result = queryExecutor.fetchMessagesWeb('000', 5, 45); + expect(result.length).toBe(0); + }); + + it('should fetch the remaining messages when limit exceeds the bounds', () => { + const result = queryExecutor.fetchMessagesWeb(threadID, 100, 40); + expect(result.length).toBe(10); + for (let i = 0; i < 10; i++) { + assertMessageEqual(result[i], 9 - i); + } + }); + + it('should return all the messages when limit is high enough', () => { + const result = queryExecutor.fetchMessagesWeb(threadID, 500, 0); + expect(result.length).toBe(50); + for (let i = 0; i < 50; i++) { + assertMessageEqual(result[i], 49 - i); + } + }); +}); 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 @@ -180,4 +180,5 @@ clientDBMessageInfoToWebMessage, webMessageToClientDBMessageInfo, createNullableString, + createNullableInt, }; diff --git a/web/shared-worker/types/sqlite-query-executor.js b/web/shared-worker/types/sqlite-query-executor.js --- a/web/shared-worker/types/sqlite-query-executor.js +++ b/web/shared-worker/types/sqlite-query-executor.js @@ -48,7 +48,7 @@ +version: number, }; -type MessageEntity = { +export type MessageEntity = { +message: WebMessage, +medias: $ReadOnlyArray, }; @@ -63,6 +63,11 @@ removeDrafts(ids: $ReadOnlyArray): void; getInitialMessagesWeb(): $ReadOnlyArray; + fetchMessagesWeb( + threadID: string, + limit: number, + offset: number, + ): $ReadOnlyArray; removeAllMessages(): void; removeMessages(ids: $ReadOnlyArray): void; removeMessagesForThreads(threadIDs: $ReadOnlyArray): void;