diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 65f65d551dc..23f72749224 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -1,98 +1,57 @@ 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, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { Keyboard, type LayoutChangeEvent } from 'react-native'; +import { TrueSheet } 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 { 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'; +import { ACTION_SHEET_MAX_HEIGHT_FRACTION } from './utils'; +import { useActionSheetDetents } from './useActionSheetDetents'; 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 { height: windowHeight, fontScale } = useResponsiveLayout(); const { bottom, right, left } = useSafeAreaInsets(); - const { fontScale } = useWindowDimensions(); - const itemHeight = 48 * fontScale; - const bottomSheetRef = useRef(null); + 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 [contentHeight, setContentHeight] = useState(0); + + const itemHeight = 48 * fontScale; + + const detents = useActionSheetDetents({ + data, + windowHeight, + contentHeight, + itemHeight, + bottom + }); 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); + ({ nativeEvent: { layout } }: LayoutChangeEvent) => { + const height = Math.min(layout.height, windowHeight * ACTION_SHEET_MAX_HEIGHT_FRACTION); + setContentHeight(height); }, - [animatedContentHeight, windowHeight] + [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); }; useBackHandler(() => { @@ -102,6 +61,12 @@ const ActionSheet = React.memo( return isVisible; }); + useEffect(() => { + if (isVisible) { + sheetRef.current?.present(0); + } + }, [isVisible]); + useEffect(() => { if (isVisible) { Keyboard.dismiss(); @@ -121,62 +86,37 @@ const ActionSheet = React.memo( ); - const onClose = () => { - toggleVisible(); - data?.onClose && data?.onClose(); - animatedDataSnaps.value = []; + const onDidDismiss = () => { + setVisible(false); + setContentHeight(0); + 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/ActionSheet/useActionSheetDetents.ts b/app/containers/ActionSheet/useActionSheetDetents.ts new file mode 100644 index 00000000000..9acac88262a --- /dev/null +++ b/app/containers/ActionSheet/useActionSheetDetents.ts @@ -0,0 +1,26 @@ +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..aa3fa96ffae --- /dev/null +++ b/app/containers/ActionSheet/utils.ts @@ -0,0 +1,61 @@ +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']; +}; diff --git a/app/containers/TextInput/FormTextInput.tsx b/app/containers/TextInput/FormTextInput.tsx index 046cdab1e35..18a6af7ef13 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,6 @@ export const FormTextInput = ({ const { colors } = useTheme(); const [showPassword, setShowPassword] = useState(false); const showClearInput = onClearInput && value && value.length > 0; - const Input = bottomSheet ? BottomSheetTextInput : TextInput; const inputError = getInputError(error); const accessibilityLabelText = useMemo(() => { @@ -151,7 +149,7 @@ export const FormTextInput = ({ ) : null} - ({ 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"