From 206acd14cdf01612d410ab3e5397757f7bdd7d2e Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 19 Feb 2026 13:12:50 +0100 Subject: [PATCH] fix: ctx menu positioning on resizable viewport --- examples/SampleApp/yarn.lock | 8 ++++---- .../src/components/MessageList/MessageList.tsx | 9 ++++++++- .../overlayContext/MessageOverlayHostLayer.tsx | 16 ++++++++++++---- .../src/state-store/message-overlay-store.ts | 17 ++++++++++++++--- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index f4dfd835e..9ab669def 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -8354,10 +8354,10 @@ stream-chat-react-native-core@8.1.0: version "0.0.0" uid "" -stream-chat@^9.30.1: - version "9.30.1" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.30.1.tgz#86d152e4d0894854370512d17530854541f7990b" - integrity sha512-8f58tCo3QfgzaNhWHpRQzEfglSPPn4lGRn74FFTr/pn53dMJwtcKDSohV6NTHBrkYWTXYObRnHgh2IhGFUKckw== +stream-chat@^9.33.0: + version "9.34.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.34.0.tgz#e92c3262e1b6fbe92b1b1148286ee152849250dc" + integrity sha512-b65Z+ufAtygAwT2dCQ8ImgMx01b9zgS1EZ8OK5lRHhSJKYKSsSa1pS3USbbFq6QpuwGZwXM3lovGXLYoWiG84g== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 638e16303..c5bc2a34d 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -65,6 +65,7 @@ import { ThreadContextValue, useThreadContext } from '../../contexts/threadConte import { useStableCallback } from '../../hooks'; import { useStateStore } from '../../hooks/useStateStore'; +import { bumpOverlayLayoutRevision } from '../../state-store'; import { MessageInputHeightState } from '../../state-store/message-input-height-store'; import { primitives } from '../../theme'; import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper'; @@ -1171,7 +1172,13 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { if (additionalFlatListProps?.onLayout) { additionalFlatListProps.onLayout(event); } - viewportHeightRef.current = event.nativeEvent.layout.height; + const nextViewportHeight = event.nativeEvent.layout.height; + if (viewportHeightRef.current !== nextViewportHeight) { + const previousViewportHeight = viewportHeightRef.current ?? nextViewportHeight; + const closeCorrectionDeltaY = nextViewportHeight - previousViewportHeight; + bumpOverlayLayoutRevision(closeCorrectionDeltaY); + } + viewportHeightRef.current = nextViewportHeight; }); if (!ListComponent) { diff --git a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx index e0226091d..e3a4bcf8f 100644 --- a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +++ b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx @@ -31,6 +31,7 @@ export const MessageOverlayHostLayer = () => { const messageH = useSharedValue(undefined); const topH = useSharedValue(undefined); const bottomH = useSharedValue(undefined); + const closeCorrectionY = useSharedValue(0); const topInset = insets.top; // Due to edge-to-edge in combination with various libraries, Android sometimes reports @@ -50,10 +51,17 @@ export const MessageOverlayHostLayer = () => { useEffect( () => registerOverlaySharedValueController({ + incrementCloseCorrectionY: (deltaY) => { + closeCorrectionY.value += deltaY; + }, + resetCloseCorrectionY: () => { + closeCorrectionY.value = 0; + }, reset: () => { messageH.value = undefined; topH.value = undefined; bottomH.value = undefined; + closeCorrectionY.value = 0; }, setBottomH: (rect) => { bottomH.value = rect; @@ -65,7 +73,7 @@ export const MessageOverlayHostLayer = () => { topH.value = rect; }, }), - [bottomH, messageH, topH], + [bottomH, closeCorrectionY, messageH, topH], ); useEffect(() => { @@ -163,7 +171,7 @@ export const MessageOverlayHostLayer = () => { }); const topItemTranslateStyle = useAnimatedStyle(() => { - const target = isActive ? (closing ? 0 : shiftY.value) : 0; + const target = isActive ? (closing ? closeCorrectionY.value : shiftY.value) : 0; return { transform: [ { scale: backdrop.value }, @@ -184,7 +192,7 @@ export const MessageOverlayHostLayer = () => { }); const bottomItemTranslateStyle = useAnimatedStyle(() => { - const target = isActive ? (closing ? 0 : shiftY.value) : 0; + const target = isActive ? (closing ? closeCorrectionY.value : shiftY.value) : 0; return { transform: [ { scale: backdrop.value }, @@ -205,7 +213,7 @@ export const MessageOverlayHostLayer = () => { }); const hostTranslateStyle = useAnimatedStyle(() => { - const target = isActive ? (closing ? 0 : shiftY.value) : 0; + const target = isActive ? (closing ? closeCorrectionY.value : shiftY.value) : 0; return { transform: [ diff --git a/package/src/state-store/message-overlay-store.ts b/package/src/state-store/message-overlay-store.ts index e2f1e653e..09cdc2e3a 100644 --- a/package/src/state-store/message-overlay-store.ts +++ b/package/src/state-store/message-overlay-store.ts @@ -17,6 +17,8 @@ const DefaultState = { }; type OverlaySharedValueController = { + incrementCloseCorrectionY: (deltaY: number) => void; + resetCloseCorrectionY: () => void; reset: () => void; setBottomH: (rect: Rect) => void; setMessageH: (rect: Rect) => void; @@ -46,10 +48,19 @@ export const setOverlayBottomH = (bottomH: Rect) => { sharedValueController?.setBottomH(bottomH); }; -export const openOverlay = (id: string) => overlayStore.partialNext({ closing: false, id }); +export const bumpOverlayLayoutRevision = (closeCorrectionDeltaY = 0) => { + sharedValueController?.incrementCloseCorrectionY(closeCorrectionDeltaY); +}; + +export const openOverlay = (id: string) => { + sharedValueController?.resetCloseCorrectionY(); + overlayStore.partialNext({ closing: false, id }); +}; export const closeOverlay = () => { - requestAnimationFrame(() => overlayStore.partialNext({ closing: true })); + requestAnimationFrame(() => { + overlayStore.partialNext({ closing: true }); + }); }; let actionQueue: Array<() => void | Promise> = []; @@ -64,8 +75,8 @@ export const scheduleActionOnClose = (action: () => void | Promise) => { }; export const finalizeCloseOverlay = () => { - sharedValueController?.reset(); overlayStore.partialNext(DefaultState); + sharedValueController?.reset(); }; export const overlayStore = new StateStore(DefaultState);