Skip to content

fix: optimistic updates for translated messages - #1975

Merged
isekovanic merged 1 commit into
release-v10from
fix/optimistic-updates-on-translations
Nov 2, 2026
Merged

isekovanic merged 1 commit into
release-v10from
fix/optimistic-updates-on-translations

Conversation

@isekovanic

Copy link
Copy Markdown
Contributor

Ticket: https://linear.app/stream/issue/REACT-1486/an-optimistic-edit-of-a-translated-message-keeps-the-old-translations

CLA

  • I have signed the Stream CLA (required).
  • Code changes are tested

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 render i18n.<userLanguage>_text in place of text when it exists.

The optimistic edit in MessageOperationStatePolicy.optimistic builds its copy from the composed message, which spreads the edited message, so the copy kept the old i18n. The new text and message_text_updated_at were 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 new i18n. Offline it stayed stale until the queued edit was replayed, and the offline DB row carried the stale i18n until then.

An edit that changes the text now drops i18n from the optimistic copy. The UI falls back to text, 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

  • Two unit tests in MessageOperations.test.ts, one for an edit that changes the text and one for an edit that does not.
  • In the RN SDK, a test that edits a translated message through the composer and checks the rendered row while the request is in flight fails on 10.0.0-rc.28 and passes with this change.
  • tsc, eslint and prettier on the changed files, and yarn test (4849 tests) pass.

Changelog

  • Fix an optimistic message edit showing the old text in channels with auto-translation.

@MartinCupela

Copy link
Copy Markdown
Contributor

@isekovanic Recently there has been added poll translation. We should probably address that as well. WDYT?

@isekovanic

Copy link
Copy Markdown
Contributor Author

@isekovanic Recently there has been added poll translation. We should probably address that as well. WDYT?

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

@isekovanic
isekovanic force-pushed the fix/optimistic-updates-on-translations branch from c1be53c to d3c3bb2 Compare November 2, 2026 14:30
@MartinCupela
MartinCupela self-requested a review November 2, 2026 15:28
@isekovanic
isekovanic merged commit 91fc290 into release-v10 Nov 2, 2026
4 checks passed
@isekovanic
isekovanic deleted the fix/optimistic-updates-on-translations branch November 2, 2026 16:16
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))
@stream-ci-bot

Copy link
Copy Markdown

🎉 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants