feat: Dynamic action sheet height (#5202)
This commit is contained in:
parent
b8a3615f7e
commit
79bc539773
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,6 @@ export type TActionSheetOptionsItem = {
|
|||
|
||||
export type TActionSheetOptions = {
|
||||
options?: TActionSheetOptionsItem[];
|
||||
headerHeight?: number;
|
||||
customHeader?: React.ReactElement | null;
|
||||
hasCancel?: boolean;
|
||||
type?: string;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -87,8 +87,7 @@ export const MultiSelect = React.memo(
|
|||
selectedItems={selected}
|
||||
/>
|
||||
),
|
||||
onClose,
|
||||
headerHeight: 275
|
||||
onClose
|
||||
});
|
||||
};
|
||||
const onHide = () => {
|
||||
|
|
|
@ -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];
|
||||
};
|
|
@ -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')}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 />
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue