From 608eadd652f7ce5de54b594de1ec95cf675472fe Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Wed, 1 Apr 2026 19:34:31 +0200 Subject: [PATCH 1/7] fix: build.gradle file --- examples/SampleApp/android/build.gradle | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/examples/SampleApp/android/build.gradle b/examples/SampleApp/android/build.gradle index 2c015768a0..c8cc38702d 100644 --- a/examples/SampleApp/android/build.gradle +++ b/examples/SampleApp/android/build.gradle @@ -26,21 +26,6 @@ buildscript { } } -ext.REACT_NATIVE_NODE_MODULES_DIR = file("$rootDir/../node_modules/react-native").absolutePath -ext.REACT_NATIVE_WORKLETS_NODE_MODULES_DIR = - file("$rootDir/../node_modules/react-native-worklets").absolutePath - -subprojects { subproject -> - if (subproject.path != ":app") { - evaluationDependsOn(":app") - project(":app").tasks.matching { task -> - task.name.startsWith("configureCMake") - }.configureEach { - dependsOn(subproject.tasks.matching { it.name == "preBuild" }) - } - } -} - allprojects { repositories { maven { @@ -53,10 +38,4 @@ allprojects { } } -project(':app') { - tasks.matching { it.name == "preBuild" || it.name.startsWith("configureCMake") }.configureEach { - dependsOn("generateCodegenArtifactsFromSchema") - } -} - apply plugin: "com.facebook.react.rootproject" From d1e87eb3d6369b1562abad12cf29caae72139a83 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Wed, 1 Apr 2026 20:45:35 +0200 Subject: [PATCH 2/7] fix: misconfigured build.gradle files --- package/expo-package/android/build.gradle | 30 +++++++++++++++++++ package/expo-package/package.json | 3 ++ package/native-package/android/build.gradle | 30 +++++++++++++++++++ package/native-package/package.json | 4 +++ package/native-package/react-native.config.js | 13 ++++++++ 5 files changed, 80 insertions(+) create mode 100644 package/native-package/react-native.config.js diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle index d64a081fba..0790bb703f 100644 --- a/package/expo-package/android/build.gradle +++ b/package/expo-package/android/build.gradle @@ -137,3 +137,33 @@ if (isNewArchitectureEnabled()) { codegenJavaPackageName = "com.streamchatexpo" } } + +if (isNewArchitectureEnabled()) { + gradle.projectsEvaluated { + if (rootProject.ext.has("streamChatReactNativeCodegenHookInstalled")) { + return + } + + def androidAppProject = rootProject.subprojects.find { it.plugins.hasPlugin("com.android.application") } + if (androidAppProject == null) { + return + } + + def dependencyCodegenTasks = rootProject.subprojects + .findAll { it != androidAppProject } + .collect { it.tasks.findByName("generateCodegenArtifactsFromSchema") } + .findAll { it != null } + + if (dependencyCodegenTasks.isEmpty()) { + return + } + + rootProject.ext.set("streamChatReactNativeCodegenHookInstalled", true) + + androidAppProject.tasks.matching { task -> + task.name.startsWith("configureCMake") + }.configureEach { + dependsOn(dependencyCodegenTasks) + } + } +} diff --git a/package/expo-package/package.json b/package/expo-package/package.json index 301c6baa66..766e09ec37 100644 --- a/package/expo-package/package.json +++ b/package/expo-package/package.json @@ -93,6 +93,9 @@ "name": "StreamChatExpoSpec", "type": "all", "jsSrcsDir": "src/native", + "android": { + "javaPackageName": "com.streamchatexpo" + }, "ios": { "modulesProvider": { "StreamVideoThumbnail": "StreamVideoThumbnail" diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle index 6113b5c74a..ef113dedfe 100644 --- a/package/native-package/android/build.gradle +++ b/package/native-package/android/build.gradle @@ -155,3 +155,33 @@ if (isNewArchitectureEnabled()) { codegenJavaPackageName = "com.streamchatreactnative" } } + +if (isNewArchitectureEnabled()) { + gradle.projectsEvaluated { + if (rootProject.ext.has("streamChatReactNativeCodegenHookInstalled")) { + return + } + + def androidAppProject = rootProject.subprojects.find { it.plugins.hasPlugin("com.android.application") } + if (androidAppProject == null) { + return + } + + def dependencyCodegenTasks = rootProject.subprojects + .findAll { it != androidAppProject } + .collect { it.tasks.findByName("generateCodegenArtifactsFromSchema") } + .findAll { it != null } + + if (dependencyCodegenTasks.isEmpty()) { + return + } + + rootProject.ext.set("streamChatReactNativeCodegenHookInstalled", true) + + androidAppProject.tasks.matching { task -> + task.name.startsWith("configureCMake") + }.configureEach { + dependsOn(dependencyCodegenTasks) + } + } +} diff --git a/package/native-package/package.json b/package/native-package/package.json index 96d5f3ae9f..c4a182187d 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -21,6 +21,7 @@ "android/gradle", "ios", "*.podspec", + "react-native.config.js", "package.json" ], "license": "SEE LICENSE IN LICENSE", @@ -92,6 +93,9 @@ "name": "StreamChatReactNativeSpec", "type": "all", "jsSrcsDir": "src/native", + "android": { + "javaPackageName": "com.streamchatreactnative" + }, "ios": { "modulesProvider": { "StreamChatReactNative": "StreamChatReactNative", diff --git a/package/native-package/react-native.config.js b/package/native-package/react-native.config.js new file mode 100644 index 0000000000..aa31a707ed --- /dev/null +++ b/package/native-package/react-native.config.js @@ -0,0 +1,13 @@ +module.exports = { + dependency: { + platforms: { + android: { + packageImportPath: 'import com.streamchatreactnative.StreamChatReactNativePackage;', + packageInstance: 'new StreamChatReactNativePackage()', + }, + ios: { + podspecPath: 'stream-chat-react-native.podspec', + }, + }, + }, +}; From 0a11c5ba9e93a67255d7910e79138fadef664af1 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Wed, 1 Apr 2026 21:10:31 +0200 Subject: [PATCH 3/7] fix: reinstate debugging dir exposure --- examples/SampleApp/android/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/SampleApp/android/build.gradle b/examples/SampleApp/android/build.gradle index c8cc38702d..3fce23a8b8 100644 --- a/examples/SampleApp/android/build.gradle +++ b/examples/SampleApp/android/build.gradle @@ -26,6 +26,10 @@ buildscript { } } +ext.REACT_NATIVE_NODE_MODULES_DIR = file("$rootDir/../node_modules/react-native").absolutePath +ext.REACT_NATIVE_WORKLETS_NODE_MODULES_DIR = + file("$rootDir/../node_modules/react-native-worklets").absolutePath + allprojects { repositories { maven { From 6db76b8a01c376dea28ac9b237a053839b74645e Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 2 Apr 2026 16:42:35 +0200 Subject: [PATCH 4/7] feat: optimistic updates for message updating proper queueing --- .../offline-support/optimistic-update.js | 177 ++++++++++++++++++ package/src/components/Channel/Channel.tsx | 51 ++++- package/src/store/OfflineDB.ts | 2 + .../apis/__tests__/updatePendingTask.test.ts | 66 +++++++ package/src/store/apis/index.ts | 1 + package/src/store/apis/updatePendingTask.ts | 25 +++ 6 files changed, 318 insertions(+), 4 deletions(-) create mode 100644 package/src/store/apis/__tests__/updatePendingTask.test.ts create mode 100644 package/src/store/apis/updatePendingTask.ts diff --git a/package/src/__tests__/offline-support/optimistic-update.js b/package/src/__tests__/offline-support/optimistic-update.js index 6ea1be1062..cf9e8b697f 100644 --- a/package/src/__tests__/offline-support/optimistic-update.js +++ b/package/src/__tests__/offline-support/optimistic-update.js @@ -15,6 +15,7 @@ import { getOrCreateChannelApi } from '../../mock-builders/api/getOrCreateChanne import { sendMessageApi } from '../../mock-builders/api/sendMessage'; import { sendReactionApi } from '../../mock-builders/api/sendReaction'; import { useMockedApis } from '../../mock-builders/api/useMockedApis'; +import { generateFileReference } from '../../mock-builders/attachments'; import dispatchConnectionChangedEvent from '../../mock-builders/event/connectionChanged'; import { generateChannelResponse } from '../../mock-builders/generator/channel'; import { generateMember } from '../../mock-builders/generator/member'; @@ -397,6 +398,182 @@ export const OptimisticUpdates = () => { }); }); + describe('edit message', () => { + it('should keep the optimistic edit in state and DB if the LLC queues the edit', async () => { + const message = channel.state.messages[0]; + const editedText = 'edited while offline'; + + render( + + { + await chatClient.offlineDb.addPendingTask({ + channelId: channel.id, + channelType: channel.type, + messageId: message.id, + payload: [localMessage, undefined, options], + type: 'update-message', + }); + return { + message: { + ...localMessage, + message_text_updated_at: new Date(), + updated_at: new Date(), + }, + }; + }} + > + { + await editMessage({ + localMessage: { + ...message, + cid: channel.cid, + text: editedText, + }, + options: {}, + }); + }} + context={MessageInputContext} + > + + + + , + ); + + await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy()); + + await waitFor(async () => { + const updatedMessage = channel.state.findMessage(message.id); + const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks'); + const dbMessages = await BetterSqlite.selectFromTable('messages'); + const dbMessage = dbMessages.find((row) => row.id === message.id); + + expect(updatedMessage.text).toBe(editedText); + expect(updatedMessage.message_text_updated_at).toBeTruthy(); + expect(pendingTasksRows).toHaveLength(1); + expect(pendingTasksRows[0].type).toBe('update-message'); + expect(dbMessage.text).toBe(editedText); + expect(dbMessage.messageTextUpdatedAt).toBeTruthy(); + }); + }); + + it('should rollback the optimistic edit if the request fails and no replayable task exists', async () => { + const message = channel.state.messages[0]; + const originalText = message.text; + + render( + + { + throw new Error('validation'); + }} + > + { + try { + await editMessage({ + localMessage: { + ...message, + cid: channel.cid, + text: 'should rollback', + }, + options: {}, + }); + } catch (e) { + // do nothing + } + }} + context={MessageInputContext} + > + + + + , + ); + + await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy()); + + await waitFor(async () => { + const updatedMessage = channel.state.findMessage(message.id); + const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks'); + const dbMessages = await BetterSqlite.selectFromTable('messages'); + const dbMessage = dbMessages.find((row) => row.id === message.id); + + expect(updatedMessage.text).toBe(originalText); + expect(pendingTasksRows).toHaveLength(0); + expect(dbMessage.text).toBe(originalText); + }); + }); + + it('should keep the optimistic edit for attachment updates without auto-queueing', async () => { + const message = channel.state.messages[0]; + const editedText = 'edited attachment message'; + const localUri = 'file://edited-attachment.png'; + + render( + + { + throw new Error('offline'); + }} + > + { + try { + await editMessage({ + localMessage: { + ...message, + attachments: [ + { + asset_url: localUri, + originalFile: generateFileReference({ + name: 'edited-attachment.png', + type: 'image/png', + uri: localUri, + }), + type: 'file', + }, + ], + cid: channel.cid, + text: editedText, + }, + options: {}, + }); + } catch (e) { + // do nothing + } + }} + context={MessageInputContext} + > + + + + , + ); + + await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy()); + + await waitFor(async () => { + const updatedMessage = channel.state.findMessage(message.id); + const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks'); + const dbMessages = await BetterSqlite.selectFromTable('messages'); + const dbMessage = dbMessages.find((row) => row.id === message.id); + const storedAttachments = JSON.parse(dbMessage.attachments); + + expect(updatedMessage.text).toBe(editedText); + expect(updatedMessage.attachments[0].asset_url).toBe(localUri); + expect(pendingTasksRows).toHaveLength(0); + expect(dbMessage.text).toBe(editedText); + expect(storedAttachments[0].asset_url).toBe(localUri); + }); + }); + }); + describe('pending task execution', () => { it('pending task should be executed after connection is recovered', async () => { const message = channel.state.messages[0]; diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index ec851251d1..e8aba47939 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -1560,10 +1560,53 @@ const ChannelWithContext = (props: PropsWithChildren) = ); const editMessage: InputMessageInputContextValue['editMessage'] = useStableCallback( - ({ localMessage, options }) => - doUpdateMessageRequest - ? doUpdateMessageRequest(channel?.cid || '', localMessage, options) - : client.updateMessage(localMessage, undefined, options), + async ({ localMessage, options }) => { + if (!channel) { + throw new Error('Channel has not been initialized'); + } + + const cid = channel.cid; + const currentMessage = channel.state.findMessage(localMessage.id, localMessage.parent_id); + const optimisticEditedAt = new Date(); + const optimisticEditedAtString = optimisticEditedAt.toISOString(); + const optimisticMessage = { + ...currentMessage, + ...localMessage, + cid, + message_text_updated_at: optimisticEditedAtString, + updated_at: optimisticEditedAt, + } as unknown as LocalMessage; + + updateMessage(optimisticMessage); + threadInstance?.updateParentMessageOrReplyLocally( + optimisticMessage as unknown as MessageResponse, + ); + client.offlineDb?.executeQuerySafely( + (db) => + db.updateMessage({ + message: { ...optimisticMessage, cid }, + }), + { method: 'updateMessage' }, + ); + + const response = doUpdateMessageRequest + ? await doUpdateMessageRequest(cid, localMessage, options) + : await client.updateMessage(localMessage, undefined, options); + + if (response?.message) { + updateMessage(response.message); + threadInstance?.updateParentMessageOrReplyLocally(response.message); + client.offlineDb?.executeQuerySafely( + (db) => + db.updateMessage({ + message: { ...response.message, cid }, + }), + { method: 'updateMessage' }, + ); + } + + return response; + }, ); /** diff --git a/package/src/store/OfflineDB.ts b/package/src/store/OfflineDB.ts index 254a5cf2de..262d73273f 100644 --- a/package/src/store/OfflineDB.ts +++ b/package/src/store/OfflineDB.ts @@ -67,6 +67,8 @@ export class OfflineDB extends AbstractOfflineDB { addPendingTask = api.addPendingTask; + updatePendingTask = api.updatePendingTask; + deletePendingTask = api.deletePendingTask; deleteReaction = api.deleteReaction; diff --git a/package/src/store/apis/__tests__/updatePendingTask.test.ts b/package/src/store/apis/__tests__/updatePendingTask.test.ts new file mode 100644 index 0000000000..4b12803372 --- /dev/null +++ b/package/src/store/apis/__tests__/updatePendingTask.test.ts @@ -0,0 +1,66 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { addPendingTask, getPendingTasks, updatePendingTask } from '..'; +import { generateMessage } from '../../../mock-builders/generator/message'; +import { BetterSqlite } from '../../../test-utils/BetterSqlite'; +import { SqliteClient } from '../../SqliteClient'; + +describe('updatePendingTask', () => { + beforeEach(async () => { + await SqliteClient.initializeDatabase(); + await BetterSqlite.openDB(); + }); + + afterEach(() => { + BetterSqlite.dropAllTables(); + BetterSqlite.closeDB(); + jest.clearAllMocks(); + }); + + it('should replace an existing pending task row by id without changing its createdAt ordering', async () => { + const channelId = uuidv4(); + const originalMessage = generateMessage({ + cid: `messaging:${channelId}`, + id: uuidv4(), + text: 'original text', + }); + + await addPendingTask({ + channelId, + channelType: 'messaging', + messageId: originalMessage.id, + payload: [originalMessage, {}], + type: 'send-message', + }); + + const [originalRow] = await BetterSqlite.selectFromTable('pendingTasks'); + const [originalTask] = await getPendingTasks({ messageId: originalMessage.id }); + + const editedMessage = { + ...originalMessage, + text: 'edited text', + }; + + await updatePendingTask({ + id: originalTask.id, + task: { + channelId, + channelType: 'messaging', + messageId: originalMessage.id, + payload: [editedMessage, {}], + type: 'send-message', + }, + }); + + const [updatedRow] = await BetterSqlite.selectFromTable('pendingTasks'); + const [updatedTask] = await getPendingTasks({ messageId: originalMessage.id }); + + expect(updatedRow.id).toBe(originalRow.id); + expect(updatedRow.createdAt).toBe(originalRow.createdAt); + expect(updatedRow.type).toBe('send-message'); + expect(JSON.parse(updatedRow.payload)[0].text).toBe('edited text'); + expect(updatedTask.id).toBe(originalTask.id); + expect(updatedTask.type).toBe('send-message'); + expect(updatedTask.payload[0].text).toBe('edited text'); + }); +}); diff --git a/package/src/store/apis/index.ts b/package/src/store/apis/index.ts index 1d1d66686c..5bd625feda 100644 --- a/package/src/store/apis/index.ts +++ b/package/src/store/apis/index.ts @@ -13,6 +13,7 @@ export * from './getLastSyncedAt'; export * from './getMembers'; export * from './getReads'; export * from './updateMessage'; +export * from './updatePendingTask'; export * from './updateReaction'; export * from './insertReaction'; export * from './deleteReaction'; diff --git a/package/src/store/apis/updatePendingTask.ts b/package/src/store/apis/updatePendingTask.ts new file mode 100644 index 0000000000..be51374059 --- /dev/null +++ b/package/src/store/apis/updatePendingTask.ts @@ -0,0 +1,25 @@ +import type { DBUpdatePendingTaskType } from 'stream-chat'; + +import { mapTaskToStorable } from '../mappers/mapTaskToStorable'; +import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery'; +import { SqliteClient } from '../SqliteClient'; + +export const updatePendingTask = async ({ id, task }: DBUpdatePendingTaskType) => { + const storableTask = mapTaskToStorable(task); + const { createdAt, id: taskId, ...nextTask } = storableTask; + void createdAt; + void taskId; + + const query = createUpdateQuery('pendingTasks', nextTask, { + id, + }); + + SqliteClient.logger?.('info', 'updatePendingTask', { + id, + task: nextTask, + }); + + await SqliteClient.executeSql.apply(null, query); + + return [query]; +}; From 5d931b3ceeecc761f2361a1600e8d1646ca46ef7 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 2 Apr 2026 16:55:09 +0200 Subject: [PATCH 5/7] fix: avoid edited label for failed messages --- .../offline-support/optimistic-update.js | 48 +++++++++++++++++++ package/src/components/Channel/Channel.tsx | 5 +- package/src/store/apis/updatePendingTask.ts | 3 +- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/package/src/__tests__/offline-support/optimistic-update.js b/package/src/__tests__/offline-support/optimistic-update.js index cf9e8b697f..842efc3fbc 100644 --- a/package/src/__tests__/offline-support/optimistic-update.js +++ b/package/src/__tests__/offline-support/optimistic-update.js @@ -26,6 +26,7 @@ import { getTestClientWithUser } from '../../mock-builders/mock'; import { upsertChannels } from '../../store/apis'; import { SqliteClient } from '../../store/SqliteClient'; import { BetterSqlite } from '../../test-utils/BetterSqlite'; +import { MessageStatusTypes } from '../../utils/utils'; test('Workaround to allow exporting tests', () => expect(true).toBe(true)); @@ -509,6 +510,53 @@ export const OptimisticUpdates = () => { }); }); + it('should not set message_text_updated_at during optimistic edit of a failed message', async () => { + const message = channel.state.messages[0]; + const optimisticStateSpy = jest.fn(); + + render( + + { + const optimisticMessage = channel.state.findMessage(message.id); + optimisticStateSpy(optimisticMessage); + + return { + message: { + ...optimisticMessage, + }, + }; + }} + > + { + await editMessage({ + localMessage: { + ...message, + cid: channel.cid, + status: MessageStatusTypes.FAILED, + text: 'edited failed message', + }, + options: {}, + }); + }} + context={MessageInputContext} + > + + + + , + ); + + await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy()); + + await waitFor(() => { + expect(optimisticStateSpy).toHaveBeenCalled(); + expect(optimisticStateSpy.mock.calls[0][0].message_text_updated_at).toBeUndefined(); + }); + }); + it('should keep the optimistic edit for attachment updates without auto-queueing', async () => { const message = channel.state.messages[0]; const editedText = 'edited attachment message'; diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index e8aba47939..b726a4e272 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -1567,13 +1567,16 @@ const ChannelWithContext = (props: PropsWithChildren) = const cid = channel.cid; const currentMessage = channel.state.findMessage(localMessage.id, localMessage.parent_id); + const isFailedMessage = + currentMessage?.status === MessageStatusTypes.FAILED || + localMessage.status === MessageStatusTypes.FAILED; const optimisticEditedAt = new Date(); const optimisticEditedAtString = optimisticEditedAt.toISOString(); const optimisticMessage = { ...currentMessage, ...localMessage, cid, - message_text_updated_at: optimisticEditedAtString, + message_text_updated_at: isFailedMessage ? undefined : optimisticEditedAtString, updated_at: optimisticEditedAt, } as unknown as LocalMessage; diff --git a/package/src/store/apis/updatePendingTask.ts b/package/src/store/apis/updatePendingTask.ts index be51374059..af0ec2baae 100644 --- a/package/src/store/apis/updatePendingTask.ts +++ b/package/src/store/apis/updatePendingTask.ts @@ -6,9 +6,8 @@ import { SqliteClient } from '../SqliteClient'; export const updatePendingTask = async ({ id, task }: DBUpdatePendingTaskType) => { const storableTask = mapTaskToStorable(task); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { createdAt, id: taskId, ...nextTask } = storableTask; - void createdAt; - void taskId; const query = createUpdateQuery('pendingTasks', nextTask, { id, From 0e2f246b4b4ede9aa2350c0e9b729bb047a1003b Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 2 Apr 2026 17:06:35 +0200 Subject: [PATCH 6/7] chore: bump stream-chat and fix tests --- package/foobar.db-journal | Bin 16928 -> 0 bytes package/package.json | 2 +- .../offline-support/optimistic-update.js | 10 +++++----- package/yarn.lock | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 package/foobar.db-journal diff --git a/package/foobar.db-journal b/package/foobar.db-journal deleted file mode 100644 index 334d055043d2fadf0d95cd22e83d67f9a3399004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16928 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 { }); }); - it('should rollback the optimistic edit if the request fails and no replayable task exists', async () => { + it('should keep the optimistic edit if the request fails', async () => { const message = channel.state.messages[0]; - const originalText = message.text; + const editedText = 'should stay optimistic'; render( @@ -480,7 +480,7 @@ export const OptimisticUpdates = () => { localMessage: { ...message, cid: channel.cid, - text: 'should rollback', + text: editedText, }, options: {}, }); @@ -504,9 +504,9 @@ export const OptimisticUpdates = () => { const dbMessages = await BetterSqlite.selectFromTable('messages'); const dbMessage = dbMessages.find((row) => row.id === message.id); - expect(updatedMessage.text).toBe(originalText); + expect(updatedMessage.text).toBe(editedText); expect(pendingTasksRows).toHaveLength(0); - expect(dbMessage.text).toBe(originalText); + expect(dbMessage.text).toBe(editedText); }); }); diff --git a/package/yarn.lock b/package/yarn.lock index e350903d62..8fdba707ec 100644 --- a/package/yarn.lock +++ b/package/yarn.lock @@ -8352,10 +8352,10 @@ stdin-discarder@^0.2.2: resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be" integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== -stream-chat@^9.36.1: - version "9.36.2" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.36.2.tgz#cd2cfac1f8d7b045c34dce51e2de1cb66bf288f5" - integrity sha512-sSCxTXJOf0BLDMZ2/cqvFged/LLbiWoIhs7v3UsRj0EM0T8tTam7zpU77TSccNDlK5j1C1/llSUVyMLc7aCDsA== +stream-chat@^9.40.0: + version "9.40.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.40.0.tgz#32ee29ae3442744fb7068b0ecaa3cc1a3b456b97" + integrity sha512-IH3MdxS1zGwOob1dBqRTIqS7wB2Y6Spu4ufo4/yVKW/IFEYRs38BSLHcMsJISvUbPpBleXKIrUOQZu6VsgJpdw== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" From e309eee79ba3a2c718023d475979cde6ce3b5745 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 2 Apr 2026 17:10:21 +0200 Subject: [PATCH 7/7] fix: revert deleted db journal --- package/foobar.db-journal | Bin 0 -> 16928 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 package/foobar.db-journal diff --git a/package/foobar.db-journal b/package/foobar.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..334d055043d2fadf0d95cd22e83d67f9a3399004 GIT binary patch literal 16928 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