feat: localized unread count - #3250
Conversation
π WalkthroughWalkthroughChannel read handling now supports local unread counts. Local read events update channel and list state without server requests. Server marking remains conditional on ChangesLocal unread count handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: Suggested reviewers: π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touchesπ Generate docstrings
π§ͺ Generate unit tests (beta)
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 |
|
Size Change: +205 B (+0.02%) Total Size: 884 kB π¦ View Changed
βΉοΈ View Unchanged
|
Codecov Reportβ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3250 +/- ##
==========================================
+ Coverage 85.18% 85.21% +0.03%
==========================================
Files 507 507
Lines 15949 15957 +8
Branches 5025 5029 +4
==========================================
+ Hits 13586 13598 +12
+ Misses 2363 2359 -4 β View full report in Codecov by Harness. π New features to boost your workflow:
|
06fe8a4 to
127811f
Compare
| } catch (e) { | ||
| console.error(t('Failed to mark channel as read')); | ||
| if (activeUnreadHandler) { | ||
| activeUnreadHandler(0, originalTitle.current); | ||
| } else if (originalTitle.current) { | ||
| document.title = originalTitle.current; | ||
| } |
There was a problem hiding this comment.
Got rid of this catch clause - I feel like this should bubble up.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and canβt be posted inline due to platform limitations.
β οΈ Outside diff range comments (1)
src/components/Channel/Channel.tsx (1)
321-343: π©Ί Stability & Availability | π Major | β‘ Quick winHandle rejected mark-read requests.
If
channel.markRead()rejects, this callback exits before title restoration. IfdoMarkReadRequest()rejects, its unawaited promise can become unhandled. Await the custom request and restore scoped error handling for both paths.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Channel/Channel.tsx` around lines 321 - 343, Update the mark-read callback in Channel.tsx to await doMarkReadRequest and wrap both custom and channel.markRead paths with scoped error handling, ensuring rejections are handled while execution still reaches the activeUnreadHandler/document.title restoration logic. Preserve the existing unread-state update behavior for successful responses.
π§Ή Nitpick comments (1)
src/components/Channel/Channel.tsx (1)
349-356: π Maintainability & Code Quality | π΅ Trivial | β‘ Quick winUse
channel.cidin this dependency array.Replace
channelwithchannel.cid. Do not includechanneldirectly in dependency arrays.As per coding guidelines: βDo not include
channelin dependency arrays; usechannel.cidonly.βπ€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Channel/Channel.tsx` around lines 349 - 356, Update the dependency array associated with activeUnreadHandler to replace channel with channel.cid, preserving all other dependencies unchanged and avoiding direct channel object dependencies.Source: Coding guidelines
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/Channel/Channel.tsx`:
- Around line 305-308: Move the channel.disconnected early return from the outer
flow into the server channel.markRead() path. Keep channel.markReadLocally()
reachable while disconnected when local unread counts are enabled, while
preserving the guard before any server read operation.
In `@src/components/ChannelListItem/ChannelListItem.tsx`:
- Around line 120-133: Update the handleEvent logic in ChannelListItemβs
useEffect so it filters by channel.cid only when event.cid is present, allowing
CID-less notification.mark_read events to update the unread count while
preserving filtering for mismatched provided CIDs.
---
Outside diff comments:
In `@src/components/Channel/Channel.tsx`:
- Around line 321-343: Update the mark-read callback in Channel.tsx to await
doMarkReadRequest and wrap both custom and channel.markRead paths with scoped
error handling, ensuring rejections are handled while execution still reaches
the activeUnreadHandler/document.title restoration logic. Preserve the existing
unread-state update behavior for successful responses.
---
Nitpick comments:
In `@src/components/Channel/Channel.tsx`:
- Around line 349-356: Update the dependency array associated with
activeUnreadHandler to replace channel with channel.cid, preserving all other
dependencies unchanged and avoiding direct channel object dependencies.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a276737b-533a-420b-91e6-d8020ec737f1
π Files selected for processing (4)
src/components/Channel/Channel.tsxsrc/components/ChannelListItem/ChannelListItem.tsxsrc/components/ChannelListItem/hooks/useMessageDeliveryStatus.tssrc/components/MessageList/hooks/useMarkRead.ts
| if (channel.disconnected) return; | ||
|
|
||
| if (!channelConfig?.read_events && client.options.isLocalUnreadCountEnabled) { | ||
| const event = channel.markReadLocally(); |
There was a problem hiding this comment.
π― Functional Correctness | π Major | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
echo "## locate Channel.tsx"
fd -a 'Channel\.tsx$' . | sed 's#^\./##' | head -20
echo "## relevant lines in src/components/Channel/Channel.tsx"
wc -l src/components/Channel/Channel.tsx
sed -n '280,360p' src/components/Channel/Channel.tsx
echo "## search markReadLocally and disconnected usages"
rg -n "markReadLocally|markRead\(\)|disconnected|read_events|read_locally" src/components/Channel/Channel.tsx src -g '*.ts' -g '*.tsx' | head -120
echo "## package/import context"
fd -a 'package.json$' . | head -5
cat package.json | jq '{scripts, devDependencies, dependencies}' 2>/dev/null | head -180Repository: GetStream/stream-chat-react
Length of output: 12876
π Web query:
stream-chat-react ChannelChannel disconnected channel.markReadLocally isLocalUnreadCountEnabled
π‘ Result:
In the Stream Chat SDKs, isLocalUnreadCountEnabled and markReadLocally() are features designed primarily for channels where server-side read events are disabled, such as high-traffic livestream channels [1][2]. When isLocalUnreadCountEnabled is set to true during client initialization, the SDK maintains a client-side unread count [1]. Unlike standard unread counts managed by the backend, this count is local to the device and is not synced across other sessions [1]. To reset this local count, you must use channel.markReadLocally() [1]. This method dispatches a client-only event to update the local UI state without triggering a network request to the backend, making it appropriate for scenarios where channel.markRead() (which relies on server-side events) would be a no-op or unnecessary [1]. Regarding the "disconnected" state, recent updates to stream-chat-react have introduced guards to prevent errors when calling methods on a channel after the underlying client has been disconnected [3]. Previously, invoking channel methods (like those involved in state or read-tracking) on a disconnected client could throw errors or cause rendering crashes [4][3][5]. The SDK now includes internal checks (e.g., !channel.disconnected) in various hooks and methods to gracefully handle these situations [3]. If you are encountering issues with disconnected channels, ensure your project is updated to a recent version of the SDK that includes these fixes [3].
Citations:
- 1: https://getstream.io/chat/docs/sdk/react-native/guides/channel-read-state/
- 2: https://getstream.io/chat/docs/sdk/ios/client/livestream-chat/
- 3: fix: bug bashing ChannelList + Channel (#2474, #2441, #2393)Β #3227
- 4: bug: ChannelInner can call methods on a disconnected channel, causing uncaught errorsΒ #2393
- 5: You can't use a channel after client.disconnect() was calledΒ #167
π Script executed:
#!/bin/bash
set -euo pipefail
echo "## Channel.markReadLocally definition/usages in SDK package source if present"
fd -e ts -e d.ts -e tsx | rg 'Channel.*channel|stream-chat|packages|node_modules' | head -120 || true
echo "## Check dependencies and installed stream-chat source"
node - <<'JS'
const fs = require('fs');
const path = require('path');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log('stream-chat:', pkg.dependencies['stream-chat'] || pkg.devDependencies['stream-chat']);
for (const name of fs.readdirSync('node_modules', {withFileTypes: true}).filter(d => d.isDirectory())) {
if (name.startsWith('`@stream-io`') || name === 'stream-chat') {
const files = fs.readdirSync(`node_modules/${name}`, {withFileTypes: true});
const interesting = files.filter(d => d.name === 'package.json');
if (interesting.length) console.log(`node_modules/${name}`, fs.readFileSync(`node_modules/${name}/package.json`, 'utf8').slice(0,500).replace(/\n/g,' '));
}
}
JS
echo "## Inspect PR reference and SDK type docs if network available"
python3 - <<'PY'
import json
from urllib.request import urlopen
for url in [
"https://api.github.com/repos/GetStream/stream-chat-react/pulls/3227",
"https://api.github.com/repos/GetStream/chat/pulls/3252", # likely related stream-chat
]:
try:
r=urlopen(url,timeout=15)
data=json.load(r)
print(f"\n===== {url} =====")
print(data.get("title"))
print(data.get("body","")[:2000].replace("\n"," ") if data.get("body") else "no body")
except Exception as e:
print(f"\n===== {url} error =====")
print(e)
PYRepository: GetStream/stream-chat-react
Length of output: 1245
π Web query:
"markReadLocally" "Channel" "disconnected" "stream-chat" 9.50.2
π‘ Result:
In the Stream Chat SDK (including version 9.50.2), markReadLocally is a method designed to reset the current user's unread message count purely on the client side, without triggering a network request to the backend [1][2]. Key technical details regarding markReadLocally: Purpose: It is specifically intended for use in channels where read events are disabled, such as livestreams, but where the client has been configured with the isLocalUnreadCountEnabled option [1][2]. Mechanism: Instead of sending a server-side read event, markReadLocally dispatches a client-only event called message.read_locally [1][2]. This event is processed by the SDK's internal read logic, allowing the local UI unread count to update immediately without waiting for server confirmation [1][2]. Persistence: If offline support is enabled, the SDK persists the reset state to the offline database, ensuring the local unread count remains consistent across application restarts [1][2]. Usage: You call it directly on the channel instance [1]: channel.markReadLocally; Comparison with markRead: Unlike markRead, which is intended for channels with read events enabled and communicates with the Stream backend to synchronize read state across all devices, markReadLocally is a no-op regarding server synchronization [1][2]. If used on a channel that has read events enabled, it will not inform other devices or the server that the user has read the messages [1]. [1][2]
Citations:
- 1: https://getstream.io/chat/docs/sdk/react-native/guides/channel-read-state/
- 2: https://github.com/GetStream/stream-chat-js/blob/master/src/channel.ts
π Web query:
"channel.markReadLocally" "message.read_locally" "Channel" "disconnected" "stream-chat-react"
π‘ Result:
The channel.markReadLocally method is a client-side function used in the Stream Chat SDK to reset the local unread message count without sending a network request to the backend [1]. When called, it dispatches a client-only message.read_locally event, which updates the UI state (e.g., clearing unread indicators) locally [1]. This method is primarily intended for use in scenarios where server-side read events are disabled or not desired (e.g., to maintain a purely local unread count) [1]. It differs from the standard markRead method, which performs a server-side API call and is typically throttled by the SDK to prevent rate-limiting [2][3][4]. Regarding the context of a "Channel disconnected" state, calling SDK methods on a channel after the chat client has been disconnected can result in errors [5][6][7]. Recent versions of the stream-chat-react library have introduced guards to prevent these methods (such as lastRead and others) from crashing the render or causing unhandled exceptions when the client is disconnected [6]. If you encounter issues while the channel is disconnected, ensure your application logic checks the channel.disconnected property before invoking channel operations [6]. If you are experiencing crashes specifically, ensure you are using a version of the SDK that includes these connection guards [6].
Citations:
- 1: https://getstream.io/chat/docs/sdk/react-native/guides/channel-read-state/
- 2: https://getstream.io/chat/docs/sdk/react/components/contexts/channel_action_context/
- 3: https://getstream.io/chat/docs/sdk/react/guides/channel-read-state/
- 4: https://getstream.io/chat/docs/javascript/unread/
- 5: bug: ChannelInner can call methods on a disconnected channel, causing uncaught errorsΒ #2393
- 6: fix: bug bashing ChannelList + Channel (#2474, #2441, #2393)Β #3227
- 7: You can't use a channel after client.disconnect() was calledΒ #167
π Web query:
github.com GetStream stream-chat-react 3227 Channel.tsx disconnected markReadLocally
π‘ Result:
Pull Request #3227 in the GetStream/stream-chat-react repository is a maintenance and bug-fix release that addresses several stability issues, including those related to the channel state when a client is disconnected [1][2]. Specifically, PR #3227 includes a fix for Issue #2393, which reported that internal methods, such as those related to reading state, could be called on a channel instance after the client had already disconnected [1][3]. Before this fix, calling methods like channel.lastRead during a component render could throw an error (e.g., "You can't use a channel after client.disconnect was called") and crash the application because there were insufficient guards against the disconnected state [1][3]. The fix involves adding checks, such as!channel.disconnected, to guard various channel methods within the SDK's internal components, including those that might trigger mark-read operations or read-state updates [1]. In the context of GetStream SDKs: - markRead is the standard, throttled method for marking messages as read on the backend [4][5][6]. - markReadLocally is a specific method used primarily for channels where read events are disabled (e.g., some livestream channels), allowing for a purely client-side reset of unread counts without triggering network requests [7]. By guarding these operations against the disconnected state, the SDK prevents runtime crashes when the WebSocket connection is severed while a component is still mounted or attempting to update its UI state [1][3]. Top results: [1], [3], [7]
Citations:
- 1: fix: bug bashing ChannelList + Channel (#2474, #2441, #2393)Β #3227
- 2: https://raw.githubusercontent.com/GetStream/stream-chat-react/master/CHANGELOG.md
- 3: bug: ChannelInner can call methods on a disconnected channel, causing uncaught errorsΒ #2393
- 4: https://getstream.io/chat/docs/sdk/react/components/contexts/channel_action_context/
- 5: https://getstream.io/chat/docs/sdk/react/guides/channel-read-state/
- 6: https://getstream.io/chat/docs/react/unread/
- 7: https://getstream.io/chat/docs/sdk/react-native/guides/channel-read-state/
Scope the disconnected guard to server reads.
markReadLocally() processes unread state via a local client event only. Do not return for every disconnected check at line 305; move the channel.disconnected guard into the server channel.markRead() path instead.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Channel/Channel.tsx` around lines 305 - 308, Move the
channel.disconnected early return from the outer flow into the server
channel.markRead() path. Keep channel.markReadLocally() reachable while
disconnected when local unread counts are enabled, while preserving the guard
before any server read operation.
| useEffect(() => { | ||
| const handleEvent = (event: Event) => { | ||
| if (channel.cid !== event.cid) return; | ||
| if (event.user?.id !== client.user?.id) return; | ||
| setUnread(channel.countUnread()); | ||
| }; | ||
|
|
||
| client.on('notification.mark_read', handleEvent); | ||
| channel.on('notification.mark_unread', handleEvent); | ||
| channel.on('message.read_locally', handleEvent); | ||
| return () => { | ||
| client.off('notification.mark_read', handleEvent); | ||
| channel.off('notification.mark_unread', handleEvent); | ||
| channel.off('message.read_locally', handleEvent); |
There was a problem hiding this comment.
π― Functional Correctness | π Major | β‘ Quick win
Preserve CID-less notification.mark_read handling.
When event.cid is absent, Line 122 returns before the preview unread count updates. The supplied test context expects a CID-less mark-read event to clear the unread count. Filter only when event.cid is present.
Proposed fix
- if (channel.cid !== event.cid) return;
+ if (event.cid && channel.cid !== event.cid) return;The supplied test context in src/components/ChannelListItem/__tests__/ChannelListItem.test.tsx:521-575 covers this case.
π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| const handleEvent = (event: Event) => { | |
| if (channel.cid !== event.cid) return; | |
| if (event.user?.id !== client.user?.id) return; | |
| setUnread(channel.countUnread()); | |
| }; | |
| client.on('notification.mark_read', handleEvent); | |
| channel.on('notification.mark_unread', handleEvent); | |
| channel.on('message.read_locally', handleEvent); | |
| return () => { | |
| client.off('notification.mark_read', handleEvent); | |
| channel.off('notification.mark_unread', handleEvent); | |
| channel.off('message.read_locally', handleEvent); | |
| useEffect(() => { | |
| const handleEvent = (event: Event) => { | |
| if (event.cid && channel.cid !== event.cid) return; | |
| if (event.user?.id !== client.user?.id) return; | |
| setUnread(channel.countUnread()); | |
| }; | |
| client.on('notification.mark_read', handleEvent); | |
| channel.on('notification.mark_unread', handleEvent); | |
| channel.on('message.read_locally', handleEvent); | |
| return () => { | |
| client.off('notification.mark_read', handleEvent); | |
| channel.off('notification.mark_unread', handleEvent); | |
| channel.off('message.read_locally', handleEvent); |
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ChannelListItem/ChannelListItem.tsx` around lines 120 - 133,
Update the handleEvent logic in ChannelListItemβs useEffect so it filters by
channel.cid only when event.cid is present, allowing CID-less
notification.mark_read events to update the unread count while preserving
filtering for mismatched provided CIDs.
## [14.11.0](v14.10.0...v14.11.0) (2026-08-07) ### Bug Fixes * **Channel:** guard render-phase channel.getConfig() against disconnected channels ([#3257](#3257)) ([f60273f](f60273f)), closes [#3254](#3254) [#2393](#2393) [#3249](#3249) * **EmojiPicker:** drop @emoji-mart/react peer dependency ([#3255](#3255)) ([0820e4c](0820e4c)) ### Features * add icons to ComponentContext ([#3246](#3246)) ([972b68c](972b68c)) * localized unread count ([#3250](#3250)) ([1b8fa34](1b8fa34)), closes [GetStream/stream-chat-react-native#3679](GetStream/stream-chat-react-native#3679) * **MessageComposer:** introduce context for custom composers ([#3249](#3249)) ([5776c16](5776c16)), closes [#3248](#3248)
|
π This PR is included in version 14.11.0 π The release is available on: Your semantic-release bot π¦π |
π― Goal
Ref: GetStream/stream-chat-react-native#3679
Summary by CodeRabbit