From 46748bac97edfc4caf4fa3ac7a424c2d9f655bcd Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:51:26 +1100 Subject: [PATCH] Add alerts and loading screen to photo sharing functionality (#1261) * Add alerts and loading screen to photo sharing; closes #1203 * Prevent getting stuck on PhotoSharing when you back out of ObsEdit --------- Co-authored-by: Ken-ichi Ueda --- src/components/PhotoSharing.js | 125 ++++++++++++++++++ .../StackNavigators/SharedStackScreens.js | 6 + src/sharedHooks/useShare.js | 83 ++---------- 3 files changed, 144 insertions(+), 70 deletions(-) create mode 100644 src/components/PhotoSharing.js diff --git a/src/components/PhotoSharing.js b/src/components/PhotoSharing.js new file mode 100644 index 000000000..7ff6c6a0e --- /dev/null +++ b/src/components/PhotoSharing.js @@ -0,0 +1,125 @@ +// @flow + +import { useNavigation, useRoute } from "@react-navigation/native"; +import { ActivityAnimation, ViewWrapper } from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import type { Node } from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { Alert, Platform } from "react-native"; +import Observation from "realmModels/Observation"; +import { log } from "sharedHelpers/logger"; +import useStore from "stores/useStore"; + +const logger = log.extend( "PhotoSharing" ); + +const PhotoSharing = ( ): Node => { + const navigation = useNavigation( ); + const { params } = useRoute( ); + const { item } = params; + const resetStore = useStore( state => state.resetStore ); + const setObservations = useStore( state => state.setObservations ); + const setPhotoImporterState = useStore( state => state.setPhotoImporterState ); + const [navigationHandled, setNavigationHandled] = useState( null ); + + const createObservationAndNavToObsEdit = useCallback( async photoUris => { + try { + const newObservation = await Observation.createObservationWithPhotos( photoUris ); + setObservations( [newObservation] ); + navigation.navigate( "ObsEdit" ); + } catch ( e ) { + Alert.alert( + "Photo sharing failed: couldn't create new observation:", + e + ); + } + }, [ + navigation, + setObservations + ] ); + + useEffect( ( ) => { + const { mimeType, data } = item; + + if ( Platform.OS === "android" && !mimeType.startsWith( "image/" ) ) { + Alert.alert( "Android photo share failed: not an image file" ); + return; + } + + // Move to ObsEdit screen (new observation, with shared photos). + logger.info( "calling resetStore" ); + resetStore( ); + + // Create a new observation with multiple shared photos (one or more) + let photoUris:any[]; + + // data is returned as a string for a single photo on Android + // and an object with an array of data strings on iOS, i.e. + // [{"data": "", "", "mimeType": "image/jpeg] + if ( Array.isArray( data ) ) { + photoUris = data; + } else { + photoUris = [data]; + } + + logger.info( "photoUris: ", photoUris ); + + if ( Platform.OS === "android" ) { + photoUris = photoUris.map( x => ( { image: { uri: x } } ) ); + } else { + photoUris = photoUris + .filter( x => x.mimeType && x.mimeType.startsWith( "image/" ) ) + .map( x => ( { image: { uri: x.data } } ) ); + } + + if ( photoUris.length === 1 ) { + // Only one photo - go to ObsEdit directly + logger.info( "creating observation in useShare with photoUris: ", photoUris ); + createObservationAndNavToObsEdit( photoUris ); + } else { + // Go to GroupPhotos screen + setPhotoImporterState( { + galleryUris: photoUris.map( x => x.image.uri ), + groupedPhotos: photoUris.map( photo => ( { + photos: [photo] + } ) ) + } ); + navigation.navigate( "CameraNavigator", { screen: "GroupPhotos" } ); + } + }, [ + createObservationAndNavToObsEdit, + item, + navigation, + resetStore, + setObservations, + setPhotoImporterState + ] ); + + // When the user leaves this screen, we record the fact that navigation was handled... + useEffect( ( ) => { + const unsubscribe = navigation.addListener( "blur", ( ) => { + setNavigationHandled( true ); + } ); + return unsubscribe; + }, [navigation] ); + // ...and if they focus on this screen again, that means they backed out of + // obs edit and need to back to the previous screen in the nav + useEffect( ( ) => { + const unsubscribe = navigation.addListener( "focus", ( ) => { + if ( navigationHandled ) navigation.goBack( ); + } ); + return unsubscribe; + }, [ + navigation, + navigationHandled + ] ); + + return ( + + + + + + ); +}; + +export default PhotoSharing; diff --git a/src/navigation/StackNavigators/SharedStackScreens.js b/src/navigation/StackNavigators/SharedStackScreens.js index b1955c6bd..7f8d088e0 100644 --- a/src/navigation/StackNavigators/SharedStackScreens.js +++ b/src/navigation/StackNavigators/SharedStackScreens.js @@ -4,6 +4,7 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"; import LocationPickerContainer from "components/LocationPicker/LocationPickerContainer"; import ObsEdit from "components/ObsEdit/ObsEdit"; +import PhotoSharing from "components/PhotoSharing"; import { Heading4 } from "components/SharedComponents"; import SuggestionsContainer from "components/Suggestions/SuggestionsContainer"; import TaxonSearch from "components/Suggestions/TaxonSearch"; @@ -64,6 +65,11 @@ const SharedStackScreens = ( ): Node => ( component={TaxonDetails} options={hideHeader} /> + ); diff --git a/src/sharedHooks/useShare.js b/src/sharedHooks/useShare.js index f04ad56b9..f89a4d58f 100644 --- a/src/sharedHooks/useShare.js +++ b/src/sharedHooks/useShare.js @@ -2,12 +2,7 @@ import { useNavigation } from "@react-navigation/native"; import { useCallback, useEffect } from "react"; -import { Platform } from "react-native"; import ShareMenu from "react-native-share-menu"; -import Observation from "realmModels/Observation"; -import useStore from "stores/useStore"; - -import { log } from "../../react-native-logs.config"; type SharedItem = { mimeType: string, @@ -15,90 +10,38 @@ type SharedItem = { extraData: any, }; -const logger = log.extend( "useShare" ); - const useShare = ( ): void => { const navigation = useNavigation( ); - const resetStore = useStore( state => state.resetStore ); - const setObservations = useStore( state => state.setObservations ); - const setPhotoImporterState = useStore( state => state.setPhotoImporterState ); const handleShare = useCallback( async ( item: ?SharedItem ) => { if ( !item ) { - logger.info( "no item" ); + // user hasn't shared any items return; } const { mimeType, data } = item; - if ( ( !data ) - || ( - ( Platform.OS === "android" ) - && ( ( !mimeType ) || ( !mimeType.startsWith( "image/" ) ) ) - ) - ) { - logger.info( "no data or not an image" ); + if ( !mimeType && !data ) { + // user hasn't shared any images return; } - // Move to ObsEdit screen (new observation, with shared photos). - logger.info( "calling resetStore" ); - resetStore( ); + // show user a loading animation screen (like PhotoGallery) + // while observations are created + navigation.navigate( "PhotoSharing", { item } ); + }, [navigation] ); - // Create a new observation with multiple shared photos (one or more) - - let photoUris:any[]; - - if ( Array.isArray( data ) ) { - photoUris = data; - } else { - photoUris = [data]; - } - - logger.info( "photoUris: ", photoUris ); - - if ( Platform.OS === "android" ) { - photoUris = photoUris.map( x => ( { image: { uri: x } } ) ); - } else { - photoUris = photoUris - .filter( x => x.mimeType && x.mimeType.startsWith( "image/" ) ) - .map( x => ( { image: { uri: x.data } } ) ); - } - - if ( photoUris.length === 1 ) { - // Only one photo - go to ObsEdit directly - logger.info( "creating observations in useShare with photoUris: ", photoUris ); - const newObservations = await Promise.all( [{ photos: photoUris }].map( - ( { photos } ) => Observation.createObservationWithPhotos( photos ) - ) ); - setObservations( newObservations ); - - navigation.navigate( "ObsEdit" ); - } else { - // Go to GroupPhotos screen - setPhotoImporterState( { - galleryUris: photoUris.map( x => x.image.uri ), - groupedPhotos: photoUris.map( photo => ( { - photos: [photo] - } ) ) - } ); - navigation.navigate( "CameraNavigator", { screen: "GroupPhotos" } ); - } - }, [resetStore, setObservations, navigation, setPhotoImporterState] ); - - useEffect( () => { + useEffect( ( ) => { ShareMenu.getInitialShare( handleShare ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); + }, [handleShare] ); - useEffect( () => { + useEffect( ( ) => { const listener = ShareMenu.addNewShareListener( handleShare ); - return () => { - listener?.remove(); + return ( ) => { + listener?.remove( ); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); + }, [handleShare] ); }; export default useShare;