From 0a75dd9a62bc20e0ecc60620f5411dd08a193d5c Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Sat, 16 Aug 2025 10:52:08 +0700 Subject: [PATCH 1/7] =?UTF-8?q?Fix=20-=20Clicking=20the=20=E2=80=9CFrom?= =?UTF-8?q?=E2=80=9D=20link=20redirects=20to=20the=20conversation,=20but?= =?UTF-8?q?=20not=20to=20the=20specific=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActionSheetKeyboardSpace.tsx | 4 +--- .../ActionSheetAwareScrollView/index.ios.tsx | 23 ++++++++++++++++--- .../ActionSheetAwareScrollView/type.tsx | 7 ++++++ .../BaseInvertedFlatList/index.tsx | 7 ++++-- 4 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 src/components/ActionSheetAwareScrollView/type.tsx diff --git a/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx b/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx index cf96a0823b0f..88db96d90a34 100644 --- a/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx +++ b/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx @@ -4,7 +4,6 @@ import {useKeyboardHandler} from 'react-native-keyboard-controller'; import Reanimated, {useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; import type {SharedValue} from 'react-native-reanimated'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; -import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {Actions, ActionSheetAwareScrollViewContext, States} from './ActionSheetAwareScrollViewContext'; @@ -66,7 +65,6 @@ type ActionSheetKeyboardSpaceProps = ViewProps & { }; function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { - const styles = useThemeStyles(); const { unmodifiedPaddings: {top: paddingTop = 0, bottom: paddingBottom = 0}, } = useSafeAreaPaddings(); @@ -255,7 +253,7 @@ function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { return ( diff --git a/src/components/ActionSheetAwareScrollView/index.ios.tsx b/src/components/ActionSheetAwareScrollView/index.ios.tsx index 9c465b966daf..3bc7fd903069 100644 --- a/src/components/ActionSheetAwareScrollView/index.ios.tsx +++ b/src/components/ActionSheetAwareScrollView/index.ios.tsx @@ -1,12 +1,14 @@ import type {PropsWithChildren} from 'react'; -import React, {forwardRef, useCallback} from 'react'; +import React, {forwardRef, useCallback, useEffect, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ScrollView, ScrollViewProps} from 'react-native'; +import {InteractionManager} from 'react-native'; import Reanimated, {useAnimatedRef, useScrollViewOffset} from 'react-native-reanimated'; import {Actions, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider} from './ActionSheetAwareScrollViewContext'; import ActionSheetKeyboardSpace from './ActionSheetKeyboardSpace'; +import type ActionSheetAwareScrollViewProps from './type'; -const ActionSheetAwareScrollView = forwardRef>((props, ref) => { +const ActionSheetAwareScrollView = forwardRef>(({isInitialData, ...props}, ref) => { const scrollViewAnimatedRef = useAnimatedRef(); const position = useScrollViewOffset(scrollViewAnimatedRef); @@ -23,6 +25,16 @@ const ActionSheetAwareScrollView = forwardRef { + if (isInitialData) { + return; + } + InteractionManager.runAfterInteractions(() => { + setIsInitialRenderDone(true); + }); + }, [isInitialData]); return ( - {props.children} + {React.Children.map(props.children, (child, index) => { + if (index === 0 && isInitialRenderDone) { + return {child}; + } + return child; + })} ); }); diff --git a/src/components/ActionSheetAwareScrollView/type.tsx b/src/components/ActionSheetAwareScrollView/type.tsx new file mode 100644 index 000000000000..367b9747d58e --- /dev/null +++ b/src/components/ActionSheetAwareScrollView/type.tsx @@ -0,0 +1,7 @@ +import type {ScrollViewProps} from 'react-native'; + +type ActionSheetAwareScrollViewProps = ScrollViewProps & { + isInitialData?: boolean; +}; + +export default ActionSheetAwareScrollViewProps; diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 5cccc596a522..691f27b3a3f7 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -1,6 +1,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {FlatListProps, ListRenderItem, ListRenderItemInfo, FlatList as RNFlatList, ScrollViewProps} from 'react-native'; +import type ActionSheetAwareScrollViewProps from '@components/ActionSheetAwareScrollView/type'; import FlatList from '@components/FlatList'; import usePrevious from '@hooks/usePrevious'; import getInitialPaginationSize from './getInitialPaginationSize'; @@ -19,17 +20,18 @@ function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: n return String(index); } -type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem' | 'initialScrollIndex'> & { +type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem' | 'initialScrollIndex' | 'renderScrollComponent'> & { shouldEnableAutoScrollToTopThreshold?: boolean; data: T[]; renderItem: ListRenderItem; initialScrollKey?: string | null; + renderScrollComponent?: (props: ActionSheetAwareScrollViewProps) => React.ReactElement; }; const AUTOSCROLL_TO_TOP_THRESHOLD = 250; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...rest} = props; + const {renderScrollComponent, shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...rest} = props; // `initialScrollIndex` doesn't work properly with FlatList, this uses an alternative approach to achieve the same effect. // What we do is start rendering the list from `initialScrollKey` and then whenever we reach the start we render more // previous items, until everything is rendered. We also progressively render new data that is added at the start of the @@ -135,6 +137,7 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa onStartReached={handleStartReached} renderItem={handleRenderItem} keyExtractor={keyExtractor} + renderScrollComponent={renderScrollComponent ? (scrollProps) => renderScrollComponent({...scrollProps, isInitialData}) : undefined} /> ); } From 26e70da2be235d0fe486f45ced7fee10fd566c69 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Mon, 25 Aug 2025 17:35:33 +0700 Subject: [PATCH 2/7] =?UTF-8?q?Fix=20-=20Clicking=20the=20=E2=80=9CFrom?= =?UTF-8?q?=E2=80=9D=20link=20redirects=20to=20the=20conversation,=20but?= =?UTF-8?q?=20not=20to=20the=20specific=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActionSheetAwareScrollView/index.ios.tsx | 28 +++++-------------- .../ActionSheetAwareScrollView/type.tsx | 7 ----- ...ce.tsx => useActionSheetKeyboardSpace.tsx} | 20 +++++-------- .../BaseInvertedFlatList/index.tsx | 7 ++--- 4 files changed, 16 insertions(+), 46 deletions(-) delete mode 100644 src/components/ActionSheetAwareScrollView/type.tsx rename src/components/ActionSheetAwareScrollView/{ActionSheetKeyboardSpace.tsx => useActionSheetKeyboardSpace.tsx} (94%) diff --git a/src/components/ActionSheetAwareScrollView/index.ios.tsx b/src/components/ActionSheetAwareScrollView/index.ios.tsx index 3bc7fd903069..8f2dde9646f5 100644 --- a/src/components/ActionSheetAwareScrollView/index.ios.tsx +++ b/src/components/ActionSheetAwareScrollView/index.ios.tsx @@ -1,14 +1,12 @@ import type {PropsWithChildren} from 'react'; -import React, {forwardRef, useCallback, useEffect, useState} from 'react'; +import React, {forwardRef, useCallback} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ScrollView, ScrollViewProps} from 'react-native'; -import {InteractionManager} from 'react-native'; import Reanimated, {useAnimatedRef, useScrollViewOffset} from 'react-native-reanimated'; import {Actions, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider} from './ActionSheetAwareScrollViewContext'; -import ActionSheetKeyboardSpace from './ActionSheetKeyboardSpace'; -import type ActionSheetAwareScrollViewProps from './type'; +import useActionSheetKeyboardSpace from './useActionSheetKeyboardSpace'; -const ActionSheetAwareScrollView = forwardRef>(({isInitialData, ...props}, ref) => { +const ActionSheetAwareScrollView = forwardRef>(({style, children, ...rest}, ref) => { const scrollViewAnimatedRef = useAnimatedRef(); const position = useScrollViewOffset(scrollViewAnimatedRef); @@ -25,29 +23,17 @@ const ActionSheetAwareScrollView = forwardRef { - if (isInitialData) { - return; - } - InteractionManager.runAfterInteractions(() => { - setIsInitialRenderDone(true); - }); - }, [isInitialData]); + const {animatedStyle} = useActionSheetKeyboardSpace({position}); return ( - {React.Children.map(props.children, (child, index) => { - if (index === 0 && isInitialRenderDone) { - return {child}; - } - return child; - })} + {children} ); }); diff --git a/src/components/ActionSheetAwareScrollView/type.tsx b/src/components/ActionSheetAwareScrollView/type.tsx deleted file mode 100644 index 367b9747d58e..000000000000 --- a/src/components/ActionSheetAwareScrollView/type.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import type {ScrollViewProps} from 'react-native'; - -type ActionSheetAwareScrollViewProps = ScrollViewProps & { - isInitialData?: boolean; -}; - -export default ActionSheetAwareScrollViewProps; diff --git a/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx similarity index 94% rename from src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx rename to src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx index 88db96d90a34..9dad2ca015b6 100644 --- a/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx +++ b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx @@ -1,7 +1,7 @@ -import React, {useContext, useEffect} from 'react'; +import {useContext, useEffect} from 'react'; import type {ViewProps} from 'react-native'; import {useKeyboardHandler} from 'react-native-keyboard-controller'; -import Reanimated, {useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; +import {useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; import type {SharedValue} from 'react-native-reanimated'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -64,7 +64,7 @@ type ActionSheetKeyboardSpaceProps = ViewProps & { position?: SharedValue; }; -function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { +function useActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { const { unmodifiedPaddings: {top: paddingTop = 0, bottom: paddingBottom = 0}, } = useSafeAreaPaddings(); @@ -153,10 +153,12 @@ function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { if (popoverHeight) { if (previousElementOffset !== 0 || elementOffset > previousElementOffset) { const returnValue = elementOffset < 0 ? 0 : elementOffset; + return returnValue; return withSpring(returnValue, SPRING_CONFIG); } const returnValue = Math.max(previousElementOffset, 0); + return returnValue; return withSpring(returnValue, SPRING_CONFIG); } @@ -251,15 +253,7 @@ function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { paddingTop: translateY.get(), })); - return ( - - ); + return {animatedStyle}; } -ActionSheetKeyboardSpace.displayName = 'ActionSheetKeyboardSpace'; - -export default ActionSheetKeyboardSpace; +export default useActionSheetKeyboardSpace; diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 691f27b3a3f7..5cccc596a522 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -1,7 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {FlatListProps, ListRenderItem, ListRenderItemInfo, FlatList as RNFlatList, ScrollViewProps} from 'react-native'; -import type ActionSheetAwareScrollViewProps from '@components/ActionSheetAwareScrollView/type'; import FlatList from '@components/FlatList'; import usePrevious from '@hooks/usePrevious'; import getInitialPaginationSize from './getInitialPaginationSize'; @@ -20,18 +19,17 @@ function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: n return String(index); } -type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem' | 'initialScrollIndex' | 'renderScrollComponent'> & { +type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem' | 'initialScrollIndex'> & { shouldEnableAutoScrollToTopThreshold?: boolean; data: T[]; renderItem: ListRenderItem; initialScrollKey?: string | null; - renderScrollComponent?: (props: ActionSheetAwareScrollViewProps) => React.ReactElement; }; const AUTOSCROLL_TO_TOP_THRESHOLD = 250; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {renderScrollComponent, shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...rest} = props; + const {shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...rest} = props; // `initialScrollIndex` doesn't work properly with FlatList, this uses an alternative approach to achieve the same effect. // What we do is start rendering the list from `initialScrollKey` and then whenever we reach the start we render more // previous items, until everything is rendered. We also progressively render new data that is added at the start of the @@ -137,7 +135,6 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa onStartReached={handleStartReached} renderItem={handleRenderItem} keyExtractor={keyExtractor} - renderScrollComponent={renderScrollComponent ? (scrollProps) => renderScrollComponent({...scrollProps, isInitialData}) : undefined} /> ); } From 565bb67c325d8574e382d66bd94987a9f6b242a1 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Mon, 25 Aug 2025 17:38:29 +0700 Subject: [PATCH 3/7] =?UTF-8?q?Fix=20-=20Clicking=20the=20=E2=80=9CFrom?= =?UTF-8?q?=E2=80=9D=20link=20redirects=20to=20the=20conversation,=20but?= =?UTF-8?q?=20not=20to=20the=20specific=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx index 9dad2ca015b6..cb97ffec117a 100644 --- a/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx +++ b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx @@ -153,12 +153,10 @@ function useActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { if (popoverHeight) { if (previousElementOffset !== 0 || elementOffset > previousElementOffset) { const returnValue = elementOffset < 0 ? 0 : elementOffset; - return returnValue; return withSpring(returnValue, SPRING_CONFIG); } const returnValue = Math.max(previousElementOffset, 0); - return returnValue; return withSpring(returnValue, SPRING_CONFIG); } From 8ad459acb6b08a76fa8075c4bb5c7bcce05e3f4a Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Wed, 27 Aug 2025 12:10:59 +0700 Subject: [PATCH 4/7] =?UTF-8?q?Fix=20-=20Clicking=20the=20=E2=80=9CFrom?= =?UTF-8?q?=E2=80=9D=20link=20redirects=20to=20the=20conversation,=20but?= =?UTF-8?q?=20not=20to=20the=20specific=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActionSheetAwareScrollView/index.ios.tsx | 21 +- .../useActionSheetKeyboardSpace.tsx | 249 ------------------ 2 files changed, 10 insertions(+), 260 deletions(-) delete mode 100644 src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx diff --git a/src/components/ActionSheetAwareScrollView/index.ios.tsx b/src/components/ActionSheetAwareScrollView/index.ios.tsx index ce223dbdd028..00a154fae29d 100644 --- a/src/components/ActionSheetAwareScrollView/index.ios.tsx +++ b/src/components/ActionSheetAwareScrollView/index.ios.tsx @@ -6,7 +6,7 @@ import Reanimated, {useAnimatedRef, useAnimatedStyle} from 'react-native-reanima import {Actions, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider} from './ActionSheetAwareScrollViewContext'; import useActionSheetKeyboardSpacing from './useActionSheetKeyboardSpacing'; -const ActionSheetAwareScrollView = forwardRef>(({children, ...props}, ref) => { +const ActionSheetAwareScrollView = forwardRef>(({children, style, ...props}, ref) => { const scrollViewAnimatedRef = useAnimatedRef(); const onRef = useCallback( @@ -25,19 +25,18 @@ const ActionSheetAwareScrollView = forwardRef ({ - paddingBottom: spacing.get(), + paddingTop: spacing.get(), })); return ( - - - {children} - - + + {children} + ); }); diff --git a/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx deleted file mode 100644 index fd8c2074cc99..000000000000 --- a/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import {useContext, useEffect} from 'react'; -import {useKeyboardHandler} from 'react-native-keyboard-controller'; -import type Reanimated from 'react-native-reanimated'; -import {useAnimatedReaction, useDerivedValue, useScrollViewOffset, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; -import type {AnimatedRef} from 'react-native-reanimated'; -import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import {Actions, ActionSheetAwareScrollViewContext, States} from './ActionSheetAwareScrollViewContext'; - -const KeyboardState = { - UNKNOWN: 0, - OPENING: 1, - OPEN: 2, - CLOSING: 3, - CLOSED: 4, -}; - -const SPRING_CONFIG = { - mass: 3, - stiffness: 1000, - damping: 500, -}; - -const useAnimatedKeyboard = () => { - const state = useSharedValue(KeyboardState.UNKNOWN); - const height = useSharedValue(0); - const lastHeight = useSharedValue(0); - const heightWhenOpened = useSharedValue(0); - - useKeyboardHandler( - { - onStart: (e) => { - 'worklet'; - - // Save the last keyboard height - if (e.height !== 0) { - heightWhenOpened.set(e.height); - height.set(0); - } - height.set(heightWhenOpened.get()); - lastHeight.set(e.height); - state.set(e.height > 0 ? KeyboardState.OPENING : KeyboardState.CLOSING); - }, - onMove: (e) => { - 'worklet'; - - height.set(e.height); - }, - onEnd: (e) => { - 'worklet'; - - state.set(e.height > 0 ? KeyboardState.OPEN : KeyboardState.CLOSED); - height.set(e.height); - }, - }, - [], - ); - - return {state, height, heightWhenOpened}; -}; - -function useActionSheetKeyboardSpacing(scrollViewAnimatedRef: AnimatedRef) { - const position = useScrollViewOffset(scrollViewAnimatedRef); - - const { - unmodifiedPaddings: {top: paddingTop = 0, bottom: paddingBottom = 0}, - } = useSafeAreaPaddings(); - const keyboard = useAnimatedKeyboard(); - - // Similar to using `global` in worklet but it's just a local object - const syncLocalWorkletState = useSharedValue(KeyboardState.UNKNOWN); - const {windowHeight} = useWindowDimensions(); - const {currentActionSheetState, transitionActionSheetStateWorklet: transition, resetStateMachine} = useContext(ActionSheetAwareScrollViewContext); - - // Reset state machine when component unmounts - // eslint-disable-next-line arrow-body-style - useEffect(() => { - return () => resetStateMachine(); - }, [resetStateMachine]); - - useAnimatedReaction( - () => keyboard.state.get(), - (lastState) => { - if (lastState === syncLocalWorkletState.get()) { - return; - } - // eslint-disable-next-line react-compiler/react-compiler - syncLocalWorkletState.set(lastState); - - if (lastState === KeyboardState.OPEN) { - transition({type: Actions.OPEN_KEYBOARD}); - } else if (lastState === KeyboardState.CLOSED) { - transition({type: Actions.CLOSE_KEYBOARD}); - } - }, - [], - ); - - const spacing = useDerivedValue(() => { - const {current, previous} = currentActionSheetState.get(); - - // We don't need to run any additional logic. it will always return 0 for idle state - if (current.state === States.IDLE) { - return withSpring(0, SPRING_CONFIG); - } - - const keyboardHeight = keyboard.height.get() === 0 ? 0 : keyboard.height.get() - paddingBottom; - - // Sometimes we need to know the last keyboard height - const lastKeyboardHeight = keyboard.heightWhenOpened.get() - paddingBottom; - const {popoverHeight = 0, frameY, height} = current.payload ?? {}; - const invertedKeyboardHeight = keyboard.state.get() === KeyboardState.CLOSED ? lastKeyboardHeight : 0; - const elementOffset = frameY !== undefined && height !== undefined && popoverHeight !== undefined ? frameY + paddingTop + height - (windowHeight - popoverHeight) : 0; - - // when the state is not idle we know for sure we have the previous state - const previousPayload = previous.payload ?? {}; - const previousElementOffset = - previousPayload.frameY !== undefined && previousPayload.height !== undefined && previousPayload.popoverHeight !== undefined - ? previousPayload.frameY + paddingTop + previousPayload.height - (windowHeight - previousPayload.popoverHeight) - : 0; - - const isOpeningKeyboard = syncLocalWorkletState.get() === 1; - const isClosingKeyboard = syncLocalWorkletState.get() === 3; - const isClosedKeyboard = syncLocalWorkletState.get() === 4; - - // Depending on the current and sometimes previous state we can return - // either animation or just a value - switch (current.state) { - case States.KEYBOARD_OPEN: { - if (isClosedKeyboard || isOpeningKeyboard) { - return lastKeyboardHeight - keyboardHeight; - } - if (previous.state === States.KEYBOARD_CLOSED_POPOVER || (previous.state === States.KEYBOARD_OPEN && elementOffset < 0)) { - const returnValue = Math.max(keyboard.heightWhenOpened.get() - keyboard.height.get() - paddingBottom, 0) + Math.max(elementOffset, 0); - return returnValue; - } - return withSpring(0, SPRING_CONFIG); - } - - case States.POPOVER_CLOSED: { - return withSpring(0, SPRING_CONFIG, () => { - transition({ - type: Actions.END_TRANSITION, - }); - }); - } - - case States.POPOVER_OPEN: { - if (popoverHeight) { - if (previousElementOffset !== 0 || elementOffset > previousElementOffset) { - const returnValue = elementOffset < 0 ? 0 : elementOffset; - return withSpring(returnValue, SPRING_CONFIG); - } - - const returnValue = Math.max(previousElementOffset, 0); - return withSpring(returnValue, SPRING_CONFIG); - } - - return 0; - } - - case States.KEYBOARD_POPOVER_OPEN: { - if (keyboard.state.get() === KeyboardState.OPEN) { - return withSpring(0, SPRING_CONFIG); - } - - const nextOffset = elementOffset + lastKeyboardHeight; - const scrollOffset = position?.get() ?? 0; - - // Check if there's a space not filled by content and we need to move - const hasWhiteGap = - popoverHeight && - // Content would go too far up (beyond popover bounds) - (nextOffset < -popoverHeight || - // Or content would go below top of screen (only if not significantly scrolled) - (nextOffset > 0 && popoverHeight < lastKeyboardHeight && scrollOffset < popoverHeight) || - // Or content would create a gap by being positioned above minimum allowed position - (popoverHeight < lastKeyboardHeight && nextOffset > -popoverHeight && scrollOffset < popoverHeight) || - // Or there's a significant gap considering scroll position - (popoverHeight < lastKeyboardHeight && - scrollOffset > 0 && - scrollOffset < popoverHeight && - // When scrolled, check if the gap between content and keyboard would be too large - (nextOffset + scrollOffset > popoverHeight / 2 || - // Or if content would be pushed too far down relative to scroll - elementOffset + scrollOffset > -popoverHeight / 2))); - - if (keyboard.state.get() === KeyboardState.CLOSED) { - if (hasWhiteGap) { - return withSpring(nextOffset, SPRING_CONFIG); - } - - if (nextOffset > invertedKeyboardHeight) { - return withSpring(nextOffset < 0 ? 0 : nextOffset, SPRING_CONFIG); - } - } - - if (elementOffset < 0) { - const heightDifference = (frameY ?? 0) - keyboardHeight - paddingTop - paddingBottom; - if (isClosingKeyboard) { - if (hasWhiteGap) { - const targetOffset = Math.max(heightDifference - (scrollOffset > 0 ? scrollOffset / 2 : 0), -popoverHeight); - return withSequence(withTiming(keyboardHeight, {duration: 0}), withSpring(targetOffset, SPRING_CONFIG)); - } - - return withSpring(Math.max(elementOffset + lastKeyboardHeight, -popoverHeight), SPRING_CONFIG); - } - - if (hasWhiteGap && heightDifference > paddingTop) { - return withSequence(withTiming(lastKeyboardHeight - keyboardHeight, {duration: 0}), withSpring(Math.max(heightDifference, -popoverHeight), SPRING_CONFIG)); - } - - return lastKeyboardHeight - keyboardHeight; - } - - return lastKeyboardHeight; - } - - case States.KEYBOARD_CLOSED_POPOVER: { - if (elementOffset < 0) { - transition({type: Actions.END_TRANSITION}); - - return 0; - } - - if (keyboard.state.get() === KeyboardState.CLOSED) { - const returnValue = elementOffset + lastKeyboardHeight; - return returnValue; - } - - if (keyboard.height.get() > 0) { - const returnValue = keyboard.heightWhenOpened.get() - keyboard.height.get() + elementOffset; - return returnValue; - } - - return withTiming(elementOffset + lastKeyboardHeight, { - duration: 0, - }); - } - - default: - return 0; - } - }, []); - - return spacing; -} - -export default useActionSheetKeyboardSpacing; From 7b127d4d77488d9db51aa4d983165a7d02363e93 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Wed, 27 Aug 2025 12:15:21 +0700 Subject: [PATCH 5/7] =?UTF-8?q?Fix=20-=20Clicking=20the=20=E2=80=9CFrom?= =?UTF-8?q?=E2=80=9D=20link=20redirects=20to=20the=20conversation,=20but?= =?UTF-8?q?=20not=20to=20the=20specific=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActionSheetAwareScrollView/index.ios.tsx | 21 +- .../useActionSheetKeyboardSpace.tsx | 249 ++++++++++++++++++ 2 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx diff --git a/src/components/ActionSheetAwareScrollView/index.ios.tsx b/src/components/ActionSheetAwareScrollView/index.ios.tsx index 00a154fae29d..ce223dbdd028 100644 --- a/src/components/ActionSheetAwareScrollView/index.ios.tsx +++ b/src/components/ActionSheetAwareScrollView/index.ios.tsx @@ -6,7 +6,7 @@ import Reanimated, {useAnimatedRef, useAnimatedStyle} from 'react-native-reanima import {Actions, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider} from './ActionSheetAwareScrollViewContext'; import useActionSheetKeyboardSpacing from './useActionSheetKeyboardSpacing'; -const ActionSheetAwareScrollView = forwardRef>(({children, style, ...props}, ref) => { +const ActionSheetAwareScrollView = forwardRef>(({children, ...props}, ref) => { const scrollViewAnimatedRef = useAnimatedRef(); const onRef = useCallback( @@ -25,18 +25,19 @@ const ActionSheetAwareScrollView = forwardRef ({ - paddingTop: spacing.get(), + paddingBottom: spacing.get(), })); return ( - - {children} - + + + {children} + + ); }); diff --git a/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx new file mode 100644 index 000000000000..fd8c2074cc99 --- /dev/null +++ b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx @@ -0,0 +1,249 @@ +import {useContext, useEffect} from 'react'; +import {useKeyboardHandler} from 'react-native-keyboard-controller'; +import type Reanimated from 'react-native-reanimated'; +import {useAnimatedReaction, useDerivedValue, useScrollViewOffset, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; +import type {AnimatedRef} from 'react-native-reanimated'; +import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import {Actions, ActionSheetAwareScrollViewContext, States} from './ActionSheetAwareScrollViewContext'; + +const KeyboardState = { + UNKNOWN: 0, + OPENING: 1, + OPEN: 2, + CLOSING: 3, + CLOSED: 4, +}; + +const SPRING_CONFIG = { + mass: 3, + stiffness: 1000, + damping: 500, +}; + +const useAnimatedKeyboard = () => { + const state = useSharedValue(KeyboardState.UNKNOWN); + const height = useSharedValue(0); + const lastHeight = useSharedValue(0); + const heightWhenOpened = useSharedValue(0); + + useKeyboardHandler( + { + onStart: (e) => { + 'worklet'; + + // Save the last keyboard height + if (e.height !== 0) { + heightWhenOpened.set(e.height); + height.set(0); + } + height.set(heightWhenOpened.get()); + lastHeight.set(e.height); + state.set(e.height > 0 ? KeyboardState.OPENING : KeyboardState.CLOSING); + }, + onMove: (e) => { + 'worklet'; + + height.set(e.height); + }, + onEnd: (e) => { + 'worklet'; + + state.set(e.height > 0 ? KeyboardState.OPEN : KeyboardState.CLOSED); + height.set(e.height); + }, + }, + [], + ); + + return {state, height, heightWhenOpened}; +}; + +function useActionSheetKeyboardSpacing(scrollViewAnimatedRef: AnimatedRef) { + const position = useScrollViewOffset(scrollViewAnimatedRef); + + const { + unmodifiedPaddings: {top: paddingTop = 0, bottom: paddingBottom = 0}, + } = useSafeAreaPaddings(); + const keyboard = useAnimatedKeyboard(); + + // Similar to using `global` in worklet but it's just a local object + const syncLocalWorkletState = useSharedValue(KeyboardState.UNKNOWN); + const {windowHeight} = useWindowDimensions(); + const {currentActionSheetState, transitionActionSheetStateWorklet: transition, resetStateMachine} = useContext(ActionSheetAwareScrollViewContext); + + // Reset state machine when component unmounts + // eslint-disable-next-line arrow-body-style + useEffect(() => { + return () => resetStateMachine(); + }, [resetStateMachine]); + + useAnimatedReaction( + () => keyboard.state.get(), + (lastState) => { + if (lastState === syncLocalWorkletState.get()) { + return; + } + // eslint-disable-next-line react-compiler/react-compiler + syncLocalWorkletState.set(lastState); + + if (lastState === KeyboardState.OPEN) { + transition({type: Actions.OPEN_KEYBOARD}); + } else if (lastState === KeyboardState.CLOSED) { + transition({type: Actions.CLOSE_KEYBOARD}); + } + }, + [], + ); + + const spacing = useDerivedValue(() => { + const {current, previous} = currentActionSheetState.get(); + + // We don't need to run any additional logic. it will always return 0 for idle state + if (current.state === States.IDLE) { + return withSpring(0, SPRING_CONFIG); + } + + const keyboardHeight = keyboard.height.get() === 0 ? 0 : keyboard.height.get() - paddingBottom; + + // Sometimes we need to know the last keyboard height + const lastKeyboardHeight = keyboard.heightWhenOpened.get() - paddingBottom; + const {popoverHeight = 0, frameY, height} = current.payload ?? {}; + const invertedKeyboardHeight = keyboard.state.get() === KeyboardState.CLOSED ? lastKeyboardHeight : 0; + const elementOffset = frameY !== undefined && height !== undefined && popoverHeight !== undefined ? frameY + paddingTop + height - (windowHeight - popoverHeight) : 0; + + // when the state is not idle we know for sure we have the previous state + const previousPayload = previous.payload ?? {}; + const previousElementOffset = + previousPayload.frameY !== undefined && previousPayload.height !== undefined && previousPayload.popoverHeight !== undefined + ? previousPayload.frameY + paddingTop + previousPayload.height - (windowHeight - previousPayload.popoverHeight) + : 0; + + const isOpeningKeyboard = syncLocalWorkletState.get() === 1; + const isClosingKeyboard = syncLocalWorkletState.get() === 3; + const isClosedKeyboard = syncLocalWorkletState.get() === 4; + + // Depending on the current and sometimes previous state we can return + // either animation or just a value + switch (current.state) { + case States.KEYBOARD_OPEN: { + if (isClosedKeyboard || isOpeningKeyboard) { + return lastKeyboardHeight - keyboardHeight; + } + if (previous.state === States.KEYBOARD_CLOSED_POPOVER || (previous.state === States.KEYBOARD_OPEN && elementOffset < 0)) { + const returnValue = Math.max(keyboard.heightWhenOpened.get() - keyboard.height.get() - paddingBottom, 0) + Math.max(elementOffset, 0); + return returnValue; + } + return withSpring(0, SPRING_CONFIG); + } + + case States.POPOVER_CLOSED: { + return withSpring(0, SPRING_CONFIG, () => { + transition({ + type: Actions.END_TRANSITION, + }); + }); + } + + case States.POPOVER_OPEN: { + if (popoverHeight) { + if (previousElementOffset !== 0 || elementOffset > previousElementOffset) { + const returnValue = elementOffset < 0 ? 0 : elementOffset; + return withSpring(returnValue, SPRING_CONFIG); + } + + const returnValue = Math.max(previousElementOffset, 0); + return withSpring(returnValue, SPRING_CONFIG); + } + + return 0; + } + + case States.KEYBOARD_POPOVER_OPEN: { + if (keyboard.state.get() === KeyboardState.OPEN) { + return withSpring(0, SPRING_CONFIG); + } + + const nextOffset = elementOffset + lastKeyboardHeight; + const scrollOffset = position?.get() ?? 0; + + // Check if there's a space not filled by content and we need to move + const hasWhiteGap = + popoverHeight && + // Content would go too far up (beyond popover bounds) + (nextOffset < -popoverHeight || + // Or content would go below top of screen (only if not significantly scrolled) + (nextOffset > 0 && popoverHeight < lastKeyboardHeight && scrollOffset < popoverHeight) || + // Or content would create a gap by being positioned above minimum allowed position + (popoverHeight < lastKeyboardHeight && nextOffset > -popoverHeight && scrollOffset < popoverHeight) || + // Or there's a significant gap considering scroll position + (popoverHeight < lastKeyboardHeight && + scrollOffset > 0 && + scrollOffset < popoverHeight && + // When scrolled, check if the gap between content and keyboard would be too large + (nextOffset + scrollOffset > popoverHeight / 2 || + // Or if content would be pushed too far down relative to scroll + elementOffset + scrollOffset > -popoverHeight / 2))); + + if (keyboard.state.get() === KeyboardState.CLOSED) { + if (hasWhiteGap) { + return withSpring(nextOffset, SPRING_CONFIG); + } + + if (nextOffset > invertedKeyboardHeight) { + return withSpring(nextOffset < 0 ? 0 : nextOffset, SPRING_CONFIG); + } + } + + if (elementOffset < 0) { + const heightDifference = (frameY ?? 0) - keyboardHeight - paddingTop - paddingBottom; + if (isClosingKeyboard) { + if (hasWhiteGap) { + const targetOffset = Math.max(heightDifference - (scrollOffset > 0 ? scrollOffset / 2 : 0), -popoverHeight); + return withSequence(withTiming(keyboardHeight, {duration: 0}), withSpring(targetOffset, SPRING_CONFIG)); + } + + return withSpring(Math.max(elementOffset + lastKeyboardHeight, -popoverHeight), SPRING_CONFIG); + } + + if (hasWhiteGap && heightDifference > paddingTop) { + return withSequence(withTiming(lastKeyboardHeight - keyboardHeight, {duration: 0}), withSpring(Math.max(heightDifference, -popoverHeight), SPRING_CONFIG)); + } + + return lastKeyboardHeight - keyboardHeight; + } + + return lastKeyboardHeight; + } + + case States.KEYBOARD_CLOSED_POPOVER: { + if (elementOffset < 0) { + transition({type: Actions.END_TRANSITION}); + + return 0; + } + + if (keyboard.state.get() === KeyboardState.CLOSED) { + const returnValue = elementOffset + lastKeyboardHeight; + return returnValue; + } + + if (keyboard.height.get() > 0) { + const returnValue = keyboard.heightWhenOpened.get() - keyboard.height.get() + elementOffset; + return returnValue; + } + + return withTiming(elementOffset + lastKeyboardHeight, { + duration: 0, + }); + } + + default: + return 0; + } + }, []); + + return spacing; +} + +export default useActionSheetKeyboardSpacing; From b9b5043d96b8c6ac5137622e7a69485b5a4777e3 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Wed, 27 Aug 2025 12:20:07 +0700 Subject: [PATCH 6/7] =?UTF-8?q?Fix=20-=20Clicking=20the=20=E2=80=9CFrom?= =?UTF-8?q?=E2=80=9D=20link=20redirects=20to=20the=20conversation,=20but?= =?UTF-8?q?=20not=20to=20the=20specific=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActionSheetAwareScrollView/index.ios.tsx | 11 +++++---- .../useActionSheetKeyboardSpace.tsx | 24 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/components/ActionSheetAwareScrollView/index.ios.tsx b/src/components/ActionSheetAwareScrollView/index.ios.tsx index 9c465b966daf..8f2dde9646f5 100644 --- a/src/components/ActionSheetAwareScrollView/index.ios.tsx +++ b/src/components/ActionSheetAwareScrollView/index.ios.tsx @@ -4,9 +4,9 @@ import React, {forwardRef, useCallback} from 'react'; import type {ScrollView, ScrollViewProps} from 'react-native'; import Reanimated, {useAnimatedRef, useScrollViewOffset} from 'react-native-reanimated'; import {Actions, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider} from './ActionSheetAwareScrollViewContext'; -import ActionSheetKeyboardSpace from './ActionSheetKeyboardSpace'; +import useActionSheetKeyboardSpace from './useActionSheetKeyboardSpace'; -const ActionSheetAwareScrollView = forwardRef>((props, ref) => { +const ActionSheetAwareScrollView = forwardRef>(({style, children, ...rest}, ref) => { const scrollViewAnimatedRef = useAnimatedRef(); const position = useScrollViewOffset(scrollViewAnimatedRef); @@ -24,13 +24,16 @@ const ActionSheetAwareScrollView = forwardRef - {props.children} + {children} ); }); diff --git a/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx index fd8c2074cc99..cb97ffec117a 100644 --- a/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx +++ b/src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpace.tsx @@ -1,8 +1,8 @@ import {useContext, useEffect} from 'react'; +import type {ViewProps} from 'react-native'; import {useKeyboardHandler} from 'react-native-keyboard-controller'; -import type Reanimated from 'react-native-reanimated'; -import {useAnimatedReaction, useDerivedValue, useScrollViewOffset, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; -import type {AnimatedRef} from 'react-native-reanimated'; +import {useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; +import type {SharedValue} from 'react-native-reanimated'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {Actions, ActionSheetAwareScrollViewContext, States} from './ActionSheetAwareScrollViewContext'; @@ -59,13 +59,17 @@ const useAnimatedKeyboard = () => { return {state, height, heightWhenOpened}; }; -function useActionSheetKeyboardSpacing(scrollViewAnimatedRef: AnimatedRef) { - const position = useScrollViewOffset(scrollViewAnimatedRef); +type ActionSheetKeyboardSpaceProps = ViewProps & { + /** scroll offset of the parent ScrollView */ + position?: SharedValue; +}; +function useActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { const { unmodifiedPaddings: {top: paddingTop = 0, bottom: paddingBottom = 0}, } = useSafeAreaPaddings(); const keyboard = useAnimatedKeyboard(); + const {position} = props; // Similar to using `global` in worklet but it's just a local object const syncLocalWorkletState = useSharedValue(KeyboardState.UNKNOWN); @@ -96,7 +100,7 @@ function useActionSheetKeyboardSpacing(scrollViewAnimatedRef: AnimatedRef { + const translateY = useDerivedValue(() => { const {current, previous} = currentActionSheetState.get(); // We don't need to run any additional logic. it will always return 0 for idle state @@ -243,7 +247,11 @@ function useActionSheetKeyboardSpacing(scrollViewAnimatedRef: AnimatedRef ({ + paddingTop: translateY.get(), + })); + + return {animatedStyle}; } -export default useActionSheetKeyboardSpacing; +export default useActionSheetKeyboardSpace; From 2ac99b283dc04f5ea23cad71abbc6aba1d0f22e7 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Thu, 28 Aug 2025 10:44:27 +0700 Subject: [PATCH 7/7] remove ActionSheetKeyboardSpace --- .../ActionSheetKeyboardSpace.tsx | 267 ------------------ 1 file changed, 267 deletions(-) delete mode 100644 src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx diff --git a/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx b/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx deleted file mode 100644 index cf96a0823b0f..000000000000 --- a/src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import React, {useContext, useEffect} from 'react'; -import type {ViewProps} from 'react-native'; -import {useKeyboardHandler} from 'react-native-keyboard-controller'; -import Reanimated, {useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSequence, withSpring, withTiming} from 'react-native-reanimated'; -import type {SharedValue} from 'react-native-reanimated'; -import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import {Actions, ActionSheetAwareScrollViewContext, States} from './ActionSheetAwareScrollViewContext'; - -const KeyboardState = { - UNKNOWN: 0, - OPENING: 1, - OPEN: 2, - CLOSING: 3, - CLOSED: 4, -}; - -const SPRING_CONFIG = { - mass: 3, - stiffness: 1000, - damping: 500, -}; - -const useAnimatedKeyboard = () => { - const state = useSharedValue(KeyboardState.UNKNOWN); - const height = useSharedValue(0); - const lastHeight = useSharedValue(0); - const heightWhenOpened = useSharedValue(0); - - useKeyboardHandler( - { - onStart: (e) => { - 'worklet'; - - // Save the last keyboard height - if (e.height !== 0) { - heightWhenOpened.set(e.height); - height.set(0); - } - height.set(heightWhenOpened.get()); - lastHeight.set(e.height); - state.set(e.height > 0 ? KeyboardState.OPENING : KeyboardState.CLOSING); - }, - onMove: (e) => { - 'worklet'; - - height.set(e.height); - }, - onEnd: (e) => { - 'worklet'; - - state.set(e.height > 0 ? KeyboardState.OPEN : KeyboardState.CLOSED); - height.set(e.height); - }, - }, - [], - ); - - return {state, height, heightWhenOpened}; -}; - -type ActionSheetKeyboardSpaceProps = ViewProps & { - /** scroll offset of the parent ScrollView */ - position?: SharedValue; -}; - -function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) { - const styles = useThemeStyles(); - const { - unmodifiedPaddings: {top: paddingTop = 0, bottom: paddingBottom = 0}, - } = useSafeAreaPaddings(); - const keyboard = useAnimatedKeyboard(); - const {position} = props; - - // Similar to using `global` in worklet but it's just a local object - const syncLocalWorkletState = useSharedValue(KeyboardState.UNKNOWN); - const {windowHeight} = useWindowDimensions(); - const {currentActionSheetState, transitionActionSheetStateWorklet: transition, resetStateMachine} = useContext(ActionSheetAwareScrollViewContext); - - // Reset state machine when component unmounts - // eslint-disable-next-line arrow-body-style - useEffect(() => { - return () => resetStateMachine(); - }, [resetStateMachine]); - - useAnimatedReaction( - () => keyboard.state.get(), - (lastState) => { - if (lastState === syncLocalWorkletState.get()) { - return; - } - // eslint-disable-next-line react-compiler/react-compiler - syncLocalWorkletState.set(lastState); - - if (lastState === KeyboardState.OPEN) { - transition({type: Actions.OPEN_KEYBOARD}); - } else if (lastState === KeyboardState.CLOSED) { - transition({type: Actions.CLOSE_KEYBOARD}); - } - }, - [], - ); - - const translateY = useDerivedValue(() => { - const {current, previous} = currentActionSheetState.get(); - - // We don't need to run any additional logic. it will always return 0 for idle state - if (current.state === States.IDLE) { - return withSpring(0, SPRING_CONFIG); - } - - const keyboardHeight = keyboard.height.get() === 0 ? 0 : keyboard.height.get() - paddingBottom; - - // Sometimes we need to know the last keyboard height - const lastKeyboardHeight = keyboard.heightWhenOpened.get() - paddingBottom; - const {popoverHeight = 0, frameY, height} = current.payload ?? {}; - const invertedKeyboardHeight = keyboard.state.get() === KeyboardState.CLOSED ? lastKeyboardHeight : 0; - const elementOffset = frameY !== undefined && height !== undefined && popoverHeight !== undefined ? frameY + paddingTop + height - (windowHeight - popoverHeight) : 0; - - // when the state is not idle we know for sure we have the previous state - const previousPayload = previous.payload ?? {}; - const previousElementOffset = - previousPayload.frameY !== undefined && previousPayload.height !== undefined && previousPayload.popoverHeight !== undefined - ? previousPayload.frameY + paddingTop + previousPayload.height - (windowHeight - previousPayload.popoverHeight) - : 0; - - const isOpeningKeyboard = syncLocalWorkletState.get() === 1; - const isClosingKeyboard = syncLocalWorkletState.get() === 3; - const isClosedKeyboard = syncLocalWorkletState.get() === 4; - - // Depending on the current and sometimes previous state we can return - // either animation or just a value - switch (current.state) { - case States.KEYBOARD_OPEN: { - if (isClosedKeyboard || isOpeningKeyboard) { - return lastKeyboardHeight - keyboardHeight; - } - if (previous.state === States.KEYBOARD_CLOSED_POPOVER || (previous.state === States.KEYBOARD_OPEN && elementOffset < 0)) { - const returnValue = Math.max(keyboard.heightWhenOpened.get() - keyboard.height.get() - paddingBottom, 0) + Math.max(elementOffset, 0); - return returnValue; - } - return withSpring(0, SPRING_CONFIG); - } - - case States.POPOVER_CLOSED: { - return withSpring(0, SPRING_CONFIG, () => { - transition({ - type: Actions.END_TRANSITION, - }); - }); - } - - case States.POPOVER_OPEN: { - if (popoverHeight) { - if (previousElementOffset !== 0 || elementOffset > previousElementOffset) { - const returnValue = elementOffset < 0 ? 0 : elementOffset; - return withSpring(returnValue, SPRING_CONFIG); - } - - const returnValue = Math.max(previousElementOffset, 0); - return withSpring(returnValue, SPRING_CONFIG); - } - - return 0; - } - - case States.KEYBOARD_POPOVER_OPEN: { - if (keyboard.state.get() === KeyboardState.OPEN) { - return withSpring(0, SPRING_CONFIG); - } - - const nextOffset = elementOffset + lastKeyboardHeight; - const scrollOffset = position?.get() ?? 0; - - // Check if there's a space not filled by content and we need to move - const hasWhiteGap = - popoverHeight && - // Content would go too far up (beyond popover bounds) - (nextOffset < -popoverHeight || - // Or content would go below top of screen (only if not significantly scrolled) - (nextOffset > 0 && popoverHeight < lastKeyboardHeight && scrollOffset < popoverHeight) || - // Or content would create a gap by being positioned above minimum allowed position - (popoverHeight < lastKeyboardHeight && nextOffset > -popoverHeight && scrollOffset < popoverHeight) || - // Or there's a significant gap considering scroll position - (popoverHeight < lastKeyboardHeight && - scrollOffset > 0 && - scrollOffset < popoverHeight && - // When scrolled, check if the gap between content and keyboard would be too large - (nextOffset + scrollOffset > popoverHeight / 2 || - // Or if content would be pushed too far down relative to scroll - elementOffset + scrollOffset > -popoverHeight / 2))); - - if (keyboard.state.get() === KeyboardState.CLOSED) { - if (hasWhiteGap) { - return withSpring(nextOffset, SPRING_CONFIG); - } - - if (nextOffset > invertedKeyboardHeight) { - return withSpring(nextOffset < 0 ? 0 : nextOffset, SPRING_CONFIG); - } - } - - if (elementOffset < 0) { - const heightDifference = (frameY ?? 0) - keyboardHeight - paddingTop - paddingBottom; - if (isClosingKeyboard) { - if (hasWhiteGap) { - const targetOffset = Math.max(heightDifference - (scrollOffset > 0 ? scrollOffset / 2 : 0), -popoverHeight); - return withSequence(withTiming(keyboardHeight, {duration: 0}), withSpring(targetOffset, SPRING_CONFIG)); - } - - return withSpring(Math.max(elementOffset + lastKeyboardHeight, -popoverHeight), SPRING_CONFIG); - } - - if (hasWhiteGap && heightDifference > paddingTop) { - return withSequence(withTiming(lastKeyboardHeight - keyboardHeight, {duration: 0}), withSpring(Math.max(heightDifference, -popoverHeight), SPRING_CONFIG)); - } - - return lastKeyboardHeight - keyboardHeight; - } - - return lastKeyboardHeight; - } - - case States.KEYBOARD_CLOSED_POPOVER: { - if (elementOffset < 0) { - transition({type: Actions.END_TRANSITION}); - - return 0; - } - - if (keyboard.state.get() === KeyboardState.CLOSED) { - const returnValue = elementOffset + lastKeyboardHeight; - return returnValue; - } - - if (keyboard.height.get() > 0) { - const returnValue = keyboard.heightWhenOpened.get() - keyboard.height.get() + elementOffset; - return returnValue; - } - - return withTiming(elementOffset + lastKeyboardHeight, { - duration: 0, - }); - } - - default: - return 0; - } - }, []); - - const animatedStyle = useAnimatedStyle(() => ({ - paddingTop: translateY.get(), - })); - - return ( - - ); -} - -ActionSheetKeyboardSpace.displayName = 'ActionSheetKeyboardSpace'; - -export default ActionSheetKeyboardSpace;