From b4c95746e12d68ff7783f5f4d3f92a8b658e8527 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:59:48 -0500 Subject: [PATCH] chore: update bottom sheet architecture (#927) * chore: create new bottom sheet wrapper * chore: reusable hook to disable android back button * chore: prevent back button hook in bottom sheet wrapper * chore: added bottom sheet description * chore: updated sync settings to use new modal * chore: update border radius * chore: unsubscribe from listener --- messages/en.json | 45 +- src/frontend/Navigation/Stack/AppScreens.tsx | 579 +++++++++--------- src/frontend/hooks/server/mediaSync.ts | 2 + .../hooks/usePreventAndroidBackButton.ts | 18 + .../ProjectSettings/MediaSyncSettings.tsx | 173 ------ .../SyncEverythingBottomSheet.tsx | 75 +++ .../SyncPreviewsBottomSheet.tsx | 82 +++ .../MediaSyncSettings/index.tsx | 107 ++++ .../sharedComponents/BottomSheetWrapper.tsx | 59 ++ src/frontend/sharedTypes/navigation.ts | 2 + 10 files changed, 674 insertions(+), 468 deletions(-) create mode 100644 src/frontend/hooks/usePreventAndroidBackButton.ts delete mode 100644 src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings.tsx create mode 100644 src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncEverythingBottomSheet.tsx create mode 100644 src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncPreviewsBottomSheet.tsx create mode 100644 src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/index.tsx create mode 100644 src/frontend/sharedComponents/BottomSheetWrapper.tsx diff --git a/messages/en.json b/messages/en.json index daab4fb84..87ab76403 100644 --- a/messages/en.json +++ b/messages/en.json @@ -871,36 +871,18 @@ "screens.MediaSyncSettings.syncEverything": { "message": "Sync Everything" }, - "screens.MediaSyncSettings.syncEverythingButtonBottomSheet": { - "message": "Sync Everything?" - }, "screens.MediaSyncSettings.syncEverythingDescription": { "message": "Your device will sync all content at full size, including photos, audio, and videos." }, - "screens.MediaSyncSettings.syncEverythingDescriptionBottomSheet": { - "message": "You are about to sync everything. This may increase the disk space used on your device." - }, "screens.MediaSyncSettings.syncEverythingWarning": { "message": "Note: This will use more storage." }, - "screens.MediaSyncSettings.syncPreviewWarningBottomSheet": { - "message": "You will no longer sync Audio or Video." - }, "screens.MediaSyncSettings.syncPreviews": { "message": "Sync Previews (Photos Only)" }, - "screens.MediaSyncSettings.syncPreviewsBottomSheetConfirm": { - "message": "Sync Previews" - }, - "screens.MediaSyncSettings.syncPreviewsButtonBottomSheet": { - "message": "Sync Previews?" - }, "screens.MediaSyncSettings.syncPreviewsDescription": { "message": "Photos will sync at a reduced smaller size. Device will not sync audio or video." }, - "screens.MediaSyncSettings.syncPreviewsDescriptionBottomSheet": { - "message": "Your device will keep all existing data but new observations will sync in a smaller, preview size." - }, "screens.MediaSyncSettings.title": { "message": "Sync Settings" }, @@ -1565,6 +1547,33 @@ "screens.Sync.ProjectSyncDisplay.waitingForDevices": { "message": "Waiting for devices" }, + "screens.SyncEverythingBottomSheet.cancel": { + "message": "Cancel" + }, + "screens.SyncEverythingBottomSheet.confirm": { + "message": "Sync Everything" + }, + "screens.SyncEverythingBottomSheet.syncEverything": { + "message": "Sync Everything?" + }, + "screens.SyncEverythingBottomSheet.syncEverythingDescription": { + "message": "You are about to sync everything. This may increase the disk space used on your device." + }, + "screens.SyncPreviewBottomSheet.cancel": { + "message": "Cancel" + }, + "screens.SyncPreviewBottomSheet.confirm": { + "message": "Sync Previews" + }, + "screens.SyncPreviewBottomSheet.syncPreviewWarningBottomSheet": { + "message": "You will no longer sync Audio or Video." + }, + "screens.SyncPreviewBottomSheet.syncPreviewsButtonBottomSheet": { + "message": "Sync Previews?" + }, + "screens.SyncPreviewBottomSheet.syncPreviewsDescriptionBottomSheet": { + "message": "Your device will keep all existing data but new observations will sync in a smaller, preview size." + }, "screens.Track.ObservationList.observation": { "message": "Observation" }, diff --git a/src/frontend/Navigation/Stack/AppScreens.tsx b/src/frontend/Navigation/Stack/AppScreens.tsx index 2c386a9ff..7dfa3bc6d 100644 --- a/src/frontend/Navigation/Stack/AppScreens.tsx +++ b/src/frontend/Navigation/Stack/AppScreens.tsx @@ -69,7 +69,7 @@ import { TrackScreen, createNavigationOptions as createTrackNavigationOptions, } from '../../screens/Track/index.tsx'; -import {MediaSyncSettings} from '../../screens/Settings/ProjectSettings/MediaSyncSettings.tsx'; +import {MediaSyncSettings} from '../../screens/Settings/ProjectSettings/MediaSyncSettings/index.tsx'; import {DataAndPrivacy} from '../../screens/Settings/DataAndPrivacy/DataAndPrivacy'; import {SettingsPrivacyPolicy} from '../../screens/Settings/DataAndPrivacy/SettingsPrivacyPolicy'; import {TrackEdit} from '../../screens/TrackEdit/index.tsx'; @@ -92,6 +92,8 @@ import { createNavigationOptions as createBackgroundMapsNavigationOptions, BackgroundMapsScreen, } from '../../screens/Settings/MapManagement/BackgroundMaps.tsx'; +import {SyncPreviewsBottomSheet} from '../../screens/Settings/ProjectSettings/MediaSyncSettings/SyncPreviewsBottomSheet.tsx'; +import {SyncEverythingBottomSheet} from '../../screens/Settings/ProjectSettings/MediaSyncSettings/SyncEverythingBottomSheet.tsx'; export const TAB_BAR_HEIGHT = 70; @@ -102,284 +104,307 @@ export const createDefaultScreenGroup = ({ }: { intl: (title: MessageDescriptor) => string; }) => ( - - ( - - {/* This provider allows the bottoms sheet used by tracks to open up behind the drawers */} - - - - - )} - /> - - - - ( - - ), - }} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + <> + + ( + + {/* This provider allows the bottoms sheet used by tracks to open up behind the drawers */} + + + + + )} + /> + + + + ( + + ), + }} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - - - - - - - + + + + + + + + - {process.env.EXPO_PUBLIC_FEATURE_TEST_DATA_UI && ( - - )} - - {}} isLoading={false} />, - }} - /> - - - + {process.env.EXPO_PUBLIC_FEATURE_TEST_DATA_UI && ( + + )} + + ( + {}} isLoading={false} /> + ), + }} + /> + + + + + + + + ); diff --git a/src/frontend/hooks/server/mediaSync.ts b/src/frontend/hooks/server/mediaSync.ts index 1c7ec2531..d493ffb91 100644 --- a/src/frontend/hooks/server/mediaSync.ts +++ b/src/frontend/hooks/server/mediaSync.ts @@ -7,6 +7,7 @@ import { import {MediaSyncSetting} from '../../sharedTypes'; export const MEDIA_SYNC_SETTING_KEY = 'media_sync_setting'; +export const UPDATE_MEDIA_SETTING = 'update_media_setting'; export function convertMediaSyncSetting(isArchive: boolean): MediaSyncSetting { return isArchive ? 'everything' : 'previews'; @@ -33,6 +34,7 @@ export function useSetMediaSyncSetting() { const queryClient = useQueryClient(); return useMutation({ + mutationKey: [UPDATE_MEDIA_SETTING], mutationFn: async (newSetting: MediaSyncSetting) => { const isArchive = isArchiveDevice(newSetting); return api.setIsArchiveDevice(isArchive); diff --git a/src/frontend/hooks/usePreventAndroidBackButton.ts b/src/frontend/hooks/usePreventAndroidBackButton.ts new file mode 100644 index 000000000..c5ab335fd --- /dev/null +++ b/src/frontend/hooks/usePreventAndroidBackButton.ts @@ -0,0 +1,18 @@ +import {useFocusEffect} from '@react-navigation/native'; +import {useCallback} from 'react'; +import {BackHandler} from 'react-native'; + +export const usePreventAndroidBackButton = () => { + return useFocusEffect( + useCallback(() => { + const onBackPress = () => true; + + const subscription = BackHandler.addEventListener( + 'hardwareBackPress', + onBackPress, + ); + + return () => subscription.remove(); + }, []), + ); +}; diff --git a/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings.tsx b/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings.tsx deleted file mode 100644 index a79136512..000000000 --- a/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import * as React from 'react'; -import {ScrollView, StyleSheet} from 'react-native'; -import {useIntl, defineMessages} from 'react-intl'; -import {SelectOne} from '../../../sharedComponents/SelectOne'; -import {SYNC_BACKGROUND} from '../../../lib/styles'; -import {MediaSyncActionSheetContent} from './MediaSyncActionSheetContent'; -import { - useBottomSheetModal, - BottomSheetModal, -} from '../../../sharedComponents/BottomSheetModal'; -import {MediaSyncSetting} from '../../../sharedTypes'; -import { - useGetMediaSyncSetting, - useSetMediaSyncSetting, -} from '../../../hooks/server/mediaSync'; - -const m = defineMessages({ - syncSettingsTitle: { - id: 'screens.MediaSyncSettings.title', - defaultMessage: 'Sync Settings', - }, - syncPreviews: { - id: 'screens.MediaSyncSettings.syncPreviews', - defaultMessage: 'Sync Previews (Photos Only)', - }, - syncPreviewsDescription: { - id: 'screens.MediaSyncSettings.syncPreviewsDescription', - defaultMessage: - 'Photos will sync at a reduced smaller size. Device will not sync audio or video.', - }, - syncEverything: { - id: 'screens.MediaSyncSettings.syncEverything', - defaultMessage: 'Sync Everything', - }, - syncEverythingDescription: { - id: 'screens.MediaSyncSettings.syncEverythingDescription', - defaultMessage: - 'Your device will sync all content at full size, including photos, audio, and videos.', - }, - syncEverythingWarning: { - id: 'screens.MediaSyncSettings.syncEverythingWarning', - defaultMessage: 'Note: This will use more storage.', - }, - syncPreviewsBottomSheet: { - id: 'screens.MediaSyncSettings.syncPreviewsButtonBottomSheet', - defaultMessage: 'Sync Previews?', - }, - syncEverythingBottomSheet: { - id: 'screens.MediaSyncSettings.syncEverythingButtonBottomSheet', - defaultMessage: 'Sync Everything?', - }, - syncPreviewsDescriptionBottomSheet: { - id: 'screens.MediaSyncSettings.syncPreviewsDescriptionBottomSheet', - defaultMessage: - 'Your device will keep all existing data but new observations will sync in a smaller, preview size.', - }, - syncPreviewWarningBottomSheet: { - id: 'screens.MediaSyncSettings.syncPreviewWarningBottomSheet', - defaultMessage: 'You will no longer sync Audio or Video.', - }, - syncEverythingDescriptionBottomSheet: { - id: 'screens.MediaSyncSettings.syncEverythingDescriptionBottomSheet', - defaultMessage: - 'You are about to sync everything. This may increase the disk space used on your device.', - }, - syncPreviewsBottomSheetConfirm: { - id: 'screens.MediaSyncSettings.syncPreviewsBottomSheetConfirm', - defaultMessage: 'Sync Previews', - }, -}); - -export const MediaSyncSettings = () => { - const {formatMessage: t} = useIntl(); - const {data: mediaSyncSetting} = useGetMediaSyncSetting(); - const { - mutate: setMediaSyncSetting, - variables, - isPending, - } = useSetMediaSyncSetting(); - const [possibleSetting, setPossibleSetting] = - React.useState(null); - - const {isOpen, openSheet, closeSheet, sheetRef} = useBottomSheetModal({ - openOnMount: false, - }); - - const handleOptionChange = (value: MediaSyncSetting) => { - setPossibleSetting(value); - openSheet(); - }; - - const handleConfirm = () => { - if (possibleSetting) { - setMediaSyncSetting(possibleSetting); - } - closeSheet(); - }; - - const handleDismiss = () => { - setPossibleSetting(null); - closeSheet(); - }; - - const options: { - value: MediaSyncSetting; - label: string; - hint: React.ReactNode; - }[] = [ - { - value: 'previews', - label: t(m.syncPreviews), - hint: t(m.syncPreviewsDescription), - }, - { - value: 'everything', - label: t(m.syncEverything), - hint: ( - <> - {t(m.syncEverythingDescription)} - {'\n\n'} - {t(m.syncEverythingWarning)} - - ), - }, - ]; - - return ( - - - - - {t(m.syncPreviewsDescriptionBottomSheet)} - {'\n\n'} - {t(m.syncPreviewWarningBottomSheet)} - - ) : ( - t(m.syncEverythingDescriptionBottomSheet) - ) - } - confirmActionText={ - possibleSetting === 'previews' - ? t(m.syncPreviewsBottomSheetConfirm) - : t(m.syncEverything) - } - confirmAction={handleConfirm} - onDismiss={handleDismiss} - /> - - - ); -}; - -MediaSyncSettings.navTitle = m.syncSettingsTitle; - -const styles = StyleSheet.create({ - container: { - padding: 20, - }, -}); diff --git a/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncEverythingBottomSheet.tsx b/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncEverythingBottomSheet.tsx new file mode 100644 index 000000000..af99ff64f --- /dev/null +++ b/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncEverythingBottomSheet.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import {View, StyleSheet} from 'react-native'; +import {BottomSheetWrapper} from '../../../../sharedComponents/BottomSheetWrapper'; +import {defineMessages, useIntl} from 'react-intl'; +import Warning from '../../../../images/Warning.svg'; +import {Button} from '../../../../sharedComponents/Button'; +import {HeaderText} from '../../../../sharedComponents/Text/HeaderText'; +import {useNavigationFromRoot} from '../../../../hooks/useNavigationWithTypes'; +import {BodyText} from '../../../../sharedComponents/Text/BodyText'; +import {useSetMediaSyncSetting} from '../../../../hooks/server/mediaSync'; + +const m = defineMessages({ + cancel: { + id: 'screens.SyncEverythingBottomSheet.cancel', + defaultMessage: 'Cancel', + }, + confirm: { + id: 'screens.SyncEverythingBottomSheet.confirm', + defaultMessage: 'Sync Everything', + }, + syncEverything: { + id: 'screens.SyncEverythingBottomSheet.syncEverything', + defaultMessage: 'Sync Everything?', + }, + syncEverythingDescription: { + id: 'screens.SyncEverythingBottomSheet.syncEverythingDescription', + defaultMessage: + 'You are about to sync everything. This may increase the disk space used on your device.', + }, +}); + +export const SyncEverythingBottomSheet = () => { + const {formatMessage} = useIntl(); + const {goBack} = useNavigationFromRoot(); + const {mutate: setMediaSyncSetting} = useSetMediaSyncSetting(); + return ( + + + + + {formatMessage(m.syncEverything)} + + + {formatMessage(m.syncEverythingDescription)} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + }, + bodyText: { + marginTop: 10, + textAlign: 'center', + }, +}); diff --git a/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncPreviewsBottomSheet.tsx b/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncPreviewsBottomSheet.tsx new file mode 100644 index 000000000..5fad50bb6 --- /dev/null +++ b/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/SyncPreviewsBottomSheet.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import {View, StyleSheet} from 'react-native'; +import {BottomSheetWrapper} from '../../../../sharedComponents/BottomSheetWrapper'; +import {defineMessages, useIntl} from 'react-intl'; +import Warning from '../../../../images/Warning.svg'; +import {Button} from '../../../../sharedComponents/Button'; +import {HeaderText} from '../../../../sharedComponents/Text/HeaderText'; +import {useNavigationFromRoot} from '../../../../hooks/useNavigationWithTypes'; +import {BodyText} from '../../../../sharedComponents/Text/BodyText'; +import {useSetMediaSyncSetting} from '../../../../hooks/server/mediaSync'; + +const m = defineMessages({ + syncPreviewsBottomSheet: { + id: 'screens.SyncPreviewBottomSheet.syncPreviewsButtonBottomSheet', + defaultMessage: 'Sync Previews?', + }, + syncPreviewsDescriptionBottomSheet: { + id: 'screens.SyncPreviewBottomSheet.syncPreviewsDescriptionBottomSheet', + defaultMessage: + 'Your device will keep all existing data but new observations will sync in a smaller, preview size.', + }, + syncPreviewWarningBottomSheet: { + id: 'screens.SyncPreviewBottomSheet.syncPreviewWarningBottomSheet', + defaultMessage: 'You will no longer sync Audio or Video.', + }, + cancel: { + id: 'screens.SyncPreviewBottomSheet.cancel', + defaultMessage: 'Cancel', + }, + confirm: { + id: 'screens.SyncPreviewBottomSheet.confirm', + defaultMessage: 'Sync Previews', + }, +}); + +export const SyncPreviewsBottomSheet = () => { + const {formatMessage} = useIntl(); + const {goBack} = useNavigationFromRoot(); + const {mutate: setMediaSyncSetting} = useSetMediaSyncSetting(); + return ( + + + + + {formatMessage(m.syncPreviewsBottomSheet)} + + + {formatMessage(m.syncPreviewsDescriptionBottomSheet)} + + + {formatMessage(m.syncPreviewWarningBottomSheet)} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + }, + bodyText: { + marginTop: 10, + textAlign: 'center', + }, +}); diff --git a/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/index.tsx b/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/index.tsx new file mode 100644 index 000000000..a824a8403 --- /dev/null +++ b/src/frontend/screens/Settings/ProjectSettings/MediaSyncSettings/index.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import {ScrollView, StyleSheet} from 'react-native'; +import {useIntl, defineMessages} from 'react-intl'; +import {SelectOne} from '../../../../sharedComponents/SelectOne'; +import {SYNC_BACKGROUND} from '../../../../lib/styles'; +import {MediaSyncSetting} from '../../../../sharedTypes'; +import { + UPDATE_MEDIA_SETTING, + useGetMediaSyncSetting, +} from '../../../../hooks/server/mediaSync'; +import {NativeNavigationComponent} from '../../../../sharedTypes/navigation'; +import {useMutationState} from '@tanstack/react-query'; + +const m = defineMessages({ + syncSettingsTitle: { + id: 'screens.MediaSyncSettings.title', + defaultMessage: 'Sync Settings', + }, + syncPreviews: { + id: 'screens.MediaSyncSettings.syncPreviews', + defaultMessage: 'Sync Previews (Photos Only)', + }, + syncPreviewsDescription: { + id: 'screens.MediaSyncSettings.syncPreviewsDescription', + defaultMessage: + 'Photos will sync at a reduced smaller size. Device will not sync audio or video.', + }, + syncEverything: { + id: 'screens.MediaSyncSettings.syncEverything', + defaultMessage: 'Sync Everything', + }, + syncEverythingDescription: { + id: 'screens.MediaSyncSettings.syncEverythingDescription', + defaultMessage: + 'Your device will sync all content at full size, including photos, audio, and videos.', + }, + syncEverythingWarning: { + id: 'screens.MediaSyncSettings.syncEverythingWarning', + defaultMessage: 'Note: This will use more storage.', + }, +}); + +export const MediaSyncSettings: NativeNavigationComponent< + 'MediaSyncSettings' +> = ({navigation}) => { + const {formatMessage: t} = useIntl(); + const {data: mediaSyncSetting} = useGetMediaSyncSetting(); + + const optimisticSyncSetting = useMutationState({ + filters: {mutationKey: [UPDATE_MEDIA_SETTING], status: 'pending'}, + select: mutation => mutation.state.variables as MediaSyncSetting, + })[0]; + + const handleOptionChange = (value: MediaSyncSetting) => { + if (value === 'previews') { + navigation.navigate('SyncPreviewsBottomSheet'); + return; + } + if (value === 'everything') { + navigation.navigate('SyncEverythingBottomSheet'); + return; + } + }; + + const options: { + value: MediaSyncSetting; + label: string; + hint: React.ReactNode; + }[] = [ + { + value: 'previews', + label: t(m.syncPreviews), + hint: t(m.syncPreviewsDescription), + }, + { + value: 'everything', + label: t(m.syncEverything), + hint: ( + <> + {t(m.syncEverythingDescription)} + {'\n\n'} + {t(m.syncEverythingWarning)} + + ), + }, + ]; + + return ( + + + + ); +}; + +MediaSyncSettings.navTitle = m.syncSettingsTitle; + +const styles = StyleSheet.create({ + container: { + padding: 20, + }, +}); diff --git a/src/frontend/sharedComponents/BottomSheetWrapper.tsx b/src/frontend/sharedComponents/BottomSheetWrapper.tsx new file mode 100644 index 000000000..b0ce05f2b --- /dev/null +++ b/src/frontend/sharedComponents/BottomSheetWrapper.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import {View} from 'react-native'; +import Animated, {SlideInDown, SlideOutDown} from 'react-native-reanimated'; +import {WHITE} from '../lib/styles'; +import {useNavigation} from '@react-navigation/native'; +import {usePreventAndroidBackButton} from '../hooks/usePreventAndroidBackButton'; + +/** + * + * @description A wrapper component that should be used for bottom sheets. It will handle the animation and prevent the back button from closing the bottom sheet. + * + * When pushing a bottom sheet ontop of another bottom sheet use `navigation.replace`, to close the original bottom sheet first. + */ +export const BottomSheetWrapper = ({children}: {children: React.ReactNode}) => { + const navigation = useNavigation(); + + const [displayContent, setDisplayContent] = React.useState(true); + + usePreventAndroidBackButton(); + + // This effect is used to prevent the bottom sheet from being removed before the animation is complete + React.useEffect(() => { + const unsubscribe = navigation.addListener('beforeRemove', e => { + e.preventDefault(); + setDisplayContent(false); + setTimeout(() => { + navigation.dispatch(e.data.action); + }, 140); + }); + + return () => { + unsubscribe(); + }; + }, [navigation]); + + return ( + + {displayContent && ( + + {children} + + )} + + ); +}; diff --git a/src/frontend/sharedTypes/navigation.ts b/src/frontend/sharedTypes/navigation.ts index 862c506e7..6f629ae1e 100644 --- a/src/frontend/sharedTypes/navigation.ts +++ b/src/frontend/sharedTypes/navigation.ts @@ -101,6 +101,8 @@ export type RootStackParamsList = { Audio: {isEditing: boolean; uri?: string; isSavedUri?: boolean}; MapManagement: undefined; BackgroundMaps: undefined; + SyncPreviewsBottomSheet: undefined; + SyncEverythingBottomSheet: undefined; }; export type OnboardingParamsList = {