From a1172f8bf5032153733ea10d3864cebb12275d4a Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 3 Oct 2023 09:57:49 -0300 Subject: [PATCH] fix: dynamic action sheet taking whole screen (#5249) * Fix dynamic action sheet taking whole screen * Bring back maxSnap logic based on options * Rollback enableContentPanningGesture * Fix e2e tests --- app/containers/ActionSheet/ActionSheet.tsx | 49 +++++++++++++------ .../ActionSheet/BottomSheetContent.tsx | 8 +-- app/containers/ActionSheet/Handle.tsx | 2 +- app/containers/ActionSheet/Provider.tsx | 4 +- app/containers/ActionSheet/styles.ts | 3 -- app/containers/MessageActions/Header.tsx | 4 +- app/containers/MessageActions/index.tsx | 3 +- .../useVideoConf/StartACallActionSheet.tsx | 7 ++- e2e/tests/room/02-room.spec.ts | 2 +- e2e/tests/room/05-threads.spec.ts | 2 +- 10 files changed, 56 insertions(+), 28 deletions(-) diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index f2e766f01..816b977d1 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -4,15 +4,18 @@ import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useR import { Keyboard, useWindowDimensions } from 'react-native'; import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated'; import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../theme'; import { isIOS, isTablet } from '../../lib/methods/helpers'; import { Handle } from './Handle'; import { TActionSheetOptions } from './Provider'; import BottomSheetContent from './BottomSheetContent'; -import styles from './styles'; +import styles, { ITEM_HEIGHT } 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, @@ -23,11 +26,11 @@ const ANIMATION_CONFIG = { const ActionSheet = React.memo( forwardRef(({ children }: { children: React.ReactElement }, ref) => { const { colors } = useTheme(); + const { height: windowHeight } = useWindowDimensions(); + const { bottom } = useSafeAreaInsets(); const bottomSheetRef = useRef(null); const [data, setData] = useState({} as TActionSheetOptions); const [isVisible, setVisible] = useState(false); - const { width, height } = useWindowDimensions(); - const isLandscape = width > height; const animatedContentHeight = useSharedValue(0); const animatedHandleHeight = useSharedValue(0); const animatedDataSnaps = useSharedValue([]); @@ -49,11 +52,33 @@ const ActionSheet = React.memo( layout: { height } } }) => { - animatedContentHeight.value = height; + /** + * 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] + [animatedContentHeight, windowHeight] ); + const maxSnap = Math.min( + (ITEM_HEIGHT + 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 = () => { @@ -82,11 +107,6 @@ const ActionSheet = React.memo( } }, [isVisible]); - // Hides action sheet when orientation changes - useEffect(() => { - setVisible(false); - }, [isLandscape]); - useImperativeHandle(ref, () => ({ showActionSheet: show, hideActionSheet: hide @@ -118,11 +138,11 @@ const ActionSheet = React.memo( [] ); - const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {}; + const bottomSheet = isTablet ? styles.bottomSheet : {}; // Must need this prop to avoid keyboard dismiss // when is android tablet and the input text is focused - const androidTablet: any = isTablet && isLandscape && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {}; + const androidTablet: any = isTablet && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {}; return ( <> @@ -130,10 +150,11 @@ const ActionSheet = React.memo( {isVisible && ( item.title} - bounces={true} + bounces={false} renderItem={renderItem} - style={{ backgroundColor: colors.focusedBackground, paddingBottom: bottom }} + style={{ backgroundColor: colors.focusedBackground }} keyboardDismissMode='interactive' indicatorStyle='black' - contentContainerStyle={styles.content} + contentContainerStyle={{ paddingBottom: bottom }} ItemSeparatorComponent={List.Separator} ListHeaderComponent={List.Separator} ListFooterComponent={renderFooter} @@ -57,7 +57,7 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL ); } return ( - + {children} ); diff --git a/app/containers/ActionSheet/Handle.tsx b/app/containers/ActionSheet/Handle.tsx index 0b3a7caf1..68f559c24 100644 --- a/app/containers/ActionSheet/Handle.tsx +++ b/app/containers/ActionSheet/Handle.tsx @@ -8,7 +8,7 @@ import { useTheme } from '../../theme'; export const Handle = React.memo(() => { const { theme } = useTheme(); return ( - + ); diff --git a/app/containers/ActionSheet/Provider.tsx b/app/containers/ActionSheet/Provider.tsx index 0293c7f21..841064c2d 100644 --- a/app/containers/ActionSheet/Provider.tsx +++ b/app/containers/ActionSheet/Provider.tsx @@ -15,10 +15,12 @@ export type TActionSheetOptionsItem = { export type TActionSheetOptions = { options?: TActionSheetOptionsItem[]; + headerHeight?: number; customHeader?: React.ReactElement | null; hasCancel?: boolean; - type?: string; + // children can both use snaps or dynamic children?: React.ReactElement | null; + /** Required if your action sheet needs vertical scroll */ snaps?: (string | number)[]; onClose?: () => void; enableContentPanningGesture?: boolean; diff --git a/app/containers/ActionSheet/styles.ts b/app/containers/ActionSheet/styles.ts index 3530fefcd..00d34935a 100644 --- a/app/containers/ActionSheet/styles.ts +++ b/app/containers/ActionSheet/styles.ts @@ -19,9 +19,6 @@ export default StyleSheet.create({ separator: { marginHorizontal: 16 }, - content: { - paddingTop: 16 - }, titleContainer: { flex: 1 }, diff --git a/app/containers/MessageActions/Header.tsx b/app/containers/MessageActions/Header.tsx index 52b3a3407..e7bfab1c0 100644 --- a/app/containers/MessageActions/Header.tsx +++ b/app/containers/MessageActions/Header.tsx @@ -31,6 +31,7 @@ interface THeaderFooter { theme: TSupportedThemes; } +export const HEADER_HEIGHT = 54; const ITEM_SIZE = 36; const CONTAINER_MARGIN = 8; const ITEM_MARGIN = 8; @@ -38,7 +39,8 @@ const ITEM_MARGIN = 8; const styles = StyleSheet.create({ container: { alignItems: 'center', - marginHorizontal: CONTAINER_MARGIN + marginHorizontal: CONTAINER_MARGIN, + paddingBottom: 16 }, headerItem: { height: ITEM_SIZE, diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index 881246d2f..9ee4be937 100644 --- a/app/containers/MessageActions/index.tsx +++ b/app/containers/MessageActions/index.tsx @@ -13,7 +13,7 @@ import { LISTENER } from '../Toast'; import EventEmitter from '../../lib/methods/helpers/events'; import { showConfirmationAlert } from '../../lib/methods/helpers/info'; import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet'; -import Header, { IHeader } from './Header'; +import Header, { HEADER_HEIGHT, IHeader } from './Header'; import events from '../../lib/methods/helpers/log/events'; import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { getPermalinkMessage } from '../../lib/methods'; @@ -511,6 +511,7 @@ const MessageActions = React.memo( await getPermissions(); showActionSheet({ options: getOptions(message), + headerHeight: HEADER_HEIGHT, customHeader: !isReadOnly || room.reactWhenReadOnly ? (
diff --git a/app/lib/hooks/useVideoConf/StartACallActionSheet.tsx b/app/lib/hooks/useVideoConf/StartACallActionSheet.tsx index 7326d03b8..0554608ec 100644 --- a/app/lib/hooks/useVideoConf/StartACallActionSheet.tsx +++ b/app/lib/hooks/useVideoConf/StartACallActionSheet.tsx @@ -2,6 +2,7 @@ import { Camera, CameraType } from 'expo-camera'; import React, { useState } from 'react'; import { StyleSheet, View } from 'react-native'; import { useDispatch } from 'react-redux'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useAppSelector } from '..'; import { cancelCall, initVideoCall } from '../../../actions/videoConf'; @@ -19,6 +20,7 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R const [mic, setMic] = useState(true); const [cam, setCam] = useState(false); const [containerWidth, setContainerWidth] = useState(0); + const { bottom } = useSafeAreaInsets(); const username = useAppSelector(state => getUserSelector(state).username); const calling = useAppSelector(state => state.videoConf.calling); @@ -36,7 +38,10 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R ); return ( - setContainerWidth(e.nativeEvent.layout.width / 2)}> + setContainerWidth(e.nativeEvent.layout.width / 2)} + > {calling ? : null} { .toExist() .withTimeout(2000); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); - await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); + await element(by.id('action-sheet')).swipe('up', 'fast', 0.5); await sleep(300); // wait for animation await waitFor(element(by[textMatcher]('Delete'))) .toExist() diff --git a/e2e/tests/room/05-threads.spec.ts b/e2e/tests/room/05-threads.spec.ts index 6ec2a7a86..9f46eceab 100644 --- a/e2e/tests/room/05-threads.spec.ts +++ b/e2e/tests/room/05-threads.spec.ts @@ -237,7 +237,7 @@ describe('Threads', () => { .withTimeout(5000); await element(by.id(`message-thread-button-${thread}`)).tap(); await tryTapping(element(by[textMatcher]('replied')).atIndex(0), 2000, true); - await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); + await element(by.id('action-sheet')).swipe('up', 'fast', 0.5); await sleep(300); // wait for animation await element(by[textMatcher]('Delete')).atIndex(0).tap(); await element(by[textMatcher]('Delete').and(by.type(alertButtonType))).tap();