From 95ad7fd7a6b1eb61ec98a040ba1b19191ced59cf Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 25 Jun 2026 16:56:05 +0200 Subject: [PATCH 01/10] feat: localized unread counts --- examples/SampleApp/metro.config.js | 7 ++++- examples/SampleApp/src/hooks/useChatClient.ts | 1 + package.json | 1 + package/src/components/Channel/Channel.tsx | 27 ++++++++++++++++--- .../hooks/useChannelPreviewData.ts | 10 +++++-- yarn.lock | 9 +++---- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/examples/SampleApp/metro.config.js b/examples/SampleApp/metro.config.js index c96816d4be..2c7e5c5410 100644 --- a/examples/SampleApp/metro.config.js +++ b/examples/SampleApp/metro.config.js @@ -69,6 +69,11 @@ const extraNodeModules = uniqueModules.reduce((acc, item) => { acc[item.packageName] = item.modulePath; return acc; }, {}); +// Point `stream-chat` at the local checkout that is portal-linked via the root package.json +// `resolutions`. We set it after the reduce (rather than seeding the reduce's initial value) +// because stream-chat is a direct dependency of this app, so `uniqueModules` would otherwise +// overwrite the seed. +extraNodeModules['stream-chat'] = '/Users/isekovanic/Projects/stream-chat-js'; config.resolver.blockList = exclusionList(blockList); config.resolver.extraNodeModules = extraNodeModules; @@ -76,6 +81,6 @@ config.resolver.extraNodeModules = extraNodeModules; config.resolver.nodeModulesPaths = [PATH.resolve(__dirname, 'node_modules')]; // add the package dir for metro to access the package folder -config.watchFolders = [packageDirPath]; +config.watchFolders = [packageDirPath, '/Users/isekovanic/Projects/stream-chat-js']; module.exports = config; diff --git a/examples/SampleApp/src/hooks/useChatClient.ts b/examples/SampleApp/src/hooks/useChatClient.ts index 1ced3e1d4e..58b0b22e5a 100644 --- a/examples/SampleApp/src/hooks/useChatClient.ts +++ b/examples/SampleApp/src/hooks/useChatClient.ts @@ -74,6 +74,7 @@ export const useChatClient = () => { unsubscribePushListenersRef.current?.(); const client = StreamChat.getInstance(config.apiKey, { timeout: 6000, + enableLocalUnreadCount: true, // TEST: localized unread count — revert before committing // logger: (type, msg) => console.log(type, msg) }); setChatClient(client); diff --git a/package.json b/package.json index a3e8cef113..a0b48341e0 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "examples/TypeScriptMessaging" ], "resolutions": { + "stream-chat": "portal:/Users/isekovanic/Projects/stream-chat-js", "@types/react": "^19.2.0", "@shopify/flash-list": "patch:@shopify/flash-list@npm%3A2.3.1#~/.yarn/patches/@shopify-flash-list-npm-2.3.1-8b5fd40241.patch" }, diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 52070c39b8..b9b39a1856 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -515,8 +515,8 @@ const ChannelWithContext = (props: PropsWithChildren) = const [thread, setThread] = useState(threadProps || null); const [threadHasMore, setThreadHasMore] = useState(true); const [threadLoadingMore, setThreadLoadingMore] = useState(false); - const [channelUnreadStateStore] = useState(new ChannelUnreadStateStore()); - const [messageInputHeightStore] = useState(new MessageInputHeightStore()); + const [channelUnreadStateStore] = useState(() => new ChannelUnreadStateStore()); + const [messageInputHeightStore] = useState(() => new MessageInputHeightStore()); // TODO: Think if we can remove this and just rely on the channelUnreadStateStore everywhere. const setChannelUnreadState = useCallback( (data: ChannelUnreadStateStoreType['channelUnreadState']) => { @@ -690,7 +690,15 @@ const ChannelWithContext = (props: PropsWithChildren) = return; } + if (event.type === 'message.local_read') { + // Local unread reset (read events disabled, e.g. livestreams): the count is already updated + // in the client state, and the preview badge / unread divider are handled elsewhere, so + // there is nothing to copy into channel state here. + return; + } + if (event.type === 'message.read' || event.type === 'notification.mark_read') { + console.log('READ EVENT MENTION ?!', event); setReadThrottled(); return; } @@ -812,7 +820,20 @@ const ChannelWithContext = (props: PropsWithChildren) = const markReadInternal: ChannelContextValue['markRead'] = throttle( async (options?: MarkReadFunctionOptions) => { const { updateChannelUnreadState = true } = options ?? {}; - if (!channel || channel?.disconnected || !clientChannelConfig?.read_events) { + if (!channel || channel?.disconnected) { + return; + } + + // When read events are disabled (e.g. livestreams) we cannot mark read on the backend. If the + // client opted into a local unread count, reset it locally instead so the user's "caught up" + // state is reflected without a server round-trip. + if (!clientChannelConfig?.read_events) { + if (client.options.enableLocalUnreadCount) { + channel.markReadLocally(); + if (updateChannelUnreadState) { + setChannelUnreadState({ last_read: new Date(), unread_messages: 0 }); + } + } return; } diff --git a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts index bd81574bbb..9e31ef943f 100644 --- a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts +++ b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts @@ -109,8 +109,14 @@ export const useChannelPreviewData = ( setForceUpdate((prev) => prev + 1); } }; - const { unsubscribe } = client.on('message.read', handleReadEvent); - return unsubscribe; + const readSubscription = client.on('message.read', handleReadEvent); + // `message.local_read` is the client-only equivalent emitted by `channel.markReadLocally()` when + // read events are disabled (e.g. livestreams with `enableLocalUnreadCount`). + const localReadSubscription = client.on('message.local_read', handleReadEvent); + return () => { + readSubscription.unsubscribe(); + localReadSubscription.unsubscribe(); + }; }, [client, channel]); /** diff --git a/yarn.lock b/yarn.lock index 81e7be7a08..6473c2e4b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19683,9 +19683,9 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@npm:^9.47.1": - version: 9.47.1 - resolution: "stream-chat@npm:9.47.1" +"stream-chat@portal:/Users/isekovanic/Projects/stream-chat-js::locator=root%40workspace%3A.": + version: 0.0.0-use.local + resolution: "stream-chat@portal:/Users/isekovanic/Projects/stream-chat-js::locator=root%40workspace%3A." dependencies: "@types/jsonwebtoken": "npm:^9.0.8" "@types/ws": "npm:^8.18.1" @@ -19701,9 +19701,8 @@ __metadata: built: true husky: built: true - checksum: 10c0/32578f59b8b439454b4a23038417477a16b74ba5677245346d5b0f1cf61139e9b4067e83a77196e1dd661ae2207842df30b345bf4f570872ce9b0cc4463f8eb7 languageName: node - linkType: hard + linkType: soft "stream-combiner2@npm:~1.1.1": version: 1.1.1 From 705c57792e039da71c19b02d86d2ba8d5b7f40d2 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 26 Jun 2026 15:07:06 +0200 Subject: [PATCH 02/10] fix: unread messages banner for local unreads --- package/src/components/MessageList/MessageFlashList.tsx | 6 +++++- package/src/components/MessageList/MessageList.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 55dd181241..8417f95b98 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -660,9 +660,13 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => const lastReadMessageId = channelUnreadState?.last_read_message_id; const lastReadMessageVisible = viewableItems.some((item) => item.item.id === lastReadMessageId); + // Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the + // client opted into a local unread count, so the gate accepts either source. + const unreadNotificationSupported = readEvents || client.options.enableLocalUnreadCount; + if ( !viewableItems.length || - !readEvents || + !unreadNotificationSupported || lastReadMessageVisible || attachmentPickerStore.state.getLatestValue().selectedPicker === 'images' ) { diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index ffb77698ef..ca577f4fce 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -550,9 +550,13 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { (item) => item.item.message.id === lastReadMessageId, ); + // Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the + // client opted into a local unread count, so the gate accepts either source. + const unreadNotificationSupported = readEvents || client.options.enableLocalUnreadCount; + if ( !viewableItems.length || - !readEvents || + !unreadNotificationSupported || lastReadMessageVisible || attachmentPickerStore.state.getLatestValue().selectedPicker === 'images' ) { From c05b4b826fe8a3752862d4db049e8e5e67b7ed55 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 26 Jun 2026 15:13:49 +0200 Subject: [PATCH 03/10] fix: rename enable flag --- examples/SampleApp/src/hooks/useChatClient.ts | 2 +- package/src/components/Channel/Channel.tsx | 2 +- .../components/ChannelPreview/hooks/useChannelPreviewData.ts | 2 +- package/src/components/MessageList/MessageFlashList.tsx | 2 +- package/src/components/MessageList/MessageList.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/SampleApp/src/hooks/useChatClient.ts b/examples/SampleApp/src/hooks/useChatClient.ts index 58b0b22e5a..c33bef5088 100644 --- a/examples/SampleApp/src/hooks/useChatClient.ts +++ b/examples/SampleApp/src/hooks/useChatClient.ts @@ -74,7 +74,7 @@ export const useChatClient = () => { unsubscribePushListenersRef.current?.(); const client = StreamChat.getInstance(config.apiKey, { timeout: 6000, - enableLocalUnreadCount: true, // TEST: localized unread count — revert before committing + isLocalUnreadCountEnabled: true, // TEST: localized unread count — revert before committing // logger: (type, msg) => console.log(type, msg) }); setChatClient(client); diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index b9b39a1856..6d57283ddd 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -828,7 +828,7 @@ const ChannelWithContext = (props: PropsWithChildren) = // client opted into a local unread count, reset it locally instead so the user's "caught up" // state is reflected without a server round-trip. if (!clientChannelConfig?.read_events) { - if (client.options.enableLocalUnreadCount) { + if (client.options.isLocalUnreadCountEnabled) { channel.markReadLocally(); if (updateChannelUnreadState) { setChannelUnreadState({ last_read: new Date(), unread_messages: 0 }); diff --git a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts index 9e31ef943f..050a3c9b2d 100644 --- a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts +++ b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts @@ -111,7 +111,7 @@ export const useChannelPreviewData = ( }; const readSubscription = client.on('message.read', handleReadEvent); // `message.local_read` is the client-only equivalent emitted by `channel.markReadLocally()` when - // read events are disabled (e.g. livestreams with `enableLocalUnreadCount`). + // read events are disabled (e.g. livestreams with `isLocalUnreadCountEnabled`). const localReadSubscription = client.on('message.local_read', handleReadEvent); return () => { readSubscription.unsubscribe(); diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 8417f95b98..cbc26f09d9 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -662,7 +662,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => // Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the // client opted into a local unread count, so the gate accepts either source. - const unreadNotificationSupported = readEvents || client.options.enableLocalUnreadCount; + const unreadNotificationSupported = readEvents || client.options.isLocalUnreadCountEnabled; if ( !viewableItems.length || diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index ca577f4fce..5ee1382dbf 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -552,7 +552,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { // Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the // client opted into a local unread count, so the gate accepts either source. - const unreadNotificationSupported = readEvents || client.options.enableLocalUnreadCount; + const unreadNotificationSupported = readEvents || client.options.isLocalUnreadCountEnabled; if ( !viewableItems.length || From 745466ef3ba1968392a0af806c489956e5ea6db9 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 30 Jun 2026 11:08:40 +0200 Subject: [PATCH 04/10] perf: remove lastRead --- package/src/components/Channel/Channel.tsx | 12 +++++------- .../Channel/hooks/useCreateChannelContext.ts | 6 ------ .../src/components/Thread/__tests__/Thread.test.tsx | 8 +++++--- .../src/contexts/channelContext/ChannelContext.tsx | 3 --- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 6d57283ddd..943d689735 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -511,7 +511,7 @@ const ChannelWithContext = (props: PropsWithChildren) = const styles = useStyles(); const [deleted, setDeleted] = useState(false); const [error, setError] = useState(false); - const [lastRead, setLastRead] = useState(); + const lastReadRef = useRef(undefined); const [thread, setThread] = useState(threadProps || null); const [threadHasMore, setThreadHasMore] = useState(true); const [threadLoadingMore, setThreadLoadingMore] = useState(false); @@ -711,7 +711,7 @@ const ChannelWithContext = (props: PropsWithChildren) = useEffect(() => { let listener: ReturnType; const initChannel = async () => { - setLastRead(new Date()); + lastReadRef.current = new Date(); const unreadCount = channel.countUnread(); const shouldLoadAtFirstUnread = shouldLoadInitialChannelAtFirstUnreadMessage(unreadCount); if (!channel || !shouldSyncChannel) { @@ -842,13 +842,13 @@ const ChannelWithContext = (props: PropsWithChildren) = } else { try { const response = await channel.markRead(); - if (updateChannelUnreadState && response && lastRead) { + if (updateChannelUnreadState && response && lastReadRef.current) { setChannelUnreadState({ - last_read: lastRead, + last_read: lastReadRef.current, last_read_message_id: response?.event.last_read_message_id, unread_messages: 0, }); - setLastRead(new Date()); + lastReadRef.current = new Date(); } } catch (err) { console.log('Error marking channel as read:', err); @@ -1599,7 +1599,6 @@ const ChannelWithContext = (props: PropsWithChildren) = hideStickyDateHeader, highlightedMessageId, isChannelActive: shouldSyncChannel, - lastRead, loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading: channelMessagesState.loading, @@ -1611,7 +1610,6 @@ const ChannelWithContext = (props: PropsWithChildren) = reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, - setLastRead, setTargetedMessage, hasPendingInitialTargetLoad, targetedMessage, diff --git a/package/src/components/Channel/hooks/useCreateChannelContext.ts b/package/src/components/Channel/hooks/useCreateChannelContext.ts index 8e8870707c..67a495449b 100644 --- a/package/src/components/Channel/hooks/useCreateChannelContext.ts +++ b/package/src/components/Channel/hooks/useCreateChannelContext.ts @@ -13,7 +13,6 @@ export const useCreateChannelContext = ({ hideStickyDateHeader, highlightedMessageId, isChannelActive, - lastRead, loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading, @@ -25,7 +24,6 @@ export const useCreateChannelContext = ({ reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, - setLastRead, setTargetedMessage, hasPendingInitialTargetLoad, targetedMessage, @@ -35,7 +33,6 @@ export const useCreateChannelContext = ({ watchers, }: ChannelContextValue) => { const channelId = channel?.id; - const lastReadTime = lastRead?.getTime(); const membersLength = Object.keys(members).length; const readUsers = Object.values(read); @@ -56,7 +53,6 @@ export const useCreateChannelContext = ({ hideStickyDateHeader, highlightedMessageId, isChannelActive, - lastRead, loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading, @@ -68,7 +64,6 @@ export const useCreateChannelContext = ({ reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, - setLastRead, setTargetedMessage, hasPendingInitialTargetLoad, targetedMessage, @@ -84,7 +79,6 @@ export const useCreateChannelContext = ({ error, isChannelActive, highlightedMessageId, - lastReadTime, loading, membersLength, readUsersLength, diff --git a/package/src/components/Thread/__tests__/Thread.test.tsx b/package/src/components/Thread/__tests__/Thread.test.tsx index 72577b01d1..56aad0705c 100644 --- a/package/src/components/Thread/__tests__/Thread.test.tsx +++ b/package/src/components/Thread/__tests__/Thread.test.tsx @@ -142,7 +142,9 @@ describe('Thread', () => { threadResponses as unknown as Parameters[0], ); - let setLastRead: ((date?: Date) => void) | undefined; + let setChannelUnreadState: + | React.ContextType['setChannelUnreadState'] + | undefined; const { getByText, toJSON } = render( @@ -161,7 +163,7 @@ describe('Thread', () => { {(c) => { - setLastRead = c.setLastRead; + setChannelUnreadState = c.setChannelUnreadState; return ; }} @@ -178,7 +180,7 @@ describe('Thread', () => { expect(getByText('Message6')).toBeTruthy(); }); - act(() => setLastRead!(new Date('2020-08-17T18:08:03.196Z'))); + act(() => setChannelUnreadState!(undefined)); const snapshot = toJSON() as unknown as { children: Array<{ diff --git a/package/src/contexts/channelContext/ChannelContext.tsx b/package/src/contexts/channelContext/ChannelContext.tsx index a14119ac0e..e9de901882 100644 --- a/package/src/contexts/channelContext/ChannelContext.tsx +++ b/package/src/contexts/channelContext/ChannelContext.tsx @@ -110,7 +110,6 @@ export type ChannelContextValue = { reloadChannel: () => Promise; scrollToFirstUnreadThreshold: number; setChannelUnreadState: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void; - setLastRead: React.Dispatch>; setTargetedMessage: (messageId?: string) => void; /** * Returns true when Channel is about to load an initial targeted message. @@ -131,8 +130,6 @@ export type ChannelContextValue = { */ highlightedMessageId?: string; isChannelActive?: boolean; - - lastRead?: Date; loading?: boolean; /** * Maximum time in milliseconds that should occur between messages From da70035c9da502de235cd4f154fdd7847359a9d0 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 30 Jun 2026 13:05:57 +0200 Subject: [PATCH 05/10] fix: respect local read event data --- package/src/components/Channel/Channel.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 943d689735..ad72f8a8ca 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -826,12 +826,17 @@ const ChannelWithContext = (props: PropsWithChildren) = // When read events are disabled (e.g. livestreams) we cannot mark read on the backend. If the // client opted into a local unread count, reset it locally instead so the user's "caught up" - // state is reflected without a server round-trip. + // state is reflected without a server round trip. if (!clientChannelConfig?.read_events) { if (client.options.isLocalUnreadCountEnabled) { - channel.markReadLocally(); - if (updateChannelUnreadState) { - setChannelUnreadState({ last_read: new Date(), unread_messages: 0 }); + const event = channel.markReadLocally(); + if (updateChannelUnreadState && event && lastReadRef.current) { + setChannelUnreadState({ + last_read: lastReadRef.current, + last_read_message_id: event.last_read_message_id, + unread_messages: 0, + }); + lastReadRef.current = new Date(); } } return; From 64f19625aa596e7f07f985b9fc559418caef7de9 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 30 Jun 2026 15:01:31 +0200 Subject: [PATCH 06/10] chore: rename event --- package/src/components/Channel/Channel.tsx | 2 +- .../components/ChannelPreview/hooks/useChannelPreviewData.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index ad72f8a8ca..7b9941bf14 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -690,7 +690,7 @@ const ChannelWithContext = (props: PropsWithChildren) = return; } - if (event.type === 'message.local_read') { + if (event.type === 'message.read_locally') { // Local unread reset (read events disabled, e.g. livestreams): the count is already updated // in the client state, and the preview badge / unread divider are handled elsewhere, so // there is nothing to copy into channel state here. diff --git a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts index 050a3c9b2d..da37a0b050 100644 --- a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts +++ b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts @@ -110,9 +110,9 @@ export const useChannelPreviewData = ( } }; const readSubscription = client.on('message.read', handleReadEvent); - // `message.local_read` is the client-only equivalent emitted by `channel.markReadLocally()` when + // `message.read_locally` is the client-only equivalent emitted by `channel.markReadLocally()` when // read events are disabled (e.g. livestreams with `isLocalUnreadCountEnabled`). - const localReadSubscription = client.on('message.local_read', handleReadEvent); + const localReadSubscription = client.on('message.read_locally', handleReadEvent); return () => { readSubscription.unsubscribe(); localReadSubscription.unsubscribe(); From df1bf1d287b227aa4b9d7a7801a469591ae35715 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 3 Jul 2026 14:27:47 +0200 Subject: [PATCH 07/10] chore: bump stream-chat-js --- examples/ExpoMessaging/package.json | 2 +- examples/SampleApp/metro.config.js | 7 +------ examples/SampleApp/package.json | 2 +- examples/TypeScriptMessaging/package.json | 2 +- package.json | 1 - package/package.json | 2 +- yarn.lock | 17 +++++++++-------- 7 files changed, 14 insertions(+), 19 deletions(-) diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 2bdfb41a19..dcdfd2e3b8 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -51,7 +51,7 @@ "react-native-teleport": "^1.0.2", "react-native-web": "^0.21.0", "react-native-worklets": "0.8.3", - "stream-chat": "^9.47.1", + "stream-chat": "^9.50.0", "stream-chat-expo": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/examples/SampleApp/metro.config.js b/examples/SampleApp/metro.config.js index 2c7e5c5410..c96816d4be 100644 --- a/examples/SampleApp/metro.config.js +++ b/examples/SampleApp/metro.config.js @@ -69,11 +69,6 @@ const extraNodeModules = uniqueModules.reduce((acc, item) => { acc[item.packageName] = item.modulePath; return acc; }, {}); -// Point `stream-chat` at the local checkout that is portal-linked via the root package.json -// `resolutions`. We set it after the reduce (rather than seeding the reduce's initial value) -// because stream-chat is a direct dependency of this app, so `uniqueModules` would otherwise -// overwrite the seed. -extraNodeModules['stream-chat'] = '/Users/isekovanic/Projects/stream-chat-js'; config.resolver.blockList = exclusionList(blockList); config.resolver.extraNodeModules = extraNodeModules; @@ -81,6 +76,6 @@ config.resolver.extraNodeModules = extraNodeModules; config.resolver.nodeModulesPaths = [PATH.resolve(__dirname, 'node_modules')]; // add the package dir for metro to access the package folder -config.watchFolders = [packageDirPath, '/Users/isekovanic/Projects/stream-chat-js']; +config.watchFolders = [packageDirPath]; module.exports = config; diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 75320bdca9..0110437fa8 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -64,7 +64,7 @@ "react-native-teleport": "^1.1.7", "react-native-video": "^6.19.2", "react-native-worklets": "^0.8.3", - "stream-chat": "^9.47.1", + "stream-chat": "^9.50.0", "stream-chat-react-native": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/examples/TypeScriptMessaging/package.json b/examples/TypeScriptMessaging/package.json index bc98950c84..abdf714598 100644 --- a/examples/TypeScriptMessaging/package.json +++ b/examples/TypeScriptMessaging/package.json @@ -33,7 +33,7 @@ "react-native-svg": "^15.12.0", "react-native-video": "^6.16.1", "react-native-worklets": "^0.4.1", - "stream-chat": "^9.47.1", + "stream-chat": "^9.50.0", "stream-chat-react-native": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/package.json b/package.json index a0b48341e0..a3e8cef113 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "examples/TypeScriptMessaging" ], "resolutions": { - "stream-chat": "portal:/Users/isekovanic/Projects/stream-chat-js", "@types/react": "^19.2.0", "@shopify/flash-list": "patch:@shopify/flash-list@npm%3A2.3.1#~/.yarn/patches/@shopify-flash-list-npm-2.3.1-8b5fd40241.patch" }, diff --git a/package/package.json b/package/package.json index 006678bb03..b0111dd906 100644 --- a/package/package.json +++ b/package/package.json @@ -78,7 +78,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^2.0.0", - "stream-chat": "^9.47.1", + "stream-chat": "^9.50.0", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 6473c2e4b9..14410c95fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7442,7 +7442,7 @@ __metadata: react-native-teleport: "npm:^1.0.2" react-native-web: "npm:^0.21.0" react-native-worklets: "npm:0.8.3" - stream-chat: "npm:^9.47.1" + stream-chat: "npm:^9.50.0" stream-chat-expo: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:~5.9.2" @@ -18884,7 +18884,7 @@ __metadata: react-native-teleport: "npm:^1.1.7" react-native-video: "npm:^6.19.2" react-native-worklets: "npm:^0.8.3" - stream-chat: "npm:^9.47.1" + stream-chat: "npm:^9.50.0" stream-chat-react-native: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:5.9.3" @@ -19611,7 +19611,7 @@ __metadata: react-native-worklets: "npm:^0.9.1" react-test-renderer: "npm:19.2.3" rimraf: "npm:^6.0.1" - stream-chat: "npm:^9.47.1" + stream-chat: "npm:^9.50.0" typescript: "npm:5.9.3" use-sync-external-store: "npm:^1.5.0" uuid: "npm:^11.1.0" @@ -19683,9 +19683,9 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@portal:/Users/isekovanic/Projects/stream-chat-js::locator=root%40workspace%3A.": - version: 0.0.0-use.local - resolution: "stream-chat@portal:/Users/isekovanic/Projects/stream-chat-js::locator=root%40workspace%3A." +"stream-chat@npm:^9.50.0": + version: 9.50.0 + resolution: "stream-chat@npm:9.50.0" dependencies: "@types/jsonwebtoken": "npm:^9.0.8" "@types/ws": "npm:^8.18.1" @@ -19701,8 +19701,9 @@ __metadata: built: true husky: built: true + checksum: 10c0/be5941d74946bd1a1371192dd71933a991fd4d8dd070975ee47dca31f938a9c604b6590af5639932f433735907e76a03a7d3519df23c0807f55ef189b21e0c1d languageName: node - linkType: soft + linkType: hard "stream-combiner2@npm:~1.1.1": version: 1.1.1 @@ -20535,7 +20536,7 @@ __metadata: react-native-svg: "npm:^15.12.0" react-native-video: "npm:^6.16.1" react-native-worklets: "npm:^0.4.1" - stream-chat: "npm:^9.47.1" + stream-chat: "npm:^9.50.0" stream-chat-react-native: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:5.9.3" From 6e3b27ca4f860d96f5fb2d90c4daa383bd781a8c Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 3 Jul 2026 14:30:10 +0200 Subject: [PATCH 08/10] chore: remove temp enabling of localized reads --- examples/SampleApp/src/hooks/useChatClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/SampleApp/src/hooks/useChatClient.ts b/examples/SampleApp/src/hooks/useChatClient.ts index c33bef5088..1ced3e1d4e 100644 --- a/examples/SampleApp/src/hooks/useChatClient.ts +++ b/examples/SampleApp/src/hooks/useChatClient.ts @@ -74,7 +74,6 @@ export const useChatClient = () => { unsubscribePushListenersRef.current?.(); const client = StreamChat.getInstance(config.apiKey, { timeout: 6000, - isLocalUnreadCountEnabled: true, // TEST: localized unread count — revert before committing // logger: (type, msg) => console.log(type, msg) }); setChatClient(client); From 314a31ab3a41609a3faf95a11da3f81273c03705 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 3 Jul 2026 16:29:41 +0200 Subject: [PATCH 09/10] chore: remove log --- package/src/components/Channel/Channel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 7b9941bf14..547007af7f 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -698,7 +698,6 @@ const ChannelWithContext = (props: PropsWithChildren) = } if (event.type === 'message.read' || event.type === 'notification.mark_read') { - console.log('READ EVENT MENTION ?!', event); setReadThrottled(); return; } From 897fa862b92ec09baefad863e71fdbb6eadbeb6a Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 3 Jul 2026 16:33:57 +0200 Subject: [PATCH 10/10] chore: udpate comments --- package/src/components/Channel/Channel.tsx | 6 +++--- package/src/components/MessageList/MessageFlashList.tsx | 5 +++-- package/src/components/MessageList/MessageList.tsx | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 547007af7f..f972ba4fb1 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -691,9 +691,9 @@ const ChannelWithContext = (props: PropsWithChildren) = } if (event.type === 'message.read_locally') { - // Local unread reset (read events disabled, e.g. livestreams): the count is already updated - // in the client state, and the preview badge / unread divider are handled elsewhere, so - // there is nothing to copy into channel state here. + // When local unread reset happens, the count is already updated in the client state, + // and the preview badge / unread divider are handled elsewhere, so there is nothing + // to copy into channel state here. Thus, we skip it. return; } diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index c669108999..2fd8404012 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -718,8 +718,9 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => const lastReadMessageId = channelUnreadState?.last_read_message_id; const lastReadMessageVisible = viewableItems.some((item) => item.item.id === lastReadMessageId); - // Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the - // client opted into a local unread count, so the gate accepts either source. + // Channels with disabled `read-events` (i.e livestreams) still surface the unread + // notification when the client opted into a local unread count, so the gate accepts + // either source. const unreadNotificationSupported = readEvents || client.options.isLocalUnreadCountEnabled; if ( diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 15e7302b2d..2ef08ac23b 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -555,8 +555,9 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { (item) => item.item.message.id === lastReadMessageId, ); - // Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the - // client opted into a local unread count, so the gate accepts either source. + // Channels with disabled `read-events` (i.e livestreams) still surface the unread + // notification when the client opted into a local unread count, so the gate accepts + // either source. const unreadNotificationSupported = readEvents || client.options.isLocalUnreadCountEnabled; if (