diff --git a/client/src/lib/components/ProfileInfo/ProfileInfo.tsx b/client/src/lib/components/ProfileInfo/ProfileInfo.tsx index c015217858..f7e3d8c910 100644 --- a/client/src/lib/components/ProfileInfo/ProfileInfo.tsx +++ b/client/src/lib/components/ProfileInfo/ProfileInfo.tsx @@ -11,6 +11,7 @@ import useUpdateProfileDetails from '../../user/hooks/useUpdateProfileDetails'; import ProfilePicture from '../User/ProfilePicture'; import useUser from '../../user/hooks/useUser'; import {COLORS} from '../../../../../shared/src/constants/colors'; +import useHideModalUntilResolved from '../../navigation/hooks/useHideModalUntilResolved'; const Container = styled.View({ alignItems: 'center', @@ -46,6 +47,8 @@ const ProfileInfo: React.FC = ({onSaveCallback}) => { useChangeProfilePicture(); const {updateProfileDetails, isUpdatingProfileDetails} = useUpdateProfileDetails(); + const hideModalAndChangeProfilePicture = + useHideModalUntilResolved(changeProfilePicture); const [displayName, setDisplayName] = useState(user?.displayName ?? ''); const [nameMissing, setNameMissing] = useState(false); const [pictureMissing, setPictureMissing] = useState(false); @@ -94,7 +97,7 @@ const ProfileInfo: React.FC = ({onSaveCallback}) => { pictureURL={user?.photoURL} hasError={pictureMissing} loading={isUpdatingProfilePicture} - onPress={changeProfilePicture} + onPress={hideModalAndChangeProfilePicture} size={SPACINGS.NINTYSIX} /> diff --git a/client/src/lib/navigation/ModalStack.tsx b/client/src/lib/navigation/ModalStack.tsx index d965a8ac77..a9080ce6c6 100644 --- a/client/src/lib/navigation/ModalStack.tsx +++ b/client/src/lib/navigation/ModalStack.tsx @@ -56,7 +56,7 @@ const modalScreenOptions: BottomSheetNavigationOptions = { pressBehavior="close" animatedIndex={animatedIndex} animatedPosition={animatedPosition} - disappearsOnIndex={-1} + disappearsOnIndex={-0.1} appearsOnIndex={0} opacity={0.1} style={style} @@ -85,7 +85,7 @@ const modalScreenOptions: BottomSheetNavigationOptions = { https://github.com/gorhom/react-native-bottom-sheet/issues/618 */ android_keyboardInputMode: 'adjustResize', - stackBehavior: 'push', + stackBehavior: 'replace', }; const ModalStack = () => { diff --git a/client/src/lib/navigation/hooks/useShareFromModal.ts b/client/src/lib/navigation/hooks/useHideModalUntilResolved.ts similarity index 74% rename from client/src/lib/navigation/hooks/useShareFromModal.ts rename to client/src/lib/navigation/hooks/useHideModalUntilResolved.ts index c41af3c756..7cacc8df43 100644 --- a/client/src/lib/navigation/hooks/useShareFromModal.ts +++ b/client/src/lib/navigation/hooks/useHideModalUntilResolved.ts @@ -1,23 +1,25 @@ import {useBottomSheet} from '@gorhom/bottom-sheet'; import {useCallback} from 'react'; -import {Share, ShareContent, ShareOptions} from 'react-native'; /* This hook hides the current modal and opens the share modal to prevent it from being rendered behind the current modal Bottom sheet modals are rendered with FullWindowModal, which is a full-screen modal that covers the entire screen https://github.com/th3rdwave/react-navigation-bottom-sheet/blob/ef8c616559a3fdbb67149d6e1ebc9bb662d71255/src/BottomSheetView.tsx#L27-L31 */ -const useShareFromModal = () => { +const useHideModalUntilResolved = ( + fn: (...args: TArgs) => Promise, +) => { const bottomSheet = useBottomSheet(); return useCallback( - async (content: ShareContent, options?: ShareOptions) => { + async (...args: TArgs) => { const currentSnapIndex = bottomSheet.animatedIndex.value; bottomSheet.snapToPosition(0.0000001); // Hide without closing it - await Share.share(content, options); + const ret = await fn(...args); bottomSheet.snapToIndex(currentSnapIndex); // Restore the position + return ret; }, - [bottomSheet], + [bottomSheet, fn], ); }; -export default useShareFromModal; +export default useHideModalUntilResolved; diff --git a/client/src/lib/user/hooks/useChangeProfilePicture.ts b/client/src/lib/user/hooks/useChangeProfilePicture.ts index 03116d7c04..5c816b0f72 100644 --- a/client/src/lib/user/hooks/useChangeProfilePicture.ts +++ b/client/src/lib/user/hooks/useChangeProfilePicture.ts @@ -65,127 +65,132 @@ const useChangeProfilePicture = () => { const [isUpdatingProfilePicture, setIsUpdatingProfilePicture] = useState(false); - const changeProfilePicture = useCallback(async () => { - await ensureUserCreated(); - - const currentUser = auth().currentUser; - - const optionTitles = [ - t('takePhotoButtonTitle'), - t('chooseFromLibraryButtonTitle'), - t('cancelButtonTitle'), - currentUser?.photoURL ? t('removeButtonTitle') : null, - ].filter(Boolean) as Array; - - const optionIndex = { - CAMERA: 0, - LIBRARY: 1, - CANCEL: 2, - REMOVE: 3, - }; - - const imagePickerOptions = { - width: 500, - height: 500, - cropping: true, - useFrontCamera: true, - forceJpg: true, - }; - - const captureProfilePicture = async () => { - try { - const image = await openCamera(imagePickerOptions); - setIsUpdatingProfilePicture(true); - await uploadProfilePicture(image.path, currentUser); - setIsUpdatingProfilePicture(false); - } catch (error: any) { - setIsUpdatingProfilePicture(false); - if (error.code !== E_PICKER_CANCELLED) { - console.error( - new Error('Select profile from camera failed:', { - cause: error, - }), - ); - } - } - }; - - ActionSheet.showActionSheetWithOptions( - { - title: t('title'), - options: optionTitles, - tintColor: 'blue', - cancelButtonIndex: optionIndex.CANCEL, - }, - async buttonIndex => { - switch (buttonIndex) { - case optionIndex.CAMERA: { - if (Platform.OS !== 'android') { - captureProfilePicture(); - } else { - const isCameraAvailable = await PermissionsAndroid.check( - PermissionsAndroid.PERMISSIONS.CAMERA, + const changeProfilePicture = useCallback( + () => + new Promise(async resolve => { + await ensureUserCreated(); + + const currentUser = auth().currentUser; + + const optionTitles = [ + t('takePhotoButtonTitle'), + t('chooseFromLibraryButtonTitle'), + t('cancelButtonTitle'), + currentUser?.photoURL ? t('removeButtonTitle') : null, + ].filter(Boolean) as Array; + + const optionIndex = { + CAMERA: 0, + LIBRARY: 1, + CANCEL: 2, + REMOVE: 3, + }; + + const imagePickerOptions = { + width: 500, + height: 500, + cropping: true, + useFrontCamera: true, + forceJpg: true, + }; + + const captureProfilePicture = async () => { + try { + const image = await openCamera(imagePickerOptions); + setIsUpdatingProfilePicture(true); + await uploadProfilePicture(image.path, currentUser); + setIsUpdatingProfilePicture(false); + } catch (error: any) { + setIsUpdatingProfilePicture(false); + if (error.code !== E_PICKER_CANCELLED) { + console.error( + new Error('Select profile from camera failed:', { + cause: error, + }), ); - if (isCameraAvailable) { - captureProfilePicture(); - } else { - try { - const granted = await PermissionsAndroid.request( + } + } + }; + + ActionSheet.showActionSheetWithOptions( + { + title: t('title'), + options: optionTitles, + tintColor: 'blue', + cancelButtonIndex: optionIndex.CANCEL, + }, + async buttonIndex => { + switch (buttonIndex) { + case optionIndex.CAMERA: { + if (Platform.OS !== 'android') { + await captureProfilePicture(); + } else { + const isCameraAvailable = await PermissionsAndroid.check( PermissionsAndroid.PERMISSIONS.CAMERA, - { - buttonPositive: t('ok'), - title: t('alertCameraTitle'), - message: t('alertCameraWhy'), - }, ); - if (granted === PermissionsAndroid.RESULTS.GRANTED) { - captureProfilePicture(); - } else if ( - granted === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN - ) { - Alert.alert( - t('alertCameraTitle'), - t('alertCameraSettings'), - ); + if (isCameraAvailable) { + await captureProfilePicture(); + } else { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.CAMERA, + { + buttonPositive: t('ok'), + title: t('alertCameraTitle'), + message: t('alertCameraWhy'), + }, + ); + if (granted === PermissionsAndroid.RESULTS.GRANTED) { + await captureProfilePicture(); + } else if ( + granted === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN + ) { + Alert.alert( + t('alertCameraTitle'), + t('alertCameraSettings'), + ); + } + } catch (err) { + console.error(err); + } } - } catch (err) { - console.error(err); } + break; } - } - break; - } - case optionIndex.LIBRARY: - openPicker(imagePickerOptions).then( - async image => { + case optionIndex.LIBRARY: + await openPicker(imagePickerOptions).then( + async image => { + setIsUpdatingProfilePicture(true); + await uploadProfilePicture(image.path, currentUser); + setIsUpdatingProfilePicture(false); + }, + error => { + setIsUpdatingProfilePicture(false); + if (error.code !== E_PICKER_CANCELLED) { + console.error( + new Error('Select profile from library failed:', { + cause: error, + }), + ); + } + }, + ); + break; + case optionIndex.REMOVE: { setIsUpdatingProfilePicture(true); - await uploadProfilePicture(image.path, currentUser); - setIsUpdatingProfilePicture(false); - }, - error => { + await removeProfilePicture(currentUser); setIsUpdatingProfilePicture(false); - if (error.code !== E_PICKER_CANCELLED) { - console.error( - new Error('Select profile from library failed:', { - cause: error, - }), - ); - } - }, - ); - break; - case optionIndex.REMOVE: { - setIsUpdatingProfilePicture(true); - await removeProfilePicture(currentUser); - setIsUpdatingProfilePicture(false); - break; - } - default: - break; - } - }, - ); - }, [t, setIsUpdatingProfilePicture]); + break; + } + default: + break; + } + resolve(); + }, + ); + }), + [t, setIsUpdatingProfilePicture], + ); return { changeProfilePicture, diff --git a/client/src/routes/modals/AssignNewHostModal/AssignNewHostModal.tsx b/client/src/routes/modals/AssignNewHostModal/AssignNewHostModal.tsx index ea4daa8049..dce69751bb 100644 --- a/client/src/routes/modals/AssignNewHostModal/AssignNewHostModal.tsx +++ b/client/src/routes/modals/AssignNewHostModal/AssignNewHostModal.tsx @@ -1,7 +1,7 @@ import {RouteProp, useIsFocused, useRoute} from '@react-navigation/native'; import React, {useCallback, useEffect} from 'react'; import {useTranslation} from 'react-i18next'; -import {View} from 'react-native'; +import {Share, View} from 'react-native'; import {BottomSheetScrollView} from '@gorhom/bottom-sheet'; import styled from 'styled-components/native'; @@ -25,7 +25,7 @@ import {SPACINGS} from '../../../lib/constants/spacings'; import {ModalHeading} from '../../../lib/components/Typography/Heading/Heading'; import useConfirmSessionReminder from '../../../lib/sessions/hooks/useConfirmSessionReminder'; import {getSessionHostingLink} from '../../../lib/sessions/api/session'; -import useShareFromModal from '../../../lib/navigation/hooks/useShareFromModal'; +import useHideModalUntilResolved from '../../../lib/navigation/hooks/useHideModalUntilResolved'; const Row = styled(View)({ flexDirection: 'row', @@ -42,18 +42,18 @@ const AssignNewHostModal = () => { const exercise = useExerciseById(session?.exerciseId, session?.language); const confirmToggleReminder = useConfirmSessionReminder(session); - const share = useShareFromModal(); + const hideModalAndShare = useHideModalUntilResolved(Share.share); const isHost = user?.uid === session.hostId; const onHostChange = useCallback(async () => { const link = await getSessionHostingLink(session.id); if (link) { - share({ + hideModalAndShare({ message: link, }); } - }, [session.id, share]); + }, [session.id, hideModalAndShare]); useEffect(() => { if (isHost) { diff --git a/client/src/routes/modals/CreateSessionModal/components/steps/SelectTypeStep.tsx b/client/src/routes/modals/CreateSessionModal/components/steps/SelectTypeStep.tsx index e9b5cf01a5..083d34e52e 100644 --- a/client/src/routes/modals/CreateSessionModal/components/steps/SelectTypeStep.tsx +++ b/client/src/routes/modals/CreateSessionModal/components/steps/SelectTypeStep.tsx @@ -1,4 +1,5 @@ import React, {Fragment, useCallback, useMemo} from 'react'; +import {Share} from 'react-native'; import {useTranslation} from 'react-i18next'; import styled from 'styled-components/native'; @@ -56,7 +57,7 @@ import CoCreators from '../../../../../lib/components/CoCreators/CoCreators'; import CardGraphic from '../../../../../lib/components/CardGraphic/CardGraphic'; import BackgroundBlock from '../../../../../lib/components/BackgroundBlock/BackgroundBlock'; import useGetSessionCardTags from '../../../../../lib/components/Cards/SessionCard/hooks/useGetSessionCardTags'; -import useShareFromModal from '../../../../../lib/navigation/hooks/useShareFromModal'; +import useHideModalUntilResolved from '../../../../../lib/navigation/hooks/useHideModalUntilResolved'; const TypeItemWrapper = styled.View<{isLast?: boolean}>(({isLast}) => ({ flexDirection: 'row', @@ -161,7 +162,7 @@ const SelectTypeStep: React.FC = ({ useNavigation>(); const getExerciseById = useGetExerciseById(); const startSession = useStartAsyncSession(); - const share = useShareFromModal(); + const hideModalAndShare = useHideModalUntilResolved(Share.share); const {rating} = useExerciseRating(selectedExercise); const {feedback} = useExerciseFeedback(selectedExercise); @@ -196,11 +197,11 @@ const SelectTypeStep: React.FC = ({ const onShare = useCallback(() => { if (exercise?.link) { - share({ + hideModalAndShare({ message: exercise.link, }); } - }, [exercise?.link, share]); + }, [exercise?.link, hideModalAndShare]); const onStartPress = useCallback(() => { if (selectedExercise) { diff --git a/client/src/routes/modals/ProfileSettingsModal/ProfileSettingsModal.tsx b/client/src/routes/modals/ProfileSettingsModal/ProfileSettingsModal.tsx index 63c92c609d..a4871b9a5a 100644 --- a/client/src/routes/modals/ProfileSettingsModal/ProfileSettingsModal.tsx +++ b/client/src/routes/modals/ProfileSettingsModal/ProfileSettingsModal.tsx @@ -41,6 +41,7 @@ import {SPACINGS} from '../../../lib/constants/spacings'; import useUserState from '../../../lib/user/state/state'; import ActionSwitch from '../../../lib/components/ActionList/ActionItems/ActionSwitch'; import useLogMindfulMinutes from '../../../lib/mindfulMinutes/hooks/useLogMindfulMinutes'; +import useHideModalUntilResolved from '../../../lib/navigation/hooks/useHideModalUntilResolved'; const Picture = styled(ProfilePicture)({ width: 144, @@ -71,6 +72,8 @@ const ProfileSettingsModal = () => { useChangeProfilePicture(); const {updateProfileDetails, isUpdatingProfileDetails} = useUpdateProfileDetails(); + const hideModalAndChangeProfilePicture = + useHideModalUntilResolved(changeProfilePicture); const {deleteUser} = useDeleteUser(); const signOut = useSignOutUser(); const user = useUser(); @@ -157,7 +160,7 @@ const ProfileSettingsModal = () => { pictureURL={user?.photoURL} letter={user?.displayName?.[0]} loading={isUpdatingProfilePicture} - onPress={changeProfilePicture} + onPress={hideModalAndChangeProfilePicture} /> diff --git a/client/src/routes/modals/SessionModal/SessionModal.tsx b/client/src/routes/modals/SessionModal/SessionModal.tsx index 169222ea9c..24dc6f403a 100644 --- a/client/src/routes/modals/SessionModal/SessionModal.tsx +++ b/client/src/routes/modals/SessionModal/SessionModal.tsx @@ -4,6 +4,7 @@ import { useNavigation, useRoute, } from '@react-navigation/native'; +import {Share} from 'react-native'; import dayjs from 'dayjs'; import React, {Fragment, useCallback, useEffect} from 'react'; import {useTranslation} from 'react-i18next'; @@ -77,7 +78,7 @@ import BackgroundBlock from '../../../lib/components/BackgroundBlock/BackgroundB import SheetModal from '../../../lib/components/Modals/SheetModal'; import {BottomSheetScrollView} from '@gorhom/bottom-sheet'; import {SessionMode} from '../../../../../shared/src/schemas/Session'; -import useShareFromModal from '../../../lib/navigation/hooks/useShareFromModal'; +import useHideModalUntilResolved from '../../../lib/navigation/hooks/useHideModalUntilResolved'; const Content = styled(Gutters)({ justifyContent: 'space-between', @@ -155,7 +156,7 @@ const SessionModal = () => { const navigation = useNavigation>(); - const share = useShareFromModal(); + const hideModalAndshare = useHideModalUntilResolved(Share.share); const addToCalendar = useAddSessionToCalendar(); const exercise = useExerciseById(session.exerciseId, session.language); const tags = useGetSessionCardTags(exercise, SessionMode.live); @@ -204,11 +205,11 @@ const SessionModal = () => { const onShare = useCallback(() => { if (session.link) { - share({ + hideModalAndshare({ message: session.link, }); } - }, [session.link, share]); + }, [session.link, hideModalAndshare]); const onHostPress = useCallback(() => { navigation.popToTop();