feat: Dynamic action sheet height (#5202)

This commit is contained in:
Diego Mello 2023-09-08 10:47:42 -03:00 committed by GitHub
parent b8a3615f7e
commit 79bc539773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 61 additions and 92 deletions

View File

@ -1,22 +1,16 @@
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 } from 'react-native';
import { Easing } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Keyboard, useWindowDimensions } from 'react-native';
import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useDimensions, useOrientation } from '../../dimensions';
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, { ITEM_HEIGHT } from './styles';
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MIN_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
import styles from './styles';
export const ACTION_SHEET_ANIMATION_DURATION = 250;
@ -32,33 +26,34 @@ const ActionSheet = React.memo(
const bottomSheetRef = useRef<BottomSheet>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false);
const { height } = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
const animatedContentHeight = useSharedValue(0);
const animatedHandleHeight = useSharedValue(0);
const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]);
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 maxSnap = Math.min(
// Items height
ITEM_HEIGHT * (data?.options?.length || 0) +
// Handle height
HANDLE_HEIGHT +
// Custom header height
(data?.headerHeight || 0) +
// Insets bottom height (Notch devices)
insets.bottom +
// Cancel button height
(data?.hasCancel ? CANCEL_HEIGHT : 0),
height - MIN_SNAP_HEIGHT
const handleContentLayout = useCallback(
({
nativeEvent: {
layout: { height }
}
}) => {
animatedContentHeight.value = height;
},
[animatedContentHeight]
);
/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps = maxSnap > height * 0.6 && !isLandscape && !data.snaps ? [height * 0.5, maxSnap] : [maxSnap];
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
@ -67,6 +62,9 @@ const ActionSheet = React.memo(
const show = (options: TActionSheetOptions) => {
setData(options);
if (options.snaps?.length) {
animatedDataSnaps.value = options.snaps;
}
toggleVisible();
};
@ -104,6 +102,7 @@ const ActionSheet = React.memo(
const onClose = () => {
toggleVisible();
data?.onClose && data?.onClose();
animatedDataSnaps.value = [];
};
const renderBackdrop = useCallback(
@ -131,7 +130,10 @@ const ActionSheet = React.memo(
{isVisible && (
<BottomSheet
ref={bottomSheetRef}
snapPoints={data?.snaps ? data.snaps : snaps}
snapPoints={animatedSnapPoints}
handleHeight={animatedHandleHeight}
// We need undefined to enable vertical swipe gesture inside the bottom sheet like in reaction picker
contentHeight={data.snaps?.length ? undefined : animatedContentHeight}
animationConfigs={ANIMATION_CONFIG}
animateOnMount={true}
backdropComponent={renderBackdrop}
@ -144,7 +146,13 @@ const ActionSheet = React.memo(
enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet}
>
<BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} />
<BottomSheetContent
options={data?.options}
hide={hide}
children={data?.children}
hasCancel={data?.hasCancel}
onLayout={handleContentLayout}
/>
</BottomSheet>
)}
</>

View File

@ -1,6 +1,7 @@
import { Text } from 'react-native';
import { Text, ViewProps } from 'react-native';
import React from 'react';
import { BottomSheetView, BottomSheetFlatList } from '@gorhom/bottom-sheet';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import I18n from '../../i18n';
import { useTheme } from '../../theme';
@ -15,10 +16,12 @@ interface IBottomSheetContentProps {
options?: TActionSheetOptionsItem[];
hide: () => void;
children?: React.ReactElement | null;
onLayout: ViewProps['onLayout'];
}
const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: IBottomSheetContentProps) => {
const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onLayout }: IBottomSheetContentProps) => {
const { colors } = useTheme();
const { bottom } = useSafeAreaInsets();
const renderFooter = () =>
hasCancel ? (
@ -42,18 +45,19 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: I
keyExtractor={item => item.title}
bounces={true}
renderItem={renderItem}
style={{ backgroundColor: colors.focusedBackground }}
style={{ backgroundColor: colors.focusedBackground, paddingBottom: bottom }}
keyboardDismissMode='interactive'
indicatorStyle='black'
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
onLayout={onLayout}
/>
);
}
return (
<BottomSheetView testID='action-sheet' style={styles.contentContainer}>
<BottomSheetView testID='action-sheet' style={[styles.contentContainer, { paddingBottom: bottom }]} onLayout={onLayout}>
{children}
</BottomSheetView>
);

View File

@ -15,7 +15,6 @@ export type TActionSheetOptionsItem = {
export type TActionSheetOptions = {
options?: TActionSheetOptionsItem[];
headerHeight?: number;
customHeader?: React.ReactElement | null;
hasCancel?: boolean;
type?: string;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { FlatList, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants';
@ -8,7 +8,6 @@ import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed } from '../../lib/methods';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles';
import { IEmoji, TAnyMessageModel } from '../../definitions';
import Touch from '../Touch';
@ -32,7 +31,6 @@ interface THeaderFooter {
theme: TSupportedThemes;
}
export const HEADER_HEIGHT = 36;
const ITEM_SIZE = 36;
const CONTAINER_MARGIN = 8;
const ITEM_MARGIN = 8;
@ -86,7 +84,7 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
);
const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => {
const { width, height } = useDimensions();
const { width, height } = useWindowDimensions();
const { theme } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
const isLandscape = width > height;

View File

@ -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, { HEADER_HEIGHT, IHeader } from './Header';
import Header, { 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,7 +511,6 @@ const MessageActions = React.memo(
await getPermissions();
showActionSheet({
options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader:
!isReadOnly || room.reactWhenReadOnly ? (
<Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />

View File

@ -87,8 +87,7 @@ export const MultiSelect = React.memo(
selectedItems={selected}
/>
),
onClose,
headerHeight: 275
onClose
});
};
const onHide = () => {

View File

@ -1,22 +0,0 @@
import { StatusBar } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { isIOS, isTablet } from '../methods/helpers';
// Not sure if it's worth adding this here in the context of the actionSheet
/**
* Return the snaps based on the size you pass (aka: Size of action sheet)
* @param {number} componentSize size of the component that will be rendered in the action sheet
*/
export const useSnaps = (componentSize: number): number[] | string[] => {
const insets = useSafeAreaInsets();
if (isIOS) {
const fixTabletInset = isTablet ? 2 : 1;
return [componentSize + (insets.bottom || insets.top) * fixTabletInset];
}
let statusHeight = 0;
if (StatusBar.currentHeight) {
statusHeight = StatusBar.currentHeight;
}
return [componentSize + statusHeight];
};

View File

@ -1,7 +1,6 @@
import { Camera, CameraType } from 'expo-camera';
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
import { useAppSelector } from '..';
@ -13,7 +12,6 @@ import Ringer, { ERingerSounds } from '../../../containers/Ringer';
import i18n from '../../../i18n';
import { getUserSelector } from '../../../selectors/login';
import { useTheme } from '../../../theme';
import { isIOS } from '../../methods/helpers';
import useUserData from '../useUserData';
export default function StartACallActionSheet({ rid }: { rid: string }): React.ReactElement {
@ -28,10 +26,6 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R
const user = useUserData(rid);
// fix safe area bottom padding on iOS
const insets = useSafeAreaInsets();
const paddingBottom = isIOS && insets.bottom ? 8 : 0;
React.useEffect(
() => () => {
if (calling) {
@ -42,10 +36,7 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R
);
return (
<View
style={[style.actionSheetContainer, { paddingBottom }]}
onLayout={e => setContainerWidth(e.nativeEvent.layout.width / 2)}
>
<View style={style.actionSheetContainer} onLayout={e => setContainerWidth(e.nativeEvent.layout.width / 2)}>
{calling ? <Ringer ringer={ERingerSounds.DIALTONE} /> : null}
<CallHeader
title={calling && user.direct ? i18n.t('Calling') : i18n.t('Start_a_call')}

View File

@ -8,7 +8,6 @@ import { compareServerVersion, showErrorAlert } from '../../methods/helpers';
import { handleAndroidBltPermission } from '../../methods/videoConf';
import { Services } from '../../services';
import { useAppSelector } from '../useAppSelector';
import { useSnaps } from '../useSnaps';
import StartACallActionSheet from './StartACallActionSheet';
import { useVideoConfCall } from './useVideoConfCall';
@ -34,7 +33,6 @@ export const useVideoConf = (
const [permission, requestPermission] = Camera.useCameraPermissions();
const { showActionSheet } = useActionSheet();
const snaps = useSnaps(404);
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
@ -68,7 +66,7 @@ export const useVideoConf = (
if (canInit) {
showActionSheet({
children: <StartACallActionSheet rid={rid} />,
snaps
snaps: [480]
});
if (!permission?.granted) {
requestPermission();

View File

@ -1,7 +1,6 @@
import { sha256 } from 'js-sha256';
import React from 'react';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
import { deleteAccount } from '../../../../actions/login';
@ -18,7 +17,6 @@ import sharedStyles from '../../../Styles';
export function DeleteAccountActionSheetContent(): React.ReactElement {
const { hideActionSheet, showActionSheet } = useActionSheet();
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const { colors } = useTheme();
const handleDeleteAccount = async (password: string) => {
@ -40,8 +38,7 @@ export function DeleteAccountActionSheetContent(): React.ReactElement {
removedRooms={removedRooms}
password={sha256(password)}
/>
),
headerHeight: 225 + insets.bottom
)
});
}, 250); // timeout for hide effect
} else if (error.data.errorType === 'error-invalid-password') {

View File

@ -237,8 +237,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
}}
onCancel={this.props.hideActionSheet}
/>
),
headerHeight: 225
)
});
return;
}
@ -417,8 +416,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
deleteOwnAccount = () => {
logEvent(events.DELETE_OWN_ACCOUNT);
this.props.showActionSheet({
children: <DeleteAccountActionSheetContent />,
headerHeight: 225
children: <DeleteAccountActionSheetContent />
});
};

View File

@ -854,7 +854,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
children: (
<ReactionPicker message={selectedMessage} onEmojiSelected={this.onReactionPress} reactionClose={this.onReactionClose} />
),
snaps: [400],
snaps: ['50%'],
enableContentPanningGesture: false
});
}, 100);