Skip to content

fix: handle user.updated in threads - #1979

Merged
isekovanic merged 2 commits into
fix/optimistic-updates-on-translationsfrom
fix/must-have-v10-fixes
Nov 2, 2026
Merged

isekovanic merged 2 commits into
fix/optimistic-updates-on-translationsfrom
fix/must-have-v10-fixes

Conversation

@isekovanic

@isekovanic isekovanic commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Ticket: https://linear.app/stream/issue/REACT-1488

Commits:

Stacked on #1975.

CLA

  • I have signed the Stream CLA (required).
  • 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.

@MartinCupela

Copy link
Copy Markdown
Contributor

Reviewed at 0106c4970; I read the code but didn't run it. The replies part looks right. One gap: the thread's parent message keeps the old author.

  • thread.state.parentMessage mirrors client.messageStore[thread.id] (subscribeParentMessageFromStore, thread.ts:682-702). The thread puts the parent there itself (hydrateState, :409).
  • On user.updated, a store entry is only rewritten by a paginator whose own index holds it: reflectUserUpdate() walks _itemIndex.values(), which is that paginator's member ids. The thread's paginator holds only replies (MessagePaginator.ts:223), so handleUserUpdated never reaches the parent. The channel's paginator rewrites it only while the parent is among its loaded messages.
  • Example: Ann started a thread last month in #general. The user opens it from the Threads view, and the channel's message list isn't loaded or holds only this week's messages. Ann renames herself to "Anna". Her replies now say "Anna", but the parent above them still says "Ann" with the old avatar.
  • Fix: in handleUserUpdated, also write the new user into the parent in client.messageStore when its author matches, as updateParentMessageLocally already writes that entry. A second test: a thread whose parent no channel paginator holds, after user.updated, shows the new name in thread.state.parentMessage.

@isekovanic

Copy link
Copy Markdown
Contributor Author

thanks @MartinCupela ,

should be fixed here

@isekovanic
isekovanic force-pushed the fix/must-have-v10-fixes branch from 36eb8e1 to 899d940 Compare November 2, 2026 14:30

@MartinCupela MartinCupela left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed the parent fix at 899d940d1: the new test fails without the handleUserUpdated change (parent still shows "Old name") and passes with it. All 184 tests in threads.test.ts and the type check pass.

@isekovanic
isekovanic merged commit 9064ffc into release-v10 Nov 2, 2026
4 checks passed
@isekovanic
isekovanic deleted the fix/must-have-v10-fixes branch November 2, 2026 16:16
isekovanic added a commit that referenced this pull request Nov 2, 2026
…, read events check (#1981)

Tickets:

- https://linear.app/stream/issue/REACT-1422
- https://linear.app/stream/issue/REACT-1322
- https://linear.app/stream/issue/REACT-1445
- https://linear.app/stream/issue/REACT-1325

Stacked on #1979.

## 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?

Four V10 fixes, one commit per ticket.

| Commit | Ticket | Fix |
| --- | --- | --- |
|
[`b8b0617b`](b8b0617)
| [REACT-1422](https://linear.app/stream/issue/REACT-1422) | A new
message while scrolled up adds to the unread snapshot's count instead of
replacing it |
|
[`0b9482c9`](0b9482c)
| [REACT-1322](https://linear.app/stream/issue/REACT-1322) | Pinning or
unpinning a reply updates the channel's pinned list |
|
[`ba464795`](ba46479)
| [REACT-1445](https://linear.app/stream/issue/REACT-1445) | Channel
lists filtered by `joined` or `invite` keep their channels, and
`addFilterResolvers()` rules run |
|
[`d8cb25cc`](d8cb25c)
| [REACT-1325](https://linear.app/stream/issue/REACT-1325) | One read
events check decides both counting unread messages and marking read |

### REACT-1422 - unread snapshot count

Commit:
[`b8b0617b`](b8b0617)

A channel opened with 14 unread messages is marked read right away, so
the read state's count drops to 0 while the unread snapshot keeps 14 for
the separator and the "N unread" banner. When the user scrolled up and
new messages arrived, `message.new` copied the read state's count into
the snapshot, so the separator and the banner went from 14 to 1, then 2.
The user expects 16.

A new unread message now adds 1 to the snapshot's own count. When the
channel wasn't marked read on open the two counts are equal, so nothing
changes there. This is also how v9 counted, in the UI.

### REACT-1322 - pinned replies

Commit:
[`0b9482c9`](0b9482c)

The channel's `message.updated` handler leaves thread-only replies to
the thread, and replies whose thread is loaded to that thread. The
pinned list was only updated inside the same branch, so pinning a reply
never added it to the channel's pinned list, and unpinning one never
removed it, until the channel was queried again.

These replies now reach the pinned list too. When the thread is loaded,
its write already reaches a listed reply through the shared message
store, so the pinned list only takes the update when the pin state
changes. Pinning or unpinning a reply whose thread is loaded publishes
the lists that hold the reply twice, once from each handler. Edits are
unaffected.

The ticket's other two parts needed no change. The `show_in_channel`
edit was already fixed, and the `state: false` re-query on a `frozen`
change doesn't empty the pinned list: the server returns
`pinned_messages` for it as well.

### REACT-1445 - `joined` and `invite` filters

Commit:
[`ba464795`](ba46479)

`ChannelPaginator` had no local rule for `joined` or `invite`, so both
fell through to the catch-all rule, which reads `channel.data` and finds
nothing. The first event that checks a channel against a list's filter
removed it from a "Joined" or "Invites" list.

- `invite` resolves the own invite's status from
`channel.state.membership`: `accepted`, `rejected`, `pending`, or none.
- `joined` is true for a member, unless the membership is a pending or
rejected invite, as in the Query Channels docs.
- `addFilterResolvers()` now puts the added rules ahead of the existing
ones. It used to append them after the catch-all, where they never ran.
An added rule now takes precedence over a built-in one for the same
field.
- `FieldToDataResolver` is exported, so apps can type their own rules.

### REACT-1325 - read events

Commit:
[`d8cb25cc`](d8cb25c)

Counting unread messages checked only the `read-events` capability,
while marking read checked only `config.readEvents.enabled`. With
`readEvents.enabled: false` the count kept growing and was never marked
read. Without the capability, `markRead` was still sent.

The new `channel.readEventsEnabled` is true when
`config.readEvents.enabled` and the capability both allow read events.
Counting, `markRead()`, `markUnread()`, the delivery reporter and the
offline read persistence all use it. That matches v9, with these
differences:

- `markRead()` and `markUnread()` throw without the capability, as they
already did when the config turns read events off. v9 returned `null`.
- `readEvents.enabled: false` in the client config now also stops
counting, unless `localUnreadCountEnabled` is set.
- Without the capability, a local unread count is reset by
`markReadLocally()`. In v9 it was never reset.
- With read events off, the reporter sends nothing, for channels and
threads, and resolves `null`.

`localUnreadCountEnabled` has no effect while read events are on. The
JSDoc and config descriptions say so now.

### Testing

- REACT-1422: a test in `channel.test.js` starts from a snapshot of 14
with the read state at 0 and expects 16 after two messages. It gets 2
without the fix.
- REACT-1322: tests for pinning and unpinning a thread-only reply, and
in `publishAmplification.baseline.test.ts` for unpinning and pinning
again a reply whose thread is loaded.
- REACT-1445: tests for `joined` and `invite` against every membership
state, a joined channel staying in a `joined: true` list when an event
ingests it again, and a rule added with `addFilterResolvers()` resolving
a field the catch-all would take.
- REACT-1325: tests for `readEvents.enabled: false` not counting, the
local count, `markRead()` without the capability, and the local reset
without the capability. Two reporter tests that expected a request with
read events off now expect none.
- `yarn test`, `yarn run types`, eslint and prettier pass.

## Changelog

- Fix the unread separator and banner count dropping when new messages
arrive while scrolled up.
- Fix pinned replies not being added to or removed from the channel's
pinned list.
- Fix channel lists filtered by `joined` or `invite` losing their
channels on the first event.
- `addFilterResolvers()` rules now take precedence over the built-in
ones.
- Export `FieldToDataResolver`.
- Add `channel.readEventsEnabled`. Unread messages are counted and
marked read only while read events are on.
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 📦🚀

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.

4 participants