From 90b3204a08132f5d48428f8ef82fdd5b31c50000 Mon Sep 17 00:00:00 2001 From: Stream SDK Bot <60655709+Stream-SDK-Bot@users.noreply.github.com> Date: Fri, 15 May 2026 16:25:44 +0100 Subject: [PATCH 1/6] chore: update sdk size (#3599) This PR was created automatically by CI. Co-authored-by: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Co-authored-by: Stream Bot --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5622f90803..a010d8eb45 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![NPM](https://img.shields.io/npm/v/stream-chat-react-native.svg)](https://www.npmjs.com/package/stream-chat-react-native) [![Build Status](https://github.com/GetStream/stream-chat-react-native/actions/workflows/release.yml/badge.svg)](https://github.com/GetStream/stream-chat-react-native/actions) [![Component Reference](https://img.shields.io/badge/docs-component%20reference-blue.svg)](https://getstream.io/chat/docs/sdk/reactnative) -![JS Bundle Size](https://img.shields.io/badge/js_bundle_size-360%20KB-blue) +![JS Bundle Size](https://img.shields.io/badge/js_bundle_size-368%20KB-blue) From daf6e295fe21796ac1cf9340a3f7410578840a59 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Mon, 18 May 2026 12:40:49 +0200 Subject: [PATCH 2/6] fix: flashlist disappearing items (#3600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal This PR addresses an issue with `MessageFlashList` where items would disappear (but still keep their layout within the internal `ScrollView`). The reason behind it is quite technical and it was pretty difficult to find, even though it's just a oneliner. The PR that introduced this was [this one](https://github.com/GetStream/stream-chat-react-native/commit/b935bbe6e43ea81425e5f39b768da0b78cf7c7db) specifically. Typically, when `overflow` is present - React Native takes a [completely different](https://github.com/facebook/react-native/blob/8636cadb8e7d0f62c213e98d264f11dfc5ea913e/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt#L885) draw path. If we look at the [concrete implementation](https://github.com/facebook/react-native/blob/8636cadb8e7d0f62c213e98d264f11dfc5ea913e/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt#L537), we can see the radius distinction: - no rounded borders: `clipRect` - rounded borders: `clipPath` So `borderRadius` + `overflow: 'hidden'` forces the more complex rounded path clipping branch (in other words meaning, instead of drawing a rectangle with discrete dimensions; draw an oval shape instead). Now, `borderRadius` also [participates in](https://github.com/facebook/react-native/blob/8636cadb8e7d0f62c213e98d264f11dfc5ea913e/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt#L747) `inset` math of the `View`. This essentially means that it creates proper bounds for the `Canvas` underneath, should something change. When a `border` is present (and it specifically needs to be a `border` with a known `width`), we essentially do bounded calculations differently. Namely, here's a breakdown of what's (likely) going on: 1. `overflow !== visible` makes `ReactViewGroup.dispatchDraw` call `clipToPaddingBox(...)` before drawing children 2. Because the view has rounded borders, `clipToPaddingBox(...)` uses `canvas.clipPath(...)`, not `canvas.clipRect(...)` 3. That rounded clipping path is computed from the view bounds, border radius and border insets 4. Removing `borderWidth` removes the border insets/border drawable layer that had previously participated in this rounded clipping geometry and invalidation path 5. Enter `MessageFlashList`, cells are recycled and absolutely repositioned while their content/layout changes and that makes the borderless rounded `clipPath` state fragile on Android: the row could remain mounted, measured and pressable, while the draw pass clipped some or all descendants out (this is why the bug represented either thorough an empty message bubble or a completely gone one, depending on which `item` type was being recycled, since single attachments are rendered fully as a bubble) 7. Removing `overflow: 'hidden'` avoids this native clipping branch entirely, so recycled `FlashList` cells are no longer dependent on a stale rounded clip path In other words, as the `CellRenderer` gets recycled from `FlashList`, its inner children do not know that they need to recalculate and so the new `props` are injected into a view which cannot display them and we either get the full bubble missing or the content not being there in text messages specifically. Very interesting nonetheless. Also probably something important to keep in mind for the future. If we are ever to use `overflow: 'hidden'` specifically in `FlashList` item descendants we need to make sure that the layout of the `View` is fully stable and measureable, so that recycling does not accidentally skip necessary recalculations. Also, ironically, this should also be a slight performance improvement as well (at least on Android) as now all `MessageContent` components are going to go down the easier drawing route. ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- .../src/components/Message/MessageItemView/MessageContent.tsx | 1 - .../Thread/__tests__/__snapshots__/Thread.test.tsx.snap | 4 ---- 2 files changed, 5 deletions(-) diff --git a/package/src/components/Message/MessageItemView/MessageContent.tsx b/package/src/components/Message/MessageItemView/MessageContent.tsx index 4f94ad2383..fc1f92bc18 100644 --- a/package/src/components/Message/MessageItemView/MessageContent.tsx +++ b/package/src/components/Message/MessageItemView/MessageContent.tsx @@ -656,7 +656,6 @@ const styles = StyleSheet.create({ containerInner: { borderTopLeftRadius: components.messageBubbleRadiusGroupBottom, borderTopRightRadius: components.messageBubbleRadiusGroupBottom, - overflow: 'hidden', }, contentBody: { flexShrink: 1, diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap index a40eb0ce5e..f41411a83b 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap @@ -506,7 +506,6 @@ exports[`Thread should match thread snapshot 1`] = ` { "borderTopLeftRadius": 20, "borderTopRightRadius": 20, - "overflow": "hidden", }, { "backgroundColor": "#ebeef1", @@ -849,7 +848,6 @@ exports[`Thread should match thread snapshot 1`] = ` { "borderTopLeftRadius": 20, "borderTopRightRadius": 20, - "overflow": "hidden", }, { "backgroundColor": "#ebeef1", @@ -1225,7 +1223,6 @@ exports[`Thread should match thread snapshot 1`] = ` { "borderTopLeftRadius": 20, "borderTopRightRadius": 20, - "overflow": "hidden", }, { "backgroundColor": "#ebeef1", @@ -1559,7 +1556,6 @@ exports[`Thread should match thread snapshot 1`] = ` { "borderTopLeftRadius": 20, "borderTopRightRadius": 20, - "overflow": "hidden", }, { "backgroundColor": "#ebeef1", From bc8eb82cc7d175b359c9cf1fa6b1495d42488cc2 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Tue, 19 May 2026 14:40:42 +0200 Subject: [PATCH 3/6] fix: flash list scroll to behaviour (#3602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal This PR addresses 2 issues with `MessageFlashList` which have made `scrollTo` behaviour unreliable. 1. It should fix (or at least make a lot better) the issue of being snapped back to the end of the list whenever we try to scroll to a quoted message for the very first time 2. It should address not being able to scroll to the first unread message Even though these 2 issues are seemingly similar, their underlying causes are completely different. #### Quoted message scroll to This particular issue happens specifically when we've loaded the messages into memory that we want to scroll to (so loading a completely different `messageSet` is working fine). The offender here is MVCP and scrolling to bottom in particular. I'm pretty certain that there's an upstream bug here, however I did not have a chance to debug it more thoroughly and find the actual root cause. Roughly what goes on is the following: - The list mounts, MVCP gets armed - We press on a quoted message and begin scrolling to it - Mid scroll, MVCP decides something's changed (because of the fact that recycling kicks in and layout gets revalidated) and triggers a pending scroll - The scroll is immediately consumed, snapping us back just after the quoted message scroll finishes I actually had a patch in `FlashList` which allows us to clear all pending MVCP scrolls and also suspend MVCP from doing anything and it worked like a charm (like an imperative API). However for now, this will have to do. At the very least, even when the issue happens it should reconcile shortly after and fix its positioning. #### Initial scroll to first unread This issue on the other hand is completely unrelated to MVCP. It's actually related to our automatic scrolling mechanism, which happens on mount and then also whenever `autoscrollToRecent` actually changes. Namely, if we look into `FlashList`'s implementation we can see that `scrollToEnd` is actually ultimately wrapped within a `setTimeout`, likely to try to delay it natively on the JS runtime for timing purposes. However, this also means that if a state update happens really quickly (for example `targetedMessage` updating) we'll end up making the 2 scrolls race. `scrollToEnd` typically wins because it's invoked later and also because it invokes the underlying scroll view's ref rather than some abstraction. We need this custom handling because `initialScrollIndex` for `FlashList` is very often not correct at all (and off by some number of offset). This is especially true whenever we scroll between 2 message sets and virtually load new data. I've yet to find why this is but maybe some day. To prevent this, we expose a new API on the `Channel` level that allows us to anticipate when we're about to scroll to a targeted message, bypassing the `scrollToEnd` entirely in favor of having a pending scroll. These fixes will be ported back to V8 as well. ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- package/src/components/Channel/Channel.tsx | 27 +++++++-- .../Channel/hooks/useCreateChannelContext.ts | 2 + .../MessageList/MessageFlashList.tsx | 56 ++++++++++--------- .../channelContext/ChannelContext.tsx | 6 ++ 4 files changed, 60 insertions(+), 31 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 3645e6dc8f..dc6724f327 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -567,6 +567,18 @@ const ChannelWithContext = (props: PropsWithChildren) = channel, }); + const shouldLoadInitialChannelAtFirstUnreadMessage = useStableCallback((unreadCount?: number) => { + if (messageId || !initialScrollToFirstUnreadMessage || !client.user) { + return false; + } + + return (unreadCount ?? channel.countUnread()) > scrollToFirstUnreadThreshold; + }); + + const hasPendingInitialTargetLoad = useStableCallback(() => { + return !!messageId || shouldLoadInitialChannelAtFirstUnreadMessage(); + }); + const { setMessages: copyMessagesStateFromChannel, viewabilityChangedCallback } = usePrunableMessageList({ maximumMessageLimit, setMessages: rawCopyMessagesStateFromChannel }); @@ -693,6 +705,7 @@ const ChannelWithContext = (props: PropsWithChildren) = const initChannel = async () => { setLastRead(new Date()); const unreadCount = channel.countUnread(); + const shouldLoadAtFirstUnread = shouldLoadInitialChannelAtFirstUnreadMessage(unreadCount); if (!channel || !shouldSyncChannel) { return; } @@ -722,13 +735,14 @@ const ChannelWithContext = (props: PropsWithChildren) = if (messageId) { await loadChannelAroundMessage({ messageId, setTargetedMessage }); - } else if ( - initialScrollToFirstUnreadMessage && - client.user && - unreadCount > scrollToFirstUnreadThreshold - ) { + } else if (shouldLoadAtFirstUnread) { + const clientUserId = client.user?.id; + if (!clientUserId) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { user, ...ownReadState } = channel.state.read[client.user.id]; + const { user, ...ownReadState } = channel.state.read[clientUserId]; await loadChannelAtFirstUnreadMessage({ channelUnreadState: ownReadState, @@ -1578,6 +1592,7 @@ const ChannelWithContext = (props: PropsWithChildren) = setChannelUnreadState, setLastRead, setTargetedMessage, + hasPendingInitialTargetLoad, targetedMessage, threadList, uploadAbortControllerRef, diff --git a/package/src/components/Channel/hooks/useCreateChannelContext.ts b/package/src/components/Channel/hooks/useCreateChannelContext.ts index 5c7c9e33ca..8e8870707c 100644 --- a/package/src/components/Channel/hooks/useCreateChannelContext.ts +++ b/package/src/components/Channel/hooks/useCreateChannelContext.ts @@ -27,6 +27,7 @@ export const useCreateChannelContext = ({ setChannelUnreadState, setLastRead, setTargetedMessage, + hasPendingInitialTargetLoad, targetedMessage, threadList, uploadAbortControllerRef, @@ -69,6 +70,7 @@ export const useCreateChannelContext = ({ setChannelUnreadState, setLastRead, setTargetedMessage, + hasPendingInitialTargetLoad, targetedMessage, threadList, uploadAbortControllerRef, diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 05a033b949..e9f8fb7bd7 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -132,6 +132,7 @@ type MessageFlashListPropsWithContext = Pick< | 'scrollToFirstUnreadThreshold' | 'setChannelUnreadState' | 'setTargetedMessage' + | 'hasPendingInitialTargetLoad' | 'targetedMessage' | 'threadList' | 'maximumMessageLimit' @@ -289,6 +290,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => setChannelUnreadState, setFlatListRef, setTargetedMessage, + hasPendingInitialTargetLoad, targetedMessage, thread, threadInstance, @@ -388,11 +390,15 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => useEffect(() => { if (autoscrollToRecent && flashListRef.current) { + if (hasPendingInitialTargetLoad?.()) { + return; + } + flashListRef.current.scrollToEnd({ animated: true, }); } - }, [autoscrollToRecent]); + }, [autoscrollToRecent, hasPendingInitialTargetLoad]); const maintainVisibleContentPosition = useMemo(() => { return { @@ -408,18 +414,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => } }, [disabled]); - const indexToScrollToRef = useRef(undefined); - - const initialIndexToScrollTo = useMemo(() => { - return targetedMessage - ? processedMessageList.findIndex((message) => message?.id === targetedMessage) - : -1; - }, [processedMessageList, targetedMessage]); - - useEffect(() => { - indexToScrollToRef.current = initialIndexToScrollTo; - }, [initialIndexToScrollTo]); - /** * Check if a messageId needs to be scrolled to after list loads, and scroll to it * Note: This effect fires on every list change with a small debounce so that scrolling isnt abrupted by an immediate rerender @@ -440,13 +434,29 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => scrollToDebounceTimeoutRef.current = setTimeout(() => { clearTimeout(scrollToDebounceTimeoutRef.current); - // now scroll to it - flashListRef.current?.scrollToIndex({ - animated: true, - index: indexOfParentInMessageList, - viewPosition: 0.5, + const scrollToIndex = async () => { + const list = flashListRef.current; + + if (!list) { + return false; + } + + await list.scrollToIndex({ + index: indexOfParentInMessageList, + animated: true, + viewPosition: 0.5, + }); + + return true; + }; + + requestAnimationFrame(async () => { + await scrollToIndex(); + requestAnimationFrame(async () => { + await scrollToIndex(); + setTargetedMessage(undefined); + }); }); - setTargetedMessage(undefined); }, WAIT_FOR_SCROLL_TIMEOUT); } }, [loadChannelAroundMessage, processedMessageList, setTargetedMessage, targetedMessage]); @@ -456,8 +466,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => (message) => message?.id === messageId, ); - indexToScrollToRef.current = indexOfParentInMessageList; - try { if (indexOfParentInMessageList === -1) { clearTimeout(scrollToDebounceTimeoutRef.current); @@ -529,7 +537,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => setScrollToBottomButtonVisible(true); return; } else { - indexToScrollToRef.current = undefined; setAutoscrollToRecent(true); } const latestNonCurrentMessageBeforeUpdate = latestNonCurrentMessageBeforeUpdateRef.current; @@ -1064,9 +1071,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => data={processedMessageList} drawDistance={800} getItemType={getItemTypeInternal} - initialScrollIndex={ - indexToScrollToRef.current === -1 ? undefined : indexToScrollToRef.current - } keyboardShouldPersistTaps='handled' keyExtractor={keyExtractor} ListFooterComponent={ListFooterComponent} @@ -1203,6 +1207,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => { scrollToFirstUnreadThreshold, setChannelUnreadState, setTargetedMessage, + hasPendingInitialTargetLoad, targetedMessage, threadList, } = useChannelContext(); @@ -1246,6 +1251,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => { scrollToFirstUnreadThreshold, setChannelUnreadState, setTargetedMessage, + hasPendingInitialTargetLoad, shouldShowUnreadUnderlay, targetedMessage, thread, diff --git a/package/src/contexts/channelContext/ChannelContext.tsx b/package/src/contexts/channelContext/ChannelContext.tsx index 6167626f5d..a14119ac0e 100644 --- a/package/src/contexts/channelContext/ChannelContext.tsx +++ b/package/src/contexts/channelContext/ChannelContext.tsx @@ -112,6 +112,12 @@ export type ChannelContextValue = { setChannelUnreadState: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void; setLastRead: React.Dispatch>; setTargetedMessage: (messageId?: string) => void; + /** + * Returns true when Channel is about to load an initial targeted message. + * + * @internal + */ + hasPendingInitialTargetLoad?: () => boolean; /** * Abort controller for cancelling async requests made for uploading images/files * Its a map of filename and AbortController From 89076d5335831e4db9242669904f053fc7413c72 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Tue, 19 May 2026 15:03:58 +0200 Subject: [PATCH 4/6] fix: stop replaying audio on expo when seeing to 0 (#3603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal This PR removes a relatively archaic functionality we've had for voice recordings where seeking to 0 would begin audio replay. This is neither according to our new design system, nor does it make specific sense. In fact, it would break the state completely as `replayAsync` does not go through the `requestPlay` lifecycle of our `audio-player` pool, meaning we would not be getting any state updates (and have audio playing). Integrators anyway have access to the pool if they wish to control this themselves. ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- examples/ExpoMessaging/yarn.lock | 8 ++++---- package/src/state-store/audio-player.ts | 13 +++---------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock index edfa2b6f3b..099e0cadf4 100644 --- a/examples/ExpoMessaging/yarn.lock +++ b/examples/ExpoMessaging/yarn.lock @@ -6073,10 +6073,10 @@ stream-chat-react-native-core@8.1.0: version "0.0.0" uid "" -stream-chat@^9.42.1: - version "9.42.1" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.42.1.tgz#8b6aa4e3e73a39ed07bb2a4f2a6829ba9354567a" - integrity sha512-o+9wQO4Ruu1A48T0IrX9ZH8+9F5xPgGLPvflaswaPeLyIZXcy8bsQdcT/HSrPmT7gs0WGD3qcbXaAJU5lMQezQ== +stream-chat@^9.44.2: + version "9.44.2" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.44.2.tgz#97d23ae4ac356b352bb0f20a31a29dc63d3ea6f5" + integrity sha512-TXALWeHyWnSn1KlGYEF0sltEHB26vFd26l5m1qlE9Q1XHo9RPPSyLb5mfXqTEY8b2FAv57Ei3hrT8nSXVWacDQ== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" diff --git a/package/src/state-store/audio-player.ts b/package/src/state-store/audio-player.ts index 8eb2a4394b..bd0a021937 100644 --- a/package/src/state-store/audio-player.ts +++ b/package/src/state-store/audio-player.ts @@ -425,17 +425,10 @@ export class AudioPlayer { } this.position = positionInMillis; if (this.isExpoCLI) { - if (positionInMillis === 0) { - // If currentTime is 0, we should replay the video from 0th position. - if (this.playerRef?.replayAsync) { - await this.playerRef.replayAsync({}); - } + if (this.playerRef?.setPositionAsync) { + await this.playerRef.setPositionAsync(positionInMillis); } else { - if (this.playerRef?.setPositionAsync) { - await this.playerRef.setPositionAsync(positionInMillis); - } else { - this.notifyError('seek-not-supported'); - } + this.notifyError('seek-not-supported'); } } else { if (this.playerRef?.seek) { From 2dedcd603eb2099511b6a64e5ebd906accbef9ee Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Tue, 19 May 2026 16:25:21 +0200 Subject: [PATCH 5/6] fix: remove keyboard controller remnants (#3605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal This PR removes remnants of `react-native-keyboard-controller` which we initially wanted to add to the SDK in a first line support fashion. However, since it was a bit rushed it was not up to our standards in terms of quality and so we decided to remove it (hence why it isn't mentioned in the docs). This was due to the complexity of the integration within our SDK specifically, not the library itself (the library's really really great and we actively recommend using it if your project allows it). However, some remnants of it spilled over in a way that it forces `react-native-keyboard-controller` to be used if it's installed. This is obviously not ideal as a lot of the issues did not get polished out and so integrations that previously used `react-native-keyboard-controller` elsewhere have a buggy handling of pretty much everything keyboard related. With this change this should be resolved. ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- .gitignore | 2 + examples/ExpoMessaging/package.json | 3 +- examples/ExpoMessaging/yarn.lock | 7 ++ package/package.json | 5 -- .../AttachmentPicker/AttachmentPicker.tsx | 16 +---- package/src/components/Channel/Channel.tsx | 2 +- .../components/ImageGallery/ImageGallery.tsx | 2 +- .../KeyboardCompatibleView.tsx | 6 ++ .../KeyboardControllerAvoidingView.tsx | 69 ------------------- package/src/components/Message/Message.tsx | 2 +- .../UIComponents/BottomSheetModal.tsx | 15 +--- .../componentsContext/defaultComponents.ts | 2 +- .../MessageInputContext.tsx | 2 +- .../src/hooks/useAfterKeyboardOpenCallback.ts | 5 +- package/src/hooks/useKeyboardVisibility.ts | 36 +++------- 15 files changed, 38 insertions(+), 136 deletions(-) delete mode 100644 package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx diff --git a/.gitignore b/.gitignore index 3895765f2f..ca66e76e98 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ index.android.bundle *.DSYM.zip **/metrics/ package/shared-native/.sync-state/ + +.claude/worktrees diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 1664d6fd02..b57ba6ed16 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -47,13 +47,14 @@ "react-dom": "19.2.0", "react-native": "0.83.2", "react-native-gesture-handler": "~2.30.0", + "react-native-keyboard-controller": "1.20.7", "react-native-maps": "1.26.20", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.23.0", "react-native-svg": "15.15.3", - "react-native-web": "^0.21.0", "react-native-teleport": "^1.0.2", + "react-native-web": "^0.21.0", "react-native-worklets": "0.7.2", "stream-chat-expo": "link:../../package/expo-package", "stream-chat-react-native-core": "link:../../package" diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock index 099e0cadf4..d307548819 100644 --- a/examples/ExpoMessaging/yarn.lock +++ b/examples/ExpoMessaging/yarn.lock @@ -5526,6 +5526,13 @@ react-native-is-edge-to-edge@1.2.1, react-native-is-edge-to-edge@^1.2.1: resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358" integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q== +react-native-keyboard-controller@1.20.7: + version "1.20.7" + resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.20.7.tgz#e1be1c15a5eb10b96a40a0812d8472e6e4bd8f29" + integrity sha512-G8S5jz1FufPrcL1vPtReATx+jJhT/j+sTqxMIb30b1z7cYEfMlkIzOCyaHgf6IMB2KA9uBmnA5M6ve2A9Ou4kw== + dependencies: + react-native-is-edge-to-edge "^1.2.1" + react-native-lightbox@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz#e52b4d7fcc141f59d7b23f0180de535e35b20ec9" diff --git a/package/package.json b/package/package.json index 4cc0500837..96eee94d36 100644 --- a/package/package.json +++ b/package/package.json @@ -94,7 +94,6 @@ "emoji-mart": ">=5.4.0", "react-native": ">=0.73.0", "react-native-gesture-handler": ">=2.18.0", - "react-native-keyboard-controller": ">=1.20.2", "react-native-reanimated": ">=3.16.0", "react-native-safe-area-context": ">=5.4.1", "react-native-svg": ">=15.8.0", @@ -112,9 +111,6 @@ }, "@emoji-mart/data": { "optional": true - }, - "react-native-keyboard-controller": { - "optional": true } }, "devDependencies": { @@ -163,7 +159,6 @@ "react-native": "0.80.2", "react-native-builder-bob": "0.40.11", "react-native-gesture-handler": "^2.26.0", - "react-native-keyboard-controller": "^1.20.2", "react-native-reanimated": "3.18.0", "react-native-safe-area-context": "^5.6.1", "react-native-svg": "15.12.0", diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx index 3b8586e82f..4df1abec1b 100644 --- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx +++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { BackHandler, - EmitterSubscription, Keyboard, Platform, View, @@ -21,7 +20,6 @@ import { useComponentsContext } from '../../contexts/componentsContext/Component import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useStableCallback } from '../../hooks'; import { BottomSheet } from '../BottomSheetCompatibility/BottomSheet'; -import { KeyboardControllerPackage } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; dayjs.extend(duration); @@ -78,18 +76,10 @@ export const AttachmentPicker = () => { } closePicker(); }; - let keyboardSubscription: EmitterSubscription | null = null; - if (KeyboardControllerPackage?.KeyboardEvents) { - keyboardSubscription = KeyboardControllerPackage.KeyboardEvents.addListener( - 'keyboardWillShow', - onKeyboardOpenHandler, - ); - } else { - const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; - keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler); - } + const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + const keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler); return () => { - keyboardSubscription?.remove(); + keyboardSubscription.remove(); }; }, [attachmentPickerStore, closePicker]); diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index dc6724f327..2ea9bc41af 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -114,7 +114,7 @@ import { } from '../../utils/utils'; import { NotificationAnnouncer } from '../Accessibility/NotificationAnnouncer'; import { AttachmentPicker } from '../AttachmentPicker/AttachmentPicker'; -import type { KeyboardCompatibleViewProps } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import type { KeyboardCompatibleViewProps } from '../KeyboardCompatibleView/KeyboardCompatibleView'; import { Emoji } from '../MessageMenu/EmojiPickerList'; import { emojis } from '../MessageMenu/emojis'; import { toUnicodeScalarString } from '../MessageMenu/utils/toUnicodeScalarString'; diff --git a/package/src/components/ImageGallery/ImageGallery.tsx b/package/src/components/ImageGallery/ImageGallery.tsx index d3af58ec2d..a07076bc59 100644 --- a/package/src/components/ImageGallery/ImageGallery.tsx +++ b/package/src/components/ImageGallery/ImageGallery.tsx @@ -36,7 +36,7 @@ import { useViewport } from '../../hooks/useViewport'; import { IconProps } from '../../icons/utils/base'; import { ImageGalleryState } from '../../state-store/image-gallery-state-store'; import { FileTypes } from '../../types/types'; -import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardCompatibleView'; import { BottomSheetModal } from '../UIComponents'; export type ImageGalleryActionHandler = () => Promise | void; diff --git a/package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx b/package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx index 52f599ea16..2371c6e364 100644 --- a/package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx +++ b/package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx @@ -22,6 +22,12 @@ import { import { KeyboardProvider } from '../../contexts/keyboardContext/KeyboardContext'; +export type KeyboardCompatibleViewProps = KeyboardAvoidingViewProps; + +export const dismissKeyboard = () => { + Keyboard.dismiss(); +}; + type State = { bottom: number; }; diff --git a/package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx b/package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx deleted file mode 100644 index fd31080d70..0000000000 --- a/package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useEffect } from 'react'; - -import { - Keyboard, - KeyboardAvoidingViewProps as ReactNativeKeyboardAvoidingViewProps, -} from 'react-native'; - -import { KeyboardCompatibleView as KeyboardCompatibleViewDefault } from './KeyboardCompatibleView'; - -type ExtraKeyboardControllerProps = { - behavior?: 'translate-with-padding'; -}; - -type KeyboardControllerModule = typeof import('react-native-keyboard-controller'); - -const optionalRequire = (): T | undefined => { - try { - return require('react-native-keyboard-controller') as T; - } catch { - return undefined; - } -}; - -export type KeyboardCompatibleViewProps = ReactNativeKeyboardAvoidingViewProps & - ExtraKeyboardControllerProps; - -const KeyboardControllerPackage = optionalRequire(); - -const { AndroidSoftInputModes, KeyboardController, KeyboardProvider, KeyboardAvoidingView } = - KeyboardControllerPackage ?? {}; - -export const KeyboardCompatibleView = (props: KeyboardCompatibleViewProps) => { - const { behavior = 'translate-with-padding', children, ...rest } = props; - - useEffect(() => { - if (AndroidSoftInputModes) { - KeyboardController?.setInputMode(AndroidSoftInputModes.SOFT_INPUT_ADJUST_RESIZE); - } - - return () => KeyboardController?.setDefaultMode(); - }, []); - - if (KeyboardProvider && KeyboardAvoidingView) { - return ( - - {/* @ts-expect-error - The reason is that react-native-keyboard-controller's KeyboardAvoidingViewProps is a discriminated union, not a simple behavior union so it complains about the `position` value passed. */} - - {children} - - - ); - } - const compatibleBehavior = behavior === 'translate-with-padding' ? 'padding' : behavior; - - return ( - - {children} - - ); -}; - -export const dismissKeyboard = () => { - if (KeyboardControllerPackage?.KeyboardController) { - KeyboardControllerPackage?.KeyboardController.dismiss(); - } - Keyboard.dismiss(); -}; - -export { KeyboardControllerPackage }; diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 316979be3f..57768e813e 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -77,7 +77,7 @@ import { MessageStatusTypes, } from '../../utils/utils'; import type { Thumbnail } from '../Attachment/utils/buildGallery/types'; -import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardCompatibleView'; import { BottomSheetModal } from '../UIComponents'; const createMessageOverlayId = (messageId?: string) => diff --git a/package/src/components/UIComponents/BottomSheetModal.tsx b/package/src/components/UIComponents/BottomSheetModal.tsx index 843aec89b8..337f4d3f2a 100644 --- a/package/src/components/UIComponents/BottomSheetModal.tsx +++ b/package/src/components/UIComponents/BottomSheetModal.tsx @@ -19,7 +19,6 @@ import { View, } from 'react-native'; import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; -import type { KeyboardEventData } from 'react-native-keyboard-controller'; import Animated, { Easing, FadeIn, @@ -43,7 +42,6 @@ import { BottomSheetProvider } from '../../contexts/bottomSheetContext/BottomShe import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useStableCallback } from '../../hooks'; import { primitives } from '../../theme'; -import { KeyboardControllerPackage } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; export type BottomSheetModalProps = { /** @@ -377,18 +375,11 @@ const BottomSheetModalInner = (props: PropsWithChildren) const listeners: EventSubscription[] = []; - if (KeyboardControllerPackage?.KeyboardEvents) { - const keyboardDidShowKC = (event: KeyboardEventData) => { - animateKeyboardOffset(event.height); - }; - + if (Platform.OS === 'ios') { listeners.push( - KeyboardControllerPackage.KeyboardEvents.addListener('keyboardDidShow', keyboardDidShowKC), - KeyboardControllerPackage.KeyboardEvents.addListener('keyboardDidHide', keyboardDidHide), + Keyboard.addListener('keyboardWillShow', keyboardDidShowRN), + Keyboard.addListener('keyboardWillHide', keyboardDidHide), ); - } else if (Platform.OS === 'ios') { - listeners.push(Keyboard.addListener('keyboardWillShow', keyboardDidShowRN)); - listeners.push(Keyboard.addListener('keyboardWillHide', keyboardDidHide)); } return () => listeners.forEach((l) => l.remove()); diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index 6735fba586..4aac65dee5 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -50,7 +50,7 @@ import { ImageGalleryGrid } from '../../components/ImageGallery/components/Image import { EmptyStateIndicator } from '../../components/Indicators/EmptyStateIndicator'; import { LoadingErrorIndicator } from '../../components/Indicators/LoadingErrorIndicator'; import { LoadingIndicator } from '../../components/Indicators/LoadingIndicator'; -import { KeyboardCompatibleView } from '../../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import { KeyboardCompatibleView } from '../../components/KeyboardCompatibleView/KeyboardCompatibleView'; import { Message } from '../../components/Message/Message'; import { MessagePinnedHeader } from '../../components/Message/MessageItemView/Headers/MessagePinnedHeader'; import { MessageReminderHeader } from '../../components/Message/MessageItemView/Headers/MessageReminderHeader'; diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index af240756a0..dd6ad459ac 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -23,7 +23,7 @@ import { import { useCreateMessageInputContext } from './hooks/useCreateMessageInputContext'; import { useMessageComposer } from './hooks/useMessageComposer'; -import { dismissKeyboard } from '../../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import { dismissKeyboard } from '../../components/KeyboardCompatibleView/KeyboardCompatibleView'; import { parseLinksFromText } from '../../components/Message/MessageItemView/utils/parseLinks'; import { useAudioRecorder } from '../../components/MessageInput/hooks/useAudioRecorder'; import { useNotificationApi } from '../../components/Notifications'; diff --git a/package/src/hooks/useAfterKeyboardOpenCallback.ts b/package/src/hooks/useAfterKeyboardOpenCallback.ts index 199bd92cc4..9ac6a78a8e 100644 --- a/package/src/hooks/useAfterKeyboardOpenCallback.ts +++ b/package/src/hooks/useAfterKeyboardOpenCallback.ts @@ -3,7 +3,6 @@ import { EventSubscription, Keyboard, Platform } from 'react-native'; import { useStableCallback } from './useStableCallback'; -import { KeyboardControllerPackage } from '../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; import { useMessageInputContext } from '../contexts/messageInputContext/MessageInputContext'; /** @@ -50,9 +49,7 @@ export const useAfterKeyboardOpenCallback = ( const keyboardEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; - keyboardSubscriptionRef.current = KeyboardControllerPackage?.KeyboardEvents - ? KeyboardControllerPackage.KeyboardEvents.addListener(keyboardEvent, runCallback) - : Keyboard.addListener(keyboardEvent, runCallback); + keyboardSubscriptionRef.current = Keyboard.addListener(keyboardEvent, runCallback); inputBoxRef.current.focus(); }); diff --git a/package/src/hooks/useKeyboardVisibility.ts b/package/src/hooks/useKeyboardVisibility.ts index cffd977b31..2218f6321a 100644 --- a/package/src/hooks/useKeyboardVisibility.ts +++ b/package/src/hooks/useKeyboardVisibility.ts @@ -1,7 +1,5 @@ import { useEffect, useState } from 'react'; -import { EventSubscription, Keyboard, Platform } from 'react-native'; - -import { KeyboardControllerPackage } from '../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import { Keyboard, Platform } from 'react-native'; /** * A custom hook that provides a boolean value indicating whether the keyboard is visible. @@ -11,30 +9,14 @@ export const useKeyboardVisibility = () => { const [isKeyboardVisible, setIsKeyboardVisible] = useState(false); useEffect(() => { - const listeners: EventSubscription[] = []; - if (KeyboardControllerPackage?.KeyboardEvents) { - listeners.push( - KeyboardControllerPackage.KeyboardEvents.addListener('keyboardWillShow', () => - setIsKeyboardVisible(true), - ), - ); - listeners.push( - KeyboardControllerPackage.KeyboardEvents.addListener('keyboardWillHide', () => - setIsKeyboardVisible(false), - ), - ); - } else { - listeners.push( - Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () => - setIsKeyboardVisible(true), - ), - ); - listeners.push( - Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => - setIsKeyboardVisible(false), - ), - ); - } + const listeners = [ + Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () => + setIsKeyboardVisible(true), + ), + Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => + setIsKeyboardVisible(false), + ), + ]; return () => listeners.forEach((listener) => listener.remove()); }, []); From 0b7977a96b9bc0fad052bf5441e5ff1a61c31ccc Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Tue, 19 May 2026 16:57:11 +0200 Subject: [PATCH 6/6] fix: respect topInset on bottom sheet once again (#3606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal This PR addresses a regression with our `AttachmentPicker` component which was done in order to fix accessibility issues. Namely, since we calculate the `topInset` for our `BottomSheet` in order to make it properly orderable by screen readers, the value for the global `topInset` was omitted. This is wrong, because then the calculations are very wrong particularly in the cases of having native modals (i.e `react-navigation/native-stack` or `expo-router`) whose height we need to take into account, similarly to how we do it for `keyboardVerticalOffset`. ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- examples/ExpoMessaging/app/channel/[cid]/index.tsx | 1 + .../ExpoMessaging/app/channel/[cid]/thread/[cid]/index.tsx | 1 + package/src/components/AttachmentPicker/AttachmentPicker.tsx | 3 ++- package/src/components/Channel/Channel.tsx | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/ExpoMessaging/app/channel/[cid]/index.tsx b/examples/ExpoMessaging/app/channel/[cid]/index.tsx index 0410bec2f6..fa8be06f86 100644 --- a/examples/ExpoMessaging/app/channel/[cid]/index.tsx +++ b/examples/ExpoMessaging/app/channel/[cid]/index.tsx @@ -73,6 +73,7 @@ export default function ChannelScreen() { channel={channel} onPressMessage={onPressMessage} keyboardVerticalOffset={headerHeight} + topInset={headerHeight} thread={thread} > diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx index 4df1abec1b..cd4d786930 100644 --- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx +++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx @@ -39,6 +39,7 @@ export const AttachmentPicker = () => { attachmentPickerBottomSheetHeight, bottomSheetRef: ref, bottomInset, + topInset, disableAttachmentPicker, } = useAttachmentPickerContext(); const { AttachmentPickerContent, AttachmentPickerSelectionBar } = useComponentsContext(); @@ -94,7 +95,7 @@ export const AttachmentPicker = () => { const initialSnapPoint = attachmentPickerBottomSheetHeight; const pickerTopInset = Math.max( 0, - windowHeight - attachmentPickerBottomSheetHeight - bottomInset, + windowHeight - topInset - attachmentPickerBottomSheetHeight - bottomInset, ); /** diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 2ea9bc41af..52070c39b8 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -496,7 +496,7 @@ const ChannelWithContext = (props: PropsWithChildren) = thread: threadFromProps, threadList, threadMessages, - topInset, + topInset = 0, isOnline, maximumMessageLimit, initializeOnMount = true,