Repository navigation
fix: optimistic updates for translated messages - #1975
Merged
Merged
Conversation
oliverlaz
approved these changes
Nov 2, 2026
Contributor
|
@isekovanic Recently there has been added poll translation. We should probably address that as well. WDYT? |
Contributor
Author
nice catch, but I don't think polls should have the same issue (we don't do optimistic updates on editing a poll anyway). if we ever do decide to do it we can think about adding this there too |
This was referenced Nov 2, 2026
greenfrvr
approved these changes
Nov 2, 2026
isekovanic
force-pushed
the
fix/optimistic-updates-on-translations
branch
from
November 2, 2026 14:30
c1be53c to
d3c3bb2
Compare
MartinCupela
self-requested a review
November 2, 2026 15:28
MartinCupela
approved these changes
Nov 2, 2026
isekovanic
added a commit
that referenced
this pull request
Nov 2, 2026
Ticket: https://linear.app/stream/issue/REACT-1488 Commits: - [`0106c4970`](0106c49) - the thread's replies - [`36eb8e195`](36eb8e1) - the thread's parent message Stacked on #1975. ## CLA - [x] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). - [x] Code changes are tested ## Description of the changes, What, Why and How? When a user changes their name or image, `client._updateUserMessageReferences` replaces the user object in the messages of every loaded channel's `messagePaginator` and `pinnedMessagesPaginator`. Replies that exist only in a thread live in that thread's own `messagePaginator`, which nothing updated, so a renamed user kept their old name and image in threads until the thread was reloaded. v9 updated the user in `channel.state.threads` as well. `ThreadManager` already routes `user.deleted` and `user.messages.deleted` to every thread in its store, since a user's replies can be in any thread. It now routes `user.updated` the same way, and the new `Thread.handleUserUpdated` calls `messagePaginator.reflectUserUpdate()`, which is what the channel's message list uses. The thread's parent message isn't one of its replies. `thread.state.parentMessage` follows the parent's entry in `client.messageStore`, which only a channel's message list rewrites, and only while the parent is among its loaded messages. For a thread opened from the Threads view, or one whose parent isn't loaded in the channel, the parent kept the old name. `handleUserUpdated` now also writes the new user into that entry when the parent's author matches. When the channel's list holds the parent, it has already written the same user object by then, so the thread skips the write and every holder publishes once. ### Testing - New test in `threads.test.ts`. A reply from "Old name" is loaded, `user.updated` arrives with "New name", and both the stored reply and the paginator's `items` show the new name. It fails without the routing. - New test in `threads.test.ts` for a thread whose parent no channel list holds: after `user.updated`, `thread.state.parentMessage` shows the new name. It fails without the parent write. - `yarn test`, `yarn run types`, `eslint` and `prettier` pass. ## Changelog - Fix replies and the parent message in a thread keeping a user's old name and image after the user is updated.
github-actions Bot
pushed a commit
that referenced
this pull request
Nov 2, 2026
## [10.0.0-rc.30](v10.0.0-rc.29...v10.0.0-rc.30) (2026-11-02) ### Bug Fixes * handle user.updated in threads ([#1979](#1979)) ([9064ffc](9064ffc)) * notify channel state subscribers on watcher, ban and member removal events ([#1977](#1977)) ([3a7be78](3a7be78)) * optimistic updates for translated messages ([#1975](#1975)) ([91fc290](91fc290)) * restore composer custom data from drafts and edits, keep custom on edit ([#1978](#1978)) ([51ddcad](51ddcad)) * unread snapshot count, pinned replies, joined and invite filters, read events check ([#1981](#1981)) ([1247d10](1247d10))
|
🎉 This PR is included in version 10.0.0-rc.30 🎉 The release is available on: Your semantic-release bot 📦🚀 |
isekovanic
added a commit
to GetStream/stream-chat-react-native
that referenced
this pull request
Nov 2, 2026
Tickets: - https://linear.app/stream/issue/REACT-1486 - https://linear.app/stream/issue/REACT-1487 - https://linear.app/stream/issue/REACT-1489 - https://linear.app/stream/issue/REACT-1490 ## 🎯 Goal Fixes for the must-have V10 issues from the board triage. Each commit fixes one ticket. | Commit | Ticket | Fix | | --- | --- | --- | | [`a0354684c`](a035468) | [REACT-1486](https://linear.app/stream/issue/REACT-1486) | Regression test for an optimistic edit of an auto-translated message | | [`b5705f953`](b5705f9) | [REACT-1489](https://linear.app/stream/issue/REACT-1489) | `setupCommandUIMiddlewares` no longer inserts duplicate middleware | | [`b0d28db84`](b0d28db) | [REACT-1490](https://linear.app/stream/issue/REACT-1490) | A channel list loads its first page even when an event already added channels to it | | [`7a0376993`](7a03769) | [REACT-1487](https://linear.app/stream/issue/REACT-1487) | Reopening a channel or thread left on an older page shows the newest messages | ## 🛠 Implementation details ### REACT-1486 - optimistic edit of a translated message Commit: [`a0354684c`](a035468) The fix is in stream-chat-js (GetStream/stream-chat-js#1975). This commit adds the RN regression test, which edits an auto-translated message through the composer and checks the rendered row while the request is in flight. It fails until `stream-chat` is bumped to an rc that includes GetStream/stream-chat-js#1975. ### REACT-1489 - duplicate command middleware Commit: [`b5705f953`](b5705f9) `setupCommandUIMiddlewares` inserts four middleware but passed `unique: true` only on the first. The composer setup function runs again whenever the composer subscribes, which happens every time a channel opens, and on every `client.config` change. The draft command injection, the active command guard and the command string extraction piled up as a result. Each copy of the draft injection prepends the command, so a draft saved with an active command came out as `/giphy /giphy cats`. All four inserts now pass `unique: true`, so running the setup again replaces them. ### REACT-1490 - channel list stuck on channels an event added Commit: [`b0d28db84`](b0d28db) `ChannelManager.ingestChannel()` puts a channel into every matching paginator, including one that was never queried. `usePaginatedChannels` treated a list as unloaded only while `channels === undefined`, and `loadNextPage` refuses a paginator that was never queried. A list that mounted after an event reached its paginator showed only those channels until pull-to-refresh. That happens when the app opens a channel from a push notification and the user goes back to the list, or with a second list, such as archived channels, that mounts later. A paginator that was never queried now counts as unloaded too, so the list runs its first query. The offline DB seed and a failed first query both mark the paginator as queried, so neither causes an extra query. ### REACT-1487 - reopening a channel or thread on an older page Commit: [`7a0376993`](7a03769) The paginators of a channel and a thread outlive the screen. After a jump to a quoted, pinned or searched message, reopening the channel or thread showed that older page. `<Channel>` relied on `watch()` to go back to the newest messages, but `watch()` returns early for a channel that is already watched and loaded since GetStream/stream-chat-js#1943. v9 re-watched on open and landed on the newest messages. The new public `useJumpToLatestOnOpen()` hook calls `jumpToTheLatestMessage()` when a channel or thread opens on an older page. The newest page is normally still loaded, so this switches back locally without a request. It does nothing when a jump already landed or is in flight, so a deep link from the screen still lands on its target. It also does nothing when `<Channel>` is about to jump to the first unread message, or with `initializeOnMount={false}`. It runs in a layout effect so the old page isn't painted first, and its dependencies are stable, so it runs once per open. `ChannelShell` runs the hook from an empty `JumpToLatestOnOpen` component under its providers, the same way it renders `NotificationAnnouncer`. That covers `<Channel>`, `<Thread>` and any message list, including custom ones. `ChannelShell` takes `initializeOnMount` as an internal prop that `<Channel>` passes on. The hook is exported for apps that set `initializeOnMount={false}` and still want this behaviour. The `MessageList in a thread` test setup now seeds replies with `setItems()`, the way a thread seeds `latest_replies`. It used to write `items` straight into the paginator state, which leaves no intervals and looked like a list that isn't showing its newest page. ## 🎨 UI Changes None. ## 🧪 Testing - Running `setupCommandUIMiddlewares` three times on one composer leaves one of each middleware. Without the fix there are three. - A `ChannelList` whose paginator got a channel from `ingestChannel()` before it was queried runs its first query on mount and shows the queried page. Without the fix it never queries. - `Channel` tests cover reopening on an older page, a deep link that already landed, a jump in flight, `initializeOnMount={false}` and the first unread jump. `Thread` tests cover the first three. Without the fix, only the two reopening tests fail. - In SampleApp on iOS, reopening a channel after a search deep link and reopening a thread after jumping to a quoted reply both land on the newest messages, and the deep link still lands on its target. Opening threads normally triggers no jump and no request. - The full unit suite, `yarn test:typecheck`, `eslint`, `prettier` and `yarn build` pass, apart from the REACT-1486 test above. ## ☑️ Checklist - [x] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket: https://linear.app/stream/issue/REACT-1486/an-optimistic-edit-of-a-translated-message-keeps-the-old-translations
CLA
Description of the changes, What, Why and How?
In a channel with auto-translation, messages carry
i18n, for example{ "language": "en", "en_text": "edit-me", "es_text": "editarme" }. The UI SDKs renderi18n.<userLanguage>_textin place oftextwhen it exists.The optimistic edit in
MessageOperationStatePolicy.optimisticbuilds its copy from the composed message, which spreads the edited message, so the copy kept the oldi18n. The newtextandmessage_text_updated_atwere applied, so the "Edited" label showed at once, but the message kept showing the translation of the old text. It only updated when the server response brought a newi18n. Offline it stayed stale until the queued edit was replayed, and the offline DB row carried the stalei18nuntil then.An edit that changes the text now drops
i18nfrom the optimistic copy. The UI falls back totext, and the server's response brings the translations of the new text. An edit that leaves the text unchanged, for example one that only removes an attachment, keeps the translations.A reader in another language sees the edited original until the response arrives, instead of the translation of the old text.
Testing
MessageOperations.test.ts, one for an edit that changes the text and one for an edit that does not.10.0.0-rc.28and passes with this change.tsc,eslintandprettieron the changed files, andyarn test(4849 tests) pass.Changelog