From 65be6cf4e7bfd7a6275558177a31e00ed389d555 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Fri, 6 Feb 2026 13:33:18 -0300 Subject: [PATCH 1/8] feat: basic setup react-native-true-sheet --- app/containers/ActionSheet/ActionSheet.tsx | 156 ++++-------------- .../ActionSheet/BottomSheetContent.tsx | 11 +- app/containers/TextInput/FormTextInput.tsx | 3 +- ios/Podfile.lock | 30 +++- jest.setup.js | 18 +- package.json | 2 +- patches/@discord+bottom-sheet+4.6.1.patch | 70 -------- yarn.lock | 12 +- 8 files changed, 90 insertions(+), 212 deletions(-) delete mode 100644 patches/@discord+bottom-sheet+4.6.1.patch diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 65f65d551dc..81bc1874bee 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -1,98 +1,36 @@ import { useBackHandler } from '@react-native-community/hooks'; import * as Haptics from 'expo-haptics'; -import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react'; -import { Keyboard, type LayoutChangeEvent, useWindowDimensions } from 'react-native'; -import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated'; -import BottomSheet, { BottomSheetBackdrop, type BottomSheetBackdropProps } from '@discord/bottom-sheet'; +import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { Keyboard, useWindowDimensions } from 'react-native'; +import { TrueSheet } from '@lodev09/react-native-true-sheet'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../theme'; -import { isIOS, isTablet } from '../../lib/methods/helpers'; +import { isTablet } from '../../lib/methods/helpers'; import { Handle } from './Handle'; import { type TActionSheetOptions } from './Provider'; import BottomSheetContent from './BottomSheetContent'; import styles from './styles'; export const ACTION_SHEET_ANIMATION_DURATION = 250; -const HANDLE_HEIGHT = 28; -const CANCEL_HEIGHT = 64; - -const ANIMATION_CONFIG = { - duration: ACTION_SHEET_ANIMATION_DURATION, - // https://easings.net/#easeInOutCubic - easing: Easing.bezier(0.645, 0.045, 0.355, 1.0) -}; const ActionSheet = React.memo( forwardRef(({ children }: { children: React.ReactElement }, ref) => { const { colors } = useTheme(); const { height: windowHeight } = useWindowDimensions(); - const { bottom, right, left } = useSafeAreaInsets(); - const { fontScale } = useWindowDimensions(); - const itemHeight = 48 * fontScale; - const bottomSheetRef = useRef(null); + const { right, left } = useSafeAreaInsets(); + const sheetRef = useRef(null); const [data, setData] = useState({} as TActionSheetOptions); const [isVisible, setVisible] = useState(false); - const animatedContentHeight = useSharedValue(0); - const animatedHandleHeight = useSharedValue(0); - const animatedDataSnaps = useSharedValue([]); - const animatedSnapPoints = useDerivedValue(() => { - if (animatedDataSnaps.value?.length) { - return animatedDataSnaps.value; - } - const contentWithHandleHeight = animatedContentHeight.value + animatedHandleHeight.value; - // Bottom sheet requires a default value to work - if (contentWithHandleHeight === 0) { - return ['25%']; - } - return [contentWithHandleHeight]; - }, [data]); - - const handleContentLayout = useCallback( - ({ - nativeEvent: { - layout: { height } - } - }: LayoutChangeEvent) => { - /** - * This logic is only necessary to prevent the action sheet from - * occupying the entire screen when the dynamic content is too big. - */ - animatedContentHeight.value = Math.min(height, windowHeight * 0.8); - }, - [animatedContentHeight, windowHeight] - ); - - const maxSnap = Math.min( - (itemHeight + 0.5) * (data?.options?.length || 0) + - HANDLE_HEIGHT + - // Custom header height - (data?.headerHeight || 0) + - // Insets bottom height (Notch devices) - bottom + - // Cancel button height - (data?.hasCancel ? CANCEL_HEIGHT : 0), - windowHeight * 0.8 - ); - - /* - * if the action sheet cover more than 60% of the screen height, - * we'll provide more one snap of 50% - */ - const snaps = maxSnap > windowHeight * 0.6 && !data.snaps ? ['50%', maxSnap] : [maxSnap]; - - const toggleVisible = () => setVisible(!isVisible); const hide = () => { - bottomSheetRef.current?.close(); + sheetRef.current?.dismiss(); }; const show = (options: TActionSheetOptions) => { setData(options); - if (options.snaps?.length) { - animatedDataSnaps.value = options.snaps; - } - toggleVisible(); + setVisible(true); + sheetRef.current?.present(0); }; useBackHandler(() => { @@ -114,69 +52,43 @@ const ActionSheet = React.memo( hideActionSheet: hide })); - const renderHandle = () => ( + const renderHeader = () => ( <> {isValidElement(data?.customHeader) ? data.customHeader : null} ); - const onClose = () => { - toggleVisible(); - data?.onClose && data?.onClose(); - animatedDataSnaps.value = []; + const onDidDismiss = () => { + setVisible(false); + data?.onClose?.(); }; - const renderBackdrop = useCallback( - (props: BottomSheetBackdropProps) => ( - - ), - [] - ); - - const bottomSheet = isTablet ? styles.bottomSheet : { marginRight: right, marginLeft: left }; - - // Must need this prop to avoid keyboard dismiss - // when is android tablet and the input text is focused - const androidTablet: any = isTablet && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {}; + const bottomSheetStyle = isTablet ? styles.bottomSheet : { marginRight: right, marginLeft: left }; return ( <> {children} - {isVisible && ( - index === -1 && onClose()} - // We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker - enableContentPanningGesture={data?.enableContentPanningGesture ?? true} - {...androidTablet}> - - - )} + + {}} + /> + ); }) diff --git a/app/containers/ActionSheet/BottomSheetContent.tsx b/app/containers/ActionSheet/BottomSheetContent.tsx index 267c4b7a20d..a723280596c 100644 --- a/app/containers/ActionSheet/BottomSheetContent.tsx +++ b/app/containers/ActionSheet/BottomSheetContent.tsx @@ -1,10 +1,10 @@ -import { Text, useWindowDimensions, type ViewProps } from 'react-native'; +import { FlatList, Text, useWindowDimensions, View, type ViewProps } from 'react-native'; import React from 'react'; -import { BottomSheetView, BottomSheetFlatList } from '@discord/bottom-sheet'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import I18n from '../../i18n'; import { useTheme } from '../../theme'; +import { isAndroid } from '../../lib/methods/helpers'; import { type IActionSheetItem, Item } from './Item'; import { type TActionSheetOptionsItem } from './Provider'; import styles from './styles'; @@ -41,7 +41,7 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL if (options) { return ( - ); } return ( - + {children} - + ); }); diff --git a/app/containers/TextInput/FormTextInput.tsx b/app/containers/TextInput/FormTextInput.tsx index 046cdab1e35..e0e11604e37 100644 --- a/app/containers/TextInput/FormTextInput.tsx +++ b/app/containers/TextInput/FormTextInput.tsx @@ -9,7 +9,6 @@ import { View, type ViewStyle } from 'react-native'; -import { BottomSheetTextInput } from '@discord/bottom-sheet'; import Touchable from 'react-native-platform-touchable'; import { A11y } from 'react-native-a11y-order'; @@ -124,7 +123,7 @@ export const FormTextInput = ({ const { colors } = useTheme(); const [showPassword, setShowPassword] = useState(false); const showClearInput = onClearInput && value && value.length > 0; - const Input = bottomSheet ? BottomSheetTextInput : TextInput; + const Input = TextInput; const inputError = getInputError(error); const accessibilityLabelText = useMemo(() => { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8d95a39e6b8..62012b99777 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2606,6 +2606,30 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNTrueSheet (3.6.11): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - SDWebImage (5.21.0): - SDWebImage/Core (= 5.21.0) - SDWebImage/Core (5.21.0) @@ -2754,6 +2778,7 @@ DEPENDENCIES: - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) + - "RNTrueSheet (from `../node_modules/@lodev09/react-native-true-sheet`)" - "simdjson (from `../node_modules/@nozbe/simdjson`)" - "WatermelonDB (from `../node_modules/@nozbe/watermelondb`)" - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -3030,6 +3055,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-screens" RNSVG: :path: "../node_modules/react-native-svg" + RNTrueSheet: + :path: "../node_modules/@lodev09/react-native-true-sheet" simdjson: :path: "../node_modules/@nozbe/simdjson" WatermelonDB: @@ -3086,7 +3113,7 @@ SPEC CHECKSUMS: nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 + RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 RCTDeprecation: 0418ac97b9f53b2e37f473da1663ef3061e46beb RCTRequired: b9fde7f981b11aa898f03a70d3d4d36b80f1b16d RCTTypeSafety: 397515ea9a8122b62a7a310adf30205f0a5e3bfc @@ -3177,6 +3204,7 @@ SPEC CHECKSUMS: RNReanimated: f52ccd5ceea2bae48d7421eec89b3f0c10d7b642 RNScreens: b13e4c45f0406f33986a39c0d8da0324bff94435 RNSVG: 680e961f640e381aab730a04b2371969686ed9f7 + RNTrueSheet: c32094ce4b285bf7f5208c10d9a84ab867fe2207 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c diff --git a/jest.setup.js b/jest.setup.js index 774f02d2527..f8f6f2877e4 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -155,12 +155,22 @@ jest.mock('expo-device', () => ({ isDevice: true })); -jest.mock('@discord/bottom-sheet', () => { - const react = require('react-native'); +jest.mock('@lodev09/react-native-true-sheet', () => { + const React = require('react'); + const { View } = require('react-native'); + const TrueSheet = React.forwardRef((props, ref) => { + React.useImperativeHandle(ref, () => ({ + present: () => Promise.resolve(), + dismiss: () => Promise.resolve(), + resize: () => Promise.resolve() + })); + return ; + }); + TrueSheet.displayName = 'TrueSheet'; return { __esModule: true, - default: react.View, - BottomSheetScrollView: react.ScrollView + TrueSheet, + TrueSheetProvider: ({ children }) => children }; }); diff --git a/package.json b/package.json index 30821c7a692..9838a5b4d26 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@bugsnag/react-native": "8.4.0", - "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", + "@lodev09/react-native-true-sheet": "~3.6.0", "@expo/vector-icons": "^14.1.0", "@hookform/resolvers": "^2.9.10", "@nozbe/watermelondb": "^0.28.1-0", diff --git a/patches/@discord+bottom-sheet+4.6.1.patch b/patches/@discord+bottom-sheet+4.6.1.patch deleted file mode 100644 index 0dff575d79f..00000000000 --- a/patches/@discord+bottom-sheet+4.6.1.patch +++ /dev/null @@ -1,70 +0,0 @@ -diff --git a/node_modules/@discord/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx b/node_modules/@discord/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx -index 2897fef..9a8505e 100644 ---- a/node_modules/@discord/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx -+++ b/node_modules/@discord/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx -@@ -1382,7 +1382,8 @@ const BottomSheetComponent = forwardRef( - if (containerHeight !== _previousContainerHeight) { - animationSource = ANIMATION_SOURCE.CONTAINER_RESIZE; - animationConfig = { -- duration: 0, -+ // https://github.com/gorhom/react-native-bottom-sheet/pull/1497 -+ duration: 1, - }; - } - } -diff --git a/node_modules/@discord/bottom-sheet/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx b/node_modules/@discord/bottom-sheet/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx -index 2219e0f..59f90ba 100644 ---- a/node_modules/@discord/bottom-sheet/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx -+++ b/node_modules/@discord/bottom-sheet/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx -@@ -92,10 +92,6 @@ function BottomSheetHandleContainerComponent({ - > - - any; - * https://gist.github.com/JakeCoxon/c7ebf6e6496f8468226fd36b596e1985 - */ - export const useStableCallback = (callback: Callback) => { -+ // @ts-ignore - const callbackRef = useRef(); - const memoCallback = useCallback( - (...args: any) => callbackRef.current && callbackRef.current(...args), -@@ -13,6 +14,7 @@ export const useStableCallback = (callback: Callback) => { - ); - useEffect(() => { - callbackRef.current = callback; -+ // @ts-ignore - return () => (callbackRef.current = undefined); - }); - return memoCallback; -diff --git a/node_modules/@discord/bottom-sheet/src/utilities/animate.ts b/node_modules/@discord/bottom-sheet/src/utilities/animate.ts -index 0ce4c9a..9562675 100644 ---- a/node_modules/@discord/bottom-sheet/src/utilities/animate.ts -+++ b/node_modules/@discord/bottom-sheet/src/utilities/animate.ts -@@ -4,6 +4,7 @@ import { - withTiming, - withSpring, - AnimationCallback, -+ ReduceMotion, - } from 'react-native-reanimated'; - import { ANIMATION_CONFIGS, ANIMATION_METHOD } from '../constants'; - -@@ -26,6 +27,8 @@ export const animate = ({ - configs = ANIMATION_CONFIGS; - } - -+ configs.reduceMotion = ReduceMotion.Never; -+ - // detect animation type - const type = - 'duration' in configs || 'easing' in configs diff --git a/yarn.lock b/yarn.lock index 4a19e871830..c4fa6694b13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2457,13 +2457,6 @@ glob "^7.1.6" read-pkg-up "^7.0.1" -"@discord/bottom-sheet@bluesky-social/react-native-bottom-sheet": - version "4.6.1" - resolved "https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908" - dependencies: - "@gorhom/portal" "1.0.14" - invariant "^2.2.4" - "@egjs/hammerjs@^2.0.17": version "2.0.17" resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" @@ -3898,6 +3891,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@lodev09/react-native-true-sheet@~3.6.0": + version "3.6.11" + resolved "https://registry.yarnpkg.com/@lodev09/react-native-true-sheet/-/react-native-true-sheet-3.6.11.tgz#12bd378b1ee3d2ba5bc04f5b654916233f356583" + integrity sha512-Je7zXgKrLUUnIjj+fXASiOg4PDXUPeD510awlZU5vkDXDhfLZ4gMqeYE/I93nZPI5AC3qQXFQ4Zek3JnhzwgIw== + "@napi-rs/wasm-runtime@^0.2.11": version "0.2.12" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" From 08056fb1db0707341672b7cf316bd8fbb7b8260b Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Fri, 6 Feb 2026 14:28:10 -0300 Subject: [PATCH 2/8] fix: scroll height --- app/containers/ActionSheet/ActionSheet.tsx | 74 ++++++++++++++++++++-- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 81bc1874bee..1ddcfb6ed00 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -1,8 +1,9 @@ import { useBackHandler } from '@react-native-community/hooks'; import * as Haptics from 'expo-haptics'; -import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react'; -import { Keyboard, useWindowDimensions } from 'react-native'; +import React, { forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { Keyboard, type LayoutChangeEvent, useWindowDimensions } from 'react-native'; import { TrueSheet } from '@lodev09/react-native-true-sheet'; +import type { SheetDetent } from '@lodev09/react-native-true-sheet'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../theme'; @@ -13,15 +14,44 @@ import BottomSheetContent from './BottomSheetContent'; import styles from './styles'; export const ACTION_SHEET_ANIMATION_DURATION = 250; +const ACTION_SHEET_MAX_HEIGHT_FRACTION = 0.75; +const HANDLE_HEIGHT = 28; +const CANCEL_HEIGHT = 64; + +function normalizeSnapsToDetents(snaps: (string | number)[]): number[] { + return snaps + .slice(0, 3) + .map(snap => { + if (typeof snap === 'number') { + if (snap <= 0 || snap > 1) return Math.min(1, Math.max(0.1, snap)); + return snap; + } + const match = String(snap).match(/^(\d+(?:\.\d+)?)\s*%$/); + if (match) return Math.min(1, Math.max(0.1, Number(match[1]) / 100)); + return 0.5; + }) + .sort((a, b) => a - b); +} const ActionSheet = React.memo( forwardRef(({ children }: { children: React.ReactElement }, ref) => { const { colors } = useTheme(); - const { height: windowHeight } = useWindowDimensions(); - const { right, left } = useSafeAreaInsets(); + const { height: windowHeight, fontScale } = useWindowDimensions(); + const { bottom, right, left } = useSafeAreaInsets(); const sheetRef = useRef(null); const [data, setData] = useState({} as TActionSheetOptions); const [isVisible, setVisible] = useState(false); + const [contentHeight, setContentHeight] = useState(0); + + const itemHeight = 48 * fontScale; + + const handleContentLayout = useCallback( + ({ nativeEvent: { layout } }: LayoutChangeEvent) => { + const height = Math.min(layout.height, windowHeight * ACTION_SHEET_MAX_HEIGHT_FRACTION); + setContentHeight(height); + }, + [windowHeight] + ); const hide = () => { sheetRef.current?.dismiss(); @@ -61,18 +91,48 @@ const ActionSheet = React.memo( const onDidDismiss = () => { setVisible(false); + setContentHeight(0); data?.onClose?.(); }; const bottomSheetStyle = isTablet ? styles.bottomSheet : { marginRight: right, marginLeft: left }; + const hasOptions = (data?.options?.length || 0) > 0; + const maxSnap = hasOptions + ? Math.min( + (itemHeight + 0.5) * (data?.options?.length || 0) + + HANDLE_HEIGHT + + (data?.headerHeight || 0) + + bottom + + (data?.hasCancel ? CANCEL_HEIGHT : 0), + windowHeight * ACTION_SHEET_MAX_HEIGHT_FRACTION + ) + : 0; + + let detents: SheetDetent[]; + if (data?.snaps?.length) { + detents = normalizeSnapsToDetents(data.snaps); + } else if (hasOptions) { + if (maxSnap > windowHeight * 0.6) { + detents = [0.5, ACTION_SHEET_MAX_HEIGHT_FRACTION]; + } else { + const fraction = Math.max(0.25, Math.min(maxSnap / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION)); + detents = [fraction]; + } + } else if (contentHeight > 0) { + const fraction = Math.min(contentHeight / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION); + detents = [Math.max(0.25, fraction)]; + } else { + detents = ['auto']; + } + return ( <> {children} {}} + onLayout={handleContentLayout} /> From b4a596fb51bce9036423eaa238c1e0d55cd6f59f Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Fri, 6 Feb 2026 14:37:58 -0300 Subject: [PATCH 3/8] fix: inconsistentaction sheet openning --- app/containers/ActionSheet/ActionSheet.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 1ddcfb6ed00..cbb3ae10a7d 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -60,7 +60,6 @@ const ActionSheet = React.memo( const show = (options: TActionSheetOptions) => { setData(options); setVisible(true); - sheetRef.current?.present(0); }; useBackHandler(() => { @@ -70,6 +69,12 @@ const ActionSheet = React.memo( return isVisible; }); + useEffect(() => { + if (isVisible) { + sheetRef.current?.present(0); + } + }, [isVisible]); + useEffect(() => { if (isVisible) { Keyboard.dismiss(); From 76cc8342b1a4c008cd1d25be36430912c5aa4b21 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Fri, 6 Feb 2026 14:46:36 -0300 Subject: [PATCH 4/8] feat: add min height --- app/containers/ActionSheet/ActionSheet.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index cbb3ae10a7d..8cbab926195 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -14,6 +14,7 @@ import BottomSheetContent from './BottomSheetContent'; import styles from './styles'; export const ACTION_SHEET_ANIMATION_DURATION = 250; +const ACTION_SHEET_MIN_HEIGHT_FRACTION = 0.35; const ACTION_SHEET_MAX_HEIGHT_FRACTION = 0.75; const HANDLE_HEIGHT = 28; const CANCEL_HEIGHT = 64; @@ -126,9 +127,13 @@ const ActionSheet = React.memo( } } else if (contentHeight > 0) { const fraction = Math.min(contentHeight / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION); - detents = [Math.max(0.25, fraction)]; + const contentDetent = Math.max(0.25, fraction); + detents = + contentDetent > ACTION_SHEET_MIN_HEIGHT_FRACTION + ? [ACTION_SHEET_MIN_HEIGHT_FRACTION, contentDetent] + : [contentDetent]; } else { - detents = ['auto']; + detents = [ACTION_SHEET_MIN_HEIGHT_FRACTION, 'auto']; } return ( From 6b25f9830bb60d73d42a0143c12b2bfe12194ee7 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Fri, 6 Feb 2026 21:06:46 +0000 Subject: [PATCH 5/8] chore: format code and fix lint issues --- app/containers/ActionSheet/ActionSheet.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 8cbab926195..78535e0d9d2 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -129,9 +129,7 @@ const ActionSheet = React.memo( const fraction = Math.min(contentHeight / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION); const contentDetent = Math.max(0.25, fraction); detents = - contentDetent > ACTION_SHEET_MIN_HEIGHT_FRACTION - ? [ACTION_SHEET_MIN_HEIGHT_FRACTION, contentDetent] - : [contentDetent]; + contentDetent > ACTION_SHEET_MIN_HEIGHT_FRACTION ? [ACTION_SHEET_MIN_HEIGHT_FRACTION, contentDetent] : [contentDetent]; } else { detents = [ACTION_SHEET_MIN_HEIGHT_FRACTION, 'auto']; } From 767bbe95f40b3599ed8dad803cff984948152590 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Fri, 6 Feb 2026 18:09:31 -0300 Subject: [PATCH 6/8] cleanup --- app/containers/ActionSheet/ActionSheet.tsx | 4 ++-- app/containers/TextInput/FormTextInput.tsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 78535e0d9d2..b884c21a72c 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -88,7 +88,7 @@ const ActionSheet = React.memo( hideActionSheet: hide })); - const renderHeader = () => ( + const renderHandle = () => ( <> {isValidElement(data?.customHeader) ? data.customHeader : null} @@ -145,7 +145,7 @@ const ActionSheet = React.memo( cornerRadius={16} dimmed grabber={false} - header={renderHeader()} + header={renderHandle()} scrollable={!!data?.options} style={[styles.container, bottomSheetStyle]} onDidDismiss={onDidDismiss}> diff --git a/app/containers/TextInput/FormTextInput.tsx b/app/containers/TextInput/FormTextInput.tsx index e0e11604e37..18a6af7ef13 100644 --- a/app/containers/TextInput/FormTextInput.tsx +++ b/app/containers/TextInput/FormTextInput.tsx @@ -123,7 +123,6 @@ export const FormTextInput = ({ const { colors } = useTheme(); const [showPassword, setShowPassword] = useState(false); const showClearInput = onClearInput && value && value.length > 0; - const Input = TextInput; const inputError = getInputError(error); const accessibilityLabelText = useMemo(() => { @@ -150,7 +149,7 @@ export const FormTextInput = ({ ) : null} - Date: Fri, 6 Feb 2026 18:31:07 -0300 Subject: [PATCH 7/8] chore: code improvements --- app/containers/ActionSheet/ActionSheet.tsx | 66 ++++-------------- .../ActionSheet/useActionSheetDetents.ts | 32 +++++++++ app/containers/ActionSheet/utils.ts | 68 +++++++++++++++++++ 3 files changed, 114 insertions(+), 52 deletions(-) create mode 100644 app/containers/ActionSheet/useActionSheetDetents.ts create mode 100644 app/containers/ActionSheet/utils.ts diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index b884c21a72c..ac8625afc0b 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -1,43 +1,26 @@ import { useBackHandler } from '@react-native-community/hooks'; import * as Haptics from 'expo-haptics'; import React, { forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; -import { Keyboard, type LayoutChangeEvent, useWindowDimensions } from 'react-native'; +import { Keyboard, type LayoutChangeEvent } from 'react-native'; import { TrueSheet } from '@lodev09/react-native-true-sheet'; -import type { SheetDetent } from '@lodev09/react-native-true-sheet'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { useTheme } from '../../theme'; import { isTablet } from '../../lib/methods/helpers'; import { Handle } from './Handle'; import { type TActionSheetOptions } from './Provider'; import BottomSheetContent from './BottomSheetContent'; import styles from './styles'; +import { ACTION_SHEET_MAX_HEIGHT_FRACTION } from './utils'; +import { useActionSheetDetents } from './useActionSheetDetents'; export const ACTION_SHEET_ANIMATION_DURATION = 250; -const ACTION_SHEET_MIN_HEIGHT_FRACTION = 0.35; -const ACTION_SHEET_MAX_HEIGHT_FRACTION = 0.75; -const HANDLE_HEIGHT = 28; -const CANCEL_HEIGHT = 64; - -function normalizeSnapsToDetents(snaps: (string | number)[]): number[] { - return snaps - .slice(0, 3) - .map(snap => { - if (typeof snap === 'number') { - if (snap <= 0 || snap > 1) return Math.min(1, Math.max(0.1, snap)); - return snap; - } - const match = String(snap).match(/^(\d+(?:\.\d+)?)\s*%$/); - if (match) return Math.min(1, Math.max(0.1, Number(match[1]) / 100)); - return 0.5; - }) - .sort((a, b) => a - b); -} const ActionSheet = React.memo( forwardRef(({ children }: { children: React.ReactElement }, ref) => { const { colors } = useTheme(); - const { height: windowHeight, fontScale } = useWindowDimensions(); + const { height: windowHeight, fontScale } = useResponsiveLayout(); const { bottom, right, left } = useSafeAreaInsets(); const sheetRef = useRef(null); const [data, setData] = useState({} as TActionSheetOptions); @@ -46,6 +29,14 @@ const ActionSheet = React.memo( const itemHeight = 48 * fontScale; + const detents = useActionSheetDetents({ + data, + windowHeight, + contentHeight, + itemHeight, + bottom + }); + const handleContentLayout = useCallback( ({ nativeEvent: { layout } }: LayoutChangeEvent) => { const height = Math.min(layout.height, windowHeight * ACTION_SHEET_MAX_HEIGHT_FRACTION); @@ -103,36 +94,7 @@ const ActionSheet = React.memo( const bottomSheetStyle = isTablet ? styles.bottomSheet : { marginRight: right, marginLeft: left }; - const hasOptions = (data?.options?.length || 0) > 0; - const maxSnap = hasOptions - ? Math.min( - (itemHeight + 0.5) * (data?.options?.length || 0) + - HANDLE_HEIGHT + - (data?.headerHeight || 0) + - bottom + - (data?.hasCancel ? CANCEL_HEIGHT : 0), - windowHeight * ACTION_SHEET_MAX_HEIGHT_FRACTION - ) - : 0; - - let detents: SheetDetent[]; - if (data?.snaps?.length) { - detents = normalizeSnapsToDetents(data.snaps); - } else if (hasOptions) { - if (maxSnap > windowHeight * 0.6) { - detents = [0.5, ACTION_SHEET_MAX_HEIGHT_FRACTION]; - } else { - const fraction = Math.max(0.25, Math.min(maxSnap / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION)); - detents = [fraction]; - } - } else if (contentHeight > 0) { - const fraction = Math.min(contentHeight / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION); - const contentDetent = Math.max(0.25, fraction); - detents = - contentDetent > ACTION_SHEET_MIN_HEIGHT_FRACTION ? [ACTION_SHEET_MIN_HEIGHT_FRACTION, contentDetent] : [contentDetent]; - } else { - detents = [ACTION_SHEET_MIN_HEIGHT_FRACTION, 'auto']; - } + return ( <> diff --git a/app/containers/ActionSheet/useActionSheetDetents.ts b/app/containers/ActionSheet/useActionSheetDetents.ts new file mode 100644 index 00000000000..f481f60a3b7 --- /dev/null +++ b/app/containers/ActionSheet/useActionSheetDetents.ts @@ -0,0 +1,32 @@ +import { useMemo } from 'react'; + +import type { TActionSheetOptions } from './Provider'; +import { getDetents } from './utils'; + +interface UseActionSheetDetentsParams { + data: TActionSheetOptions; + windowHeight: number; + contentHeight: number; + itemHeight: number; + bottom: number; +} + +export function useActionSheetDetents({ + data, + windowHeight, + contentHeight, + itemHeight, + bottom +}: UseActionSheetDetentsParams) { + return useMemo( + () => + getDetents({ + data, + windowHeight, + contentHeight, + itemHeight, + bottom + }), + [data, windowHeight, contentHeight, itemHeight, bottom] + ); +} diff --git a/app/containers/ActionSheet/utils.ts b/app/containers/ActionSheet/utils.ts new file mode 100644 index 00000000000..092e8513d84 --- /dev/null +++ b/app/containers/ActionSheet/utils.ts @@ -0,0 +1,68 @@ +import type { SheetDetent } from '@lodev09/react-native-true-sheet'; + +import type { TActionSheetOptions } from './Provider'; + +export const ACTION_SHEET_MIN_HEIGHT_FRACTION = 0.35; +export const ACTION_SHEET_MAX_HEIGHT_FRACTION = 0.75; +export const HANDLE_HEIGHT = 28; +export const CANCEL_HEIGHT = 64; + +interface IGetDetentsParams { + data: TActionSheetOptions; + windowHeight: number; + contentHeight: number; + itemHeight: number; + bottom: number; +} + +export const normalizeSnapsToDetents = (snaps: (string | number)[]): number[] => snaps + .slice(0, 3) + .map(snap => { + if (typeof snap === 'number') { + if (snap <= 0 || snap > 1) return Math.min(1, Math.max(0.1, snap)); + return snap; + } + const match = String(snap).match(/^(\d+(?:\.\d+)?)\s*%$/); + if (match) return Math.min(1, Math.max(0.1, Number(match[1]) / 100)); + return 0.5; + }) + .sort((a, b) => a - b) + +export const getDetents = ({ + data, + windowHeight, + contentHeight, + itemHeight, + bottom +}: IGetDetentsParams): SheetDetent[] => { + const hasOptions = (data?.options?.length || 0) > 0; + const maxSnap = hasOptions + ? Math.min( + (itemHeight + 0.5) * (data?.options?.length || 0) + + HANDLE_HEIGHT + + (data?.headerHeight || 0) + + bottom + + (data?.hasCancel ? CANCEL_HEIGHT : 0), + windowHeight * ACTION_SHEET_MAX_HEIGHT_FRACTION + ) + : 0; + + if (data?.snaps?.length) { + return normalizeSnapsToDetents(data.snaps); + } + if (hasOptions) { + if (maxSnap > windowHeight * 0.6) { + return [0.5, ACTION_SHEET_MAX_HEIGHT_FRACTION]; + } + const fraction = Math.max(0.25, Math.min(maxSnap / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION)); + return [fraction]; + } + if (contentHeight > 0) { + const fraction = Math.min(contentHeight / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION); + const contentDetent = Math.max(0.25, fraction); + return contentDetent > ACTION_SHEET_MIN_HEIGHT_FRACTION + ? [ACTION_SHEET_MIN_HEIGHT_FRACTION, contentDetent] + : [contentDetent]; + } + return [ACTION_SHEET_MIN_HEIGHT_FRACTION, 'auto']; +} From 1d898041fe3efdbd6e88890bdfea5f160b1d932f Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Fri, 6 Feb 2026 21:32:43 +0000 Subject: [PATCH 8/8] chore: format code and fix lint issues --- app/containers/ActionSheet/ActionSheet.tsx | 2 -- .../ActionSheet/useActionSheetDetents.ts | 8 +------- app/containers/ActionSheet/utils.ts | 19 ++++++------------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index ac8625afc0b..23f72749224 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -94,8 +94,6 @@ const ActionSheet = React.memo( const bottomSheetStyle = isTablet ? styles.bottomSheet : { marginRight: right, marginLeft: left }; - - return ( <> {children} diff --git a/app/containers/ActionSheet/useActionSheetDetents.ts b/app/containers/ActionSheet/useActionSheetDetents.ts index f481f60a3b7..9acac88262a 100644 --- a/app/containers/ActionSheet/useActionSheetDetents.ts +++ b/app/containers/ActionSheet/useActionSheetDetents.ts @@ -11,13 +11,7 @@ interface UseActionSheetDetentsParams { bottom: number; } -export function useActionSheetDetents({ - data, - windowHeight, - contentHeight, - itemHeight, - bottom -}: UseActionSheetDetentsParams) { +export function useActionSheetDetents({ data, windowHeight, contentHeight, itemHeight, bottom }: UseActionSheetDetentsParams) { return useMemo( () => getDetents({ diff --git a/app/containers/ActionSheet/utils.ts b/app/containers/ActionSheet/utils.ts index 092e8513d84..aa3fa96ffae 100644 --- a/app/containers/ActionSheet/utils.ts +++ b/app/containers/ActionSheet/utils.ts @@ -15,7 +15,8 @@ interface IGetDetentsParams { bottom: number; } -export const normalizeSnapsToDetents = (snaps: (string | number)[]): number[] => snaps +export const normalizeSnapsToDetents = (snaps: (string | number)[]): number[] => + snaps .slice(0, 3) .map(snap => { if (typeof snap === 'number') { @@ -26,15 +27,9 @@ export const normalizeSnapsToDetents = (snaps: (string | number)[]): number[] => if (match) return Math.min(1, Math.max(0.1, Number(match[1]) / 100)); return 0.5; }) - .sort((a, b) => a - b) + .sort((a, b) => a - b); -export const getDetents = ({ - data, - windowHeight, - contentHeight, - itemHeight, - bottom -}: IGetDetentsParams): SheetDetent[] => { +export const getDetents = ({ data, windowHeight, contentHeight, itemHeight, bottom }: IGetDetentsParams): SheetDetent[] => { const hasOptions = (data?.options?.length || 0) > 0; const maxSnap = hasOptions ? Math.min( @@ -60,9 +55,7 @@ export const getDetents = ({ if (contentHeight > 0) { const fraction = Math.min(contentHeight / windowHeight, ACTION_SHEET_MAX_HEIGHT_FRACTION); const contentDetent = Math.max(0.25, fraction); - return contentDetent > ACTION_SHEET_MIN_HEIGHT_FRACTION - ? [ACTION_SHEET_MIN_HEIGHT_FRACTION, contentDetent] - : [contentDetent]; + return contentDetent > ACTION_SHEET_MIN_HEIGHT_FRACTION ? [ACTION_SHEET_MIN_HEIGHT_FRACTION, contentDetent] : [contentDetent]; } return [ACTION_SHEET_MIN_HEIGHT_FRACTION, 'auto']; -} +};