-
Notifications
You must be signed in to change notification settings - Fork 299
feat: localized unread count #3250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -302,21 +302,30 @@ const ChannelInner = ( | |
| throttle( | ||
| async (options?: MarkReadWrapperOptions) => { | ||
| const { updateChannelUiUnreadState = true } = options ?? {}; | ||
| if (channel.disconnected || !channelConfig?.read_events) { | ||
| return; | ||
| } | ||
|
|
||
| lastRead.current = new Date(); | ||
| if (channel.disconnected) return; | ||
|
|
||
| if (!channelConfig?.read_events && client.options.isLocalUnreadCountEnabled) { | ||
| const event = channel.markReadLocally(); | ||
|
|
||
| if (updateChannelUiUnreadState && event) { | ||
| lastRead.current = new Date(); | ||
| _setChannelUnreadUiState({ | ||
| last_read: lastRead.current, | ||
| last_read_message_id: event.last_read_message_id, | ||
| unread_messages: 0, | ||
| }); | ||
| } | ||
| } else if (channelConfig?.read_events) { | ||
| lastRead.current = new Date(); | ||
|
|
||
| try { | ||
| if (doMarkReadRequest) { | ||
| doMarkReadRequest( | ||
| channel, | ||
| updateChannelUiUnreadState ? setChannelUnreadUiState : undefined, | ||
| ); | ||
| } else { | ||
| const markReadResponse = await channel.markRead(); | ||
| // markReadResponse.event can be null in case of a user that is not a member of a channel being marked read | ||
| // markReadResponse.event can be null in case of a user that is not a member of a channel being marked read | ||
| // in that case event is null and we should not set unread UI | ||
| if (updateChannelUiUnreadState && markReadResponse?.event) { | ||
| _setChannelUnreadUiState({ | ||
|
|
@@ -326,14 +335,12 @@ const ChannelInner = ( | |
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (activeUnreadHandler) { | ||
| activeUnreadHandler(0, originalTitle.current); | ||
| } else if (originalTitle.current) { | ||
| document.title = originalTitle.current; | ||
| } | ||
| } 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; | ||
| } | ||
|
Comment on lines
-335
to
337
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got rid of this |
||
| }, | ||
| 500, | ||
|
|
@@ -343,9 +350,9 @@ const ChannelInner = ( | |
| activeUnreadHandler, | ||
| channel, | ||
| channelConfig, | ||
| client, | ||
| doMarkReadRequest, | ||
| setChannelUnreadUiState, | ||
| t, | ||
| ], | ||
| ); | ||
|
|
||
|
|
@@ -381,7 +388,7 @@ const ChannelInner = ( | |
| if (mainChannelUpdated) { | ||
| if ( | ||
| document.hidden && | ||
| channelConfig?.read_events && | ||
| (channelConfig?.read_events || client.options.isLocalUnreadCountEnabled) && | ||
| !channel.muteStatus().muted | ||
| ) { | ||
| const unread = channel.countUnread(lastRead.current); | ||
|
|
@@ -424,6 +431,10 @@ const ChannelInner = ( | |
| }); | ||
| } | ||
|
|
||
| if (event.type === 'message.read_locally') { | ||
| return; | ||
| } | ||
|
|
||
| if (event.type === 'notification.mark_unread') | ||
| _setChannelUnreadUiState((prev) => { | ||
| if (!(event.last_read_at && event.user)) return prev; | ||
|
|
@@ -490,7 +501,11 @@ const ChannelInner = ( | |
| if (client.user?.id && channel.state.read[client.user.id]) { | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const { user, ...ownReadState } = channel.state.read[client.user.id]; | ||
| _setChannelUnreadUiState(ownReadState); | ||
| _setChannelUnreadUiState((existingState) => { | ||
| // only set the initial state here, do not override existing | ||
| if (existingState) return existingState; | ||
| return ownReadState; | ||
| }); | ||
| } | ||
| /** | ||
| * TODO: maybe pass last_read to the countUnread method to get proper value | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -117,25 +117,20 @@ export const ChannelListItem = (props: ChannelListItemProps) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof active === 'undefined' ? activeChannel?.cid === channel.cid : active; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { muted } = useIsChannelMuted(channel); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleEvent = (event: Event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!event.cid) return setUnread(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (channel.cid === event.cid) setUnread(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.on('notification.mark_read', handleEvent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => client.off('notification.mark_read', handleEvent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [channel, client]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
120
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Preserve CID-less When Proposed fix- if (channel.cid !== event.cid) return;
+ if (event.cid && channel.cid !== event.cid) return;The supplied test context in 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [channel, client]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: GetStream/stream-chat-react
Length of output: 12876
🌐 Web query:
stream-chat-react ChannelChannel disconnected channel.markReadLocally isLocalUnreadCountEnabled💡 Result:
In the Stream Chat SDKs,
isLocalUnreadCountEnabledandmarkReadLocally()are features designed primarily for channels where server-side read events are disabled, such as high-traffic livestream channels [1][2]. WhenisLocalUnreadCountEnabledis set totrueduring 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 usechannel.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 wherechannel.markRead()(which relies on server-side events) would be a no-op or unnecessary [1]. Regarding the "disconnected" state, recent updates tostream-chat-reacthave 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:
🏁 Script executed:
Repository: 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:
🌐 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:
🌐 Web query:
github.com GetStream stream-chat-react 3227 Channel.tsx disconnected markReadLocally💡 Result:
Pull Request
#3227in 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#3227includes 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:
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 thechannel.disconnectedguard into the serverchannel.markRead()path instead.🤖 Prompt for AI Agents