Repository navigation
fix: handle user.updated in threads - #1979
Merged
isekovanic merged 2 commits intoNov 2, 2026
Merged
Conversation
isekovanic
requested review from
MartinCupela,
NikolaGoranNaumovski,
greenfrvr and
oliverlaz
November 2, 2026 10:45
oliverlaz
approved these changes
Nov 2, 2026
Contributor
|
Reviewed at
|
Merged
2 tasks done
Contributor
Author
|
thanks @MartinCupela , should be fixed here |
isekovanic
force-pushed
the
fix/must-have-v10-fixes
branch
from
November 2, 2026 14:30
36eb8e1 to
899d940
Compare
MartinCupela
approved these changes
Nov 2, 2026
MartinCupela
left a comment
Contributor
There was a problem hiding this comment.
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
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))
|
🎉 This PR is included in version 10.0.0-rc.30 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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-1488
Commits:
0106c4970- the thread's replies36eb8e195- the thread's parent messageStacked on #1975.
CLA
Description of the changes, What, Why and How?
When a user changes their name or image,
client._updateUserMessageReferencesreplaces the user object in the messages of every loaded channel'smessagePaginatorandpinnedMessagesPaginator. Replies that exist only in a thread live in that thread's ownmessagePaginator, which nothing updated, so a renamed user kept their old name and image in threads until the thread was reloaded. v9 updated the user inchannel.state.threadsas well.ThreadManageralready routesuser.deletedanduser.messages.deletedto every thread in its store, since a user's replies can be in any thread. It now routesuser.updatedthe same way, and the newThread.handleUserUpdatedcallsmessagePaginator.reflectUserUpdate(), which is what the channel's message list uses.The thread's parent message isn't one of its replies.
thread.state.parentMessagefollows the parent's entry inclient.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.handleUserUpdatednow 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
threads.test.ts. A reply from "Old name" is loaded,user.updatedarrives with "New name", and both the stored reply and the paginator'sitemsshow the new name. It fails without the routing.threads.test.tsfor a thread whose parent no channel list holds: afteruser.updated,thread.state.parentMessageshows the new name. It fails without the parent write.yarn test,yarn run types,eslintandprettierpass.Changelog