feat: support local unread count on v15 - #3298
Conversation
Ports #3265 and the unread half of #3250 onto v15. The badge listened for `notification.mark_read` and `notification.mark_unread` only, and acted on every `notification.mark_read` it saw - including one carrying a `thread_id`, so marking a thread read zeroed the whole channel, and one belonging to another user. Both are now ignored. Nothing listened for `message.read`, the channel-scoped event that actually advances `channel.state.unreadCount`. Marking a channel read emits both events independently, so whether the badge cleared came down to which arrived first. Subscribing to `message.read` settles that; `message.read_locally`, the stand-in for channels whose read events are disabled, is handled with it. `useMarkRead` returned early on such a channel, leaving it permanently unread. It now runs when the client set `isLocalUnreadCountEnabled`, which the LLC answers by resetting the count locally instead of issuing a request it cannot make. `expectUnreadCountToBe` is async and 14 call sites never awaited it, so those assertions were not running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…guration `client.options.isLocalUnreadCountEnabled` moved into the configuration service as `channel.readEvents.localUnreadCountEnabled`, so `useMarkRead` selects it from `channel.configState` alongside `readEvents.enabled` rather than reading a client option. Being store-backed, it also reacts to a change registered after mount, which the option could not. Requires a stream-chat release carrying that configuration field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The badge kept its own copy of a number the channel already owns. `countUnread()` with no argument returns `read[ownUserId].unread_messages`, and everything that moves it - a new message, a read from any device, an explicit mark-unread, a truncation, mark-all-read - writes there first, so ten event subscriptions existed to re-read one store value. One `subscribeWithSelector` on that slice replaces them, keeping the 400ms throttle. The guards that belong to those writes now apply here for free, rather than being restated per event: a thread read is not a channel read, and a read by somebody else is not ours. This supersedes the handler-level versions added a commit ago. Mark-all-read follows the client's rule instead of a local one. It names no channel, and the reset is fanned across channels only when the event reports nothing left unread - otherwise some other channel still is, and zeroing every badge would be wrong. Two smaller corrections fall out: `muted` is applied at render rather than inside the throttled path, so muting shows at once, and the initial value seeds from the channel rather than 0, so the badge no longer flashes empty on mount. BREAKING CHANGE: `ChannelListItemProps.channelUpdateCount` is removed. It forced a re-read of the unread count, which now follows the channel's read state on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`key` is reserved by React: it is stripped before the component is constructed, so `props.key` was always undefined. The declaration bought consumers nothing either, since JSX already accepts `key` on any element. It only suggested a data flow that does not exist. `watchers` belonged to `ChannelList` in v14, which forwarded it as `setActiveChannel(channel, watchers)` - watcher pagination applied when activating a channel. v15 replaced that with workspace navigation and `ChannelList` dropped the prop; the declaration on the item was left behind, read by nothing. BREAKING CHANGE: `ChannelListItemProps.key` and `ChannelListItemProps.watchers` are removed. Neither had any effect. `key` keeps working as React's own reserved prop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## release-v15 #3298 +/- ##
==============================================
Coverage ? 85.40%
==============================================
Files ? 529
Lines ? 15549
Branches ? 4907
==============================================
Hits ? 13280
Misses ? 2269
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
# Conflicts: # examples/tutorial/package.json # examples/vite/package.json # package.json # yarn.lock
df60c5c to
aed8541
Compare
|
Size Change: -477 B (-0.06%) Total Size: 844 kB 📦 View Changed
ℹ️ View Unchanged
|
🎯 Goal
Depends on GetStream/stream-chat-js#1886
Makes local unread counting work on v15, and fixes the channel list's unread badge along the way.
Some channels have read events turned off — livestreams, for example. The server doesn't track reads there, so an app can opt in to counting unread messages on the client instead. On v15 that count went up but never came back down: opening the channel never tried to mark it read. The reset itself lands in stream-chat-js (GetStream/stream-chat-js#1886); this PR is the React half.
Ports #3250 and #3265 from master.
🛠 Implementation details
Marking read.
useMarkReadused to skip channels with read events off. It now runs for them whenreadEvents.localUnreadCountEnabledis on, and stream-chat-js resets the count locally instead of calling the server.The unread badge.
ChannelListItemused to keep its own copy of the unread count and update it from ten different events. It now reads the number the channel already stores (channel.countUnread()), still throttled to one update per 400ms. That fixes two things:It also no longer shows 0 for a moment when the item first renders.
Removed props. Three
ChannelListItemPropsdid nothing and are gone:channelUpdateCountforced the badge to refresh. The badge now refreshes on its own.keyis reserved by React and never reaches the component.<ChannelListItem key={…} />still works as normal.watcherswas left over from v14, whereChannelListpassed it on when opening a channel. Nothing in v15 reads it.Tests. 14 assertions in
ChannelListItem.test.tsxwere missing anawait, so they never actually checked anything. They do now.Not covered: threads in channels with read events off. Local unread counting applies to the channel only.
Before merging: this needs a stream-chat release that includes GetStream/stream-chat-js#1886. Against the current rc,
readEvents.localUnreadCountEnableddoesn't exist yet and the types won't compile.BREAKING CHANGE:
ChannelListItemProps.channelUpdateCountis removed. The unread badge now updates without it.BREAKING CHANGE:
ChannelListItemProps.keyis removed. React's ownkeyprop is unaffected.BREAKING CHANGE:
ChannelListItemProps.watchersis removed. It had no effect in v15.