From 0ba18d3ab9beeb8db1e64b6f45abf93d17be1397 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 10 Apr 2025 15:25:29 +0200 Subject: [PATCH 1/6] perf: throttle message copying updates --- package/expo-package/yarn.lock | 8 +++--- package/native-package/yarn.lock | 8 +++--- package/src/components/Channel/Channel.tsx | 31 +++++++++++++++++----- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/package/expo-package/yarn.lock b/package/expo-package/yarn.lock index aeb2acdeea..1f25f07e3f 100644 --- a/package/expo-package/yarn.lock +++ b/package/expo-package/yarn.lock @@ -4733,10 +4733,10 @@ stream-buffers@2.2.x, stream-buffers@~2.2.0: resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg== -stream-chat-react-native-core@6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-6.7.1.tgz#37cdfbc5c7f8a5bac5634b954da4bbdcd2fb95f2" - integrity sha512-4ePEMt1W+iw3zUulSDRFO9Nt4HPa8kW6wJ3Qv+ZN+y886rvcUuTuH18MFrdrJtHYb+UxCU+X3oz++3qzq7Jzxw== +stream-chat-react-native-core@6.7.2: + version "6.7.2" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-6.7.2.tgz#955e048b80c55175db084ccbb8519f52ef4bb00c" + integrity sha512-WJFOCfQ7Xpn8Lr4AE6hUh4Qhrn1eGzsoAcKmL8eSoB/etxdNllOyZ3zrwvZgyy+KIEg9bcX4y+3OWtdKW6qfsA== dependencies: "@gorhom/bottom-sheet" "^5.1.1" dayjs "1.10.5" diff --git a/package/native-package/yarn.lock b/package/native-package/yarn.lock index 317331eb77..e586735c0f 100644 --- a/package/native-package/yarn.lock +++ b/package/native-package/yarn.lock @@ -3409,10 +3409,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react-native-core@6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-6.7.1.tgz#37cdfbc5c7f8a5bac5634b954da4bbdcd2fb95f2" - integrity sha512-4ePEMt1W+iw3zUulSDRFO9Nt4HPa8kW6wJ3Qv+ZN+y886rvcUuTuH18MFrdrJtHYb+UxCU+X3oz++3qzq7Jzxw== +stream-chat-react-native-core@6.7.2: + version "6.7.2" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-6.7.2.tgz#955e048b80c55175db084ccbb8519f52ef4bb00c" + integrity sha512-WJFOCfQ7Xpn8Lr4AE6hUh4Qhrn1eGzsoAcKmL8eSoB/etxdNllOyZ3zrwvZgyy+KIEg9bcX4y+3OWtdKW6qfsA== dependencies: "@gorhom/bottom-sheet" "^5.1.1" dayjs "1.10.5" diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index ae5ca45407..b71401c7a0 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -745,10 +745,20 @@ const ChannelWithContext = < * Since we copy the current channel state all together, we need to find the greatest time among the below two and apply it as the throttling time for copying the channel state. * This is done until we remove the newMessageStateUpdateThrottleInterval prop. */ - const copyChannelStateThrottlingTime = - newMessageStateUpdateThrottleInterval > stateUpdateThrottleInterval - ? newMessageStateUpdateThrottleInterval - : stateUpdateThrottleInterval; + + const copyMsgsStateFromChannel = useMemo( + () => + throttle( + () => { + if (channel) { + copyMessagesStateFromChannel(channel); + } + }, + newMessageStateUpdateThrottleInterval, + throttleOptions, + ), + [channel, newMessageStateUpdateThrottleInterval, copyMessagesStateFromChannel], + ); const copyChannelState = useMemo( () => @@ -759,10 +769,10 @@ const ChannelWithContext = < copyMessagesStateFromChannel(channel); } }, - copyChannelStateThrottlingTime, + stateUpdateThrottleInterval, throttleOptions, ), - [channel, copyChannelStateThrottlingTime, copyMessagesStateFromChannel, copyStateFromChannel], + [stateUpdateThrottleInterval, channel, copyStateFromChannel, copyMessagesStateFromChannel], ); const handleEvent: EventHandler = (event) => { @@ -819,6 +829,15 @@ const ChannelWithContext = < // only update channel state if the events are not the previously subscribed useEffect's subscription events if (channel && channel.initialized) { + if (event.type === 'message.new') { + copyMsgsStateFromChannel(); + return; + } + + if (event.type === 'message.read') { + return; + } + copyChannelState(); } } From 96683348f8fed97aeff4cba0d8b9e943d098f2bc Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 10 Apr 2025 15:36:32 +0200 Subject: [PATCH 2/6] fix: throttle all other msg copies --- package/src/components/Channel/Channel.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index b71401c7a0..df44138bf9 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -1190,7 +1190,7 @@ const ChannelWithContext = < } channel.state.addMessageSorted(updatedMessage, true); - copyMessagesStateFromChannel(channel); + copyMsgsStateFromChannel(channel); if (thread && updatedMessage.parent_id) { extraState.threadMessages = channel.state.threads[updatedMessage.parent_id] || []; @@ -1205,7 +1205,7 @@ const ChannelWithContext = < if (channel) { channel.state.removeMessage(oldMessage); channel.state.addMessageSorted(newMessage, true); - copyMessagesStateFromChannel(channel); + copyMsgsStateFromChannel(channel); if (thread && newMessage.parent_id) { const threadMessages = channel.state.threads[newMessage.parent_id] || []; @@ -1505,7 +1505,7 @@ const ChannelWithContext = < ) => { if (channel) { channel.state.removeMessage(message); - copyMessagesStateFromChannel(channel); + copyMsgsStateFromChannel(channel); if (thread) { setThreadMessages(channel.state.threads[thread.id] || []); @@ -1545,7 +1545,7 @@ const ChannelWithContext = < user: client.user, }); - copyMessagesStateFromChannel(channel); + copyMsgsStateFromChannel(channel); const sendReactionResponse = await DBSyncManager.queueTask({ client, @@ -1631,7 +1631,7 @@ const ChannelWithContext = < user: client.user, }); - copyMessagesStateFromChannel(channel); + copyMsgsStateFromChannel(channel); await DBSyncManager.queueTask({ client, From bab5d36c6ff1f7c5a8733926f4aec2251cf42752 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 10 Apr 2025 16:02:45 +0200 Subject: [PATCH 3/6] perf: use throttled message copying everywhere --- package/src/components/Channel/Channel.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index df44138bf9..1743db7dcb 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -746,7 +746,7 @@ const ChannelWithContext = < * This is done until we remove the newMessageStateUpdateThrottleInterval prop. */ - const copyMsgsStateFromChannel = useMemo( + const copyMessagesStateFromChannelThrottled = useMemo( () => throttle( () => { @@ -776,6 +776,7 @@ const ChannelWithContext = < ); const handleEvent: EventHandler = (event) => { + console.log('SHOULD SYNC: ', event.type); if (shouldSyncChannel) { /** * Ignore user.watching.start and user.watching.stop as we should not copy the entire state when @@ -830,11 +831,11 @@ const ChannelWithContext = < // only update channel state if the events are not the previously subscribed useEffect's subscription events if (channel && channel.initialized) { if (event.type === 'message.new') { - copyMsgsStateFromChannel(); + copyMessagesStateFromChannelThrottled(); return; } - if (event.type === 'message.read') { + if (event.type === 'message.read' || event.type === 'notification.mark_read') { return; } @@ -1190,7 +1191,7 @@ const ChannelWithContext = < } channel.state.addMessageSorted(updatedMessage, true); - copyMsgsStateFromChannel(channel); + copyMessagesStateFromChannelThrottled(); if (thread && updatedMessage.parent_id) { extraState.threadMessages = channel.state.threads[updatedMessage.parent_id] || []; @@ -1205,7 +1206,7 @@ const ChannelWithContext = < if (channel) { channel.state.removeMessage(oldMessage); channel.state.addMessageSorted(newMessage, true); - copyMsgsStateFromChannel(channel); + copyMessagesStateFromChannelThrottled(); if (thread && newMessage.parent_id) { const threadMessages = channel.state.threads[newMessage.parent_id] || []; @@ -1505,7 +1506,7 @@ const ChannelWithContext = < ) => { if (channel) { channel.state.removeMessage(message); - copyMsgsStateFromChannel(channel); + copyMessagesStateFromChannelThrottled(); if (thread) { setThreadMessages(channel.state.threads[thread.id] || []); @@ -1545,7 +1546,7 @@ const ChannelWithContext = < user: client.user, }); - copyMsgsStateFromChannel(channel); + copyMessagesStateFromChannelThrottled(); const sendReactionResponse = await DBSyncManager.queueTask({ client, @@ -1631,7 +1632,7 @@ const ChannelWithContext = < user: client.user, }); - copyMsgsStateFromChannel(channel); + copyMessagesStateFromChannelThrottled(); await DBSyncManager.queueTask({ client, From 4b24551515fd905d72f69a53e368b432971a75f2 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 10 Apr 2025 17:21:34 +0200 Subject: [PATCH 4/6] perf: handle setting read state separately --- package/src/components/Channel/Channel.tsx | 43 ++++++++++------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 1743db7dcb..9d967293d5 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -741,10 +741,19 @@ const ChannelWithContext = < channel, }); - /** - * Since we copy the current channel state all together, we need to find the greatest time among the below two and apply it as the throttling time for copying the channel state. - * This is done until we remove the newMessageStateUpdateThrottleInterval prop. - */ + const setReadThrottled = useMemo( + () => + throttle( + () => { + if (channel) { + setRead(channel); + } + }, + stateUpdateThrottleInterval, + throttleOptions, + ), + [channel, stateUpdateThrottleInterval, setRead], + ); const copyMessagesStateFromChannelThrottled = useMemo( () => @@ -776,7 +785,6 @@ const ChannelWithContext = < ); const handleEvent: EventHandler = (event) => { - console.log('SHOULD SYNC: ', event.type); if (shouldSyncChannel) { /** * Ignore user.watching.start and user.watching.stop as we should not copy the entire state when @@ -836,6 +844,7 @@ const ChannelWithContext = < } if (event.type === 'message.read' || event.type === 'notification.mark_read') { + setReadThrottled(); return; } @@ -920,20 +929,6 @@ const ChannelWithContext = < return unsubscribe; }, [channel?.cid, client]); - /** - * Subscription to the Notification mark_read event. - */ - useEffect(() => { - const handleEvent: EventHandler = (event) => { - if (channel.cid === event.cid) { - setRead(channel); - } - }; - - const { unsubscribe } = client.on('notification.mark_read', handleEvent); - return unsubscribe; - }, [channel, client, setRead]); - const threadPropsExists = !!threadProps; useEffect(() => { @@ -1191,7 +1186,7 @@ const ChannelWithContext = < } channel.state.addMessageSorted(updatedMessage, true); - copyMessagesStateFromChannelThrottled(); + copyMessagesStateFromChannel(channel); if (thread && updatedMessage.parent_id) { extraState.threadMessages = channel.state.threads[updatedMessage.parent_id] || []; @@ -1206,7 +1201,7 @@ const ChannelWithContext = < if (channel) { channel.state.removeMessage(oldMessage); channel.state.addMessageSorted(newMessage, true); - copyMessagesStateFromChannelThrottled(); + copyMessagesStateFromChannel(channel); if (thread && newMessage.parent_id) { const threadMessages = channel.state.threads[newMessage.parent_id] || []; @@ -1506,7 +1501,7 @@ const ChannelWithContext = < ) => { if (channel) { channel.state.removeMessage(message); - copyMessagesStateFromChannelThrottled(); + copyMessagesStateFromChannel(channel); if (thread) { setThreadMessages(channel.state.threads[thread.id] || []); @@ -1546,7 +1541,7 @@ const ChannelWithContext = < user: client.user, }); - copyMessagesStateFromChannelThrottled(); + copyMessagesStateFromChannel(channel); const sendReactionResponse = await DBSyncManager.queueTask({ client, @@ -1632,7 +1627,7 @@ const ChannelWithContext = < user: client.user, }); - copyMessagesStateFromChannelThrottled(); + copyMessagesStateFromChannel(channel); await DBSyncManager.queueTask({ client, From a433f6ccf8167fcf9f38bc5c58e588479bcfd64d Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 10 Apr 2025 17:35:27 +0200 Subject: [PATCH 5/6] fix: channel watching in offline state --- package/src/components/Channel/Channel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 9d967293d5..f9ad338742 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -858,7 +858,7 @@ const ChannelWithContext = < const initChannel = async () => { setLastRead(new Date()); const unreadCount = channel.countUnread(); - if (!channel || !shouldSyncChannel || channel.offlineMode) { + if (!channel || !shouldSyncChannel) { return; } let errored = false; From bfd0222753ea4e32f53a5eb4273c0fc950bb0f6f Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 10 Apr 2025 18:01:40 +0200 Subject: [PATCH 6/6] fix: failing faulty test --- .../src/components/Channel/__tests__/Channel.test.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package/src/components/Channel/__tests__/Channel.test.js b/package/src/components/Channel/__tests__/Channel.test.js index 755f3a28f4..7f263ed528 100644 --- a/package/src/components/Channel/__tests__/Channel.test.js +++ b/package/src/components/Channel/__tests__/Channel.test.js @@ -356,7 +356,7 @@ describe('Channel initial load useEffect', () => { cleanup(); }); - it('should not call channel.watch if channel is not initialized', async () => { + it('should still call channel.watch if we are online and DB channels are loaded', async () => { const messages = Array.from({ length: 10 }, (_, i) => generateMessage({ id: i })); const mockedChannel = generateChannelResponse({ messages, @@ -366,13 +366,18 @@ describe('Channel initial load useEffect', () => { const channel = chatClient.channel('messaging', mockedChannel.id); await channel.watch(); channel.offlineMode = true; - channel.state = channelInitialState; + channel.state = { + ...channelInitialState, + messagePagination: { + hasPrev: true, + }, + }; const watchSpy = jest.fn(); channel.watch = watchSpy; renderComponent({ channel }); - await waitFor(() => expect(watchSpy).not.toHaveBeenCalled()); + await waitFor(() => expect(watchSpy).toHaveBeenCalledTimes(1)); }); it("should call channel.watch if channel is initialized and it's not in offline mode", async () => {