Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Suggestions after camera in ObsEdit flow #1294

Merged
merged 12 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/api/taxa.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ function mapToLocalSchema( taxon ) {
async function fetchTaxon( id: any, params: Object = {}, opts: Object = {} ): Promise<any> {
try {
const fetchParams = { ...PARAMS, ...params };
const { results } = await inatjs.taxa.fetch( id, fetchParams, opts );
if ( results.length === 0 ) return null;
const response = await inatjs.taxa.fetch( id, fetchParams, opts );
const results = response?.results;
if ( !results || results.length === 0 ) return null;

return mapToLocalSchema( results[0] );
} catch ( e ) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Camera/ARCamera/ARCamera.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const ARCamera = ( {
const handlePress = async ( ) => {
await takePhoto( );
handleCheckmarkPress( showPrediction
? result.taxon
? result
: null );
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/Camera/Buttons/Close.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const Close = ( ): Node => {
return (
<TransparentCircleButton
onPress={( ) => navigation.goBack( )}
accessibilityLabel={t( "Close-AR-camera" )}
accessibilityLabel={t( "Close" )}
accessibilityHint={t( "Navigate-to-previous-screen" )}
icon="close"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Camera/Buttons/GreenCheckmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const GreenCheckmark = ( {
<INatIconButton
onPress={handleCheckmarkPress}
accessibilityLabel={t( "Checkmark" )}
accessibilityHint={t( "Navigate-to-observation-edit-screen" )}
accessibilityHint={t( "Navigate-to-suggestions" )}
disabled={false}
icon="checkmark-circle"
color={colors.inatGreen}
Expand Down
64 changes: 35 additions & 29 deletions src/components/Camera/CameraWithDevice.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import { useNavigation } from "@react-navigation/native";
import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
import PermissionGateContainer, { WRITE_MEDIA_PERMISSIONS }
from "components/SharedComponents/PermissionGateContainer";
import { View } from "components/styledComponents";
Expand All @@ -18,7 +19,7 @@ import useDeviceOrientation, {
} from "sharedHooks/useDeviceOrientation";

import ARCamera from "./ARCamera/ARCamera";
import usePrepareStateForObsEdit from "./hooks/usePrepareStateForObsEdit";
import usePrepareStoreAndNavigate from "./hooks/usePrepareStoreAndNavigate";
import StandardCamera from "./StandardCamera/StandardCamera";

const isTablet = DeviceInfo.isTablet( );
Expand Down Expand Up @@ -48,7 +49,7 @@ const CameraWithDevice = ( {
const { deviceOrientation } = useDeviceOrientation( );
const [addPhotoPermissionResult, setAddPhotoPermissionResult] = useState( null );
const [checkmarkTapped, setCheckmarkTapped] = useState( false );
const [taxonResult, setTaxonResult] = useState( null );
const [visionCameraResult, setVisionCameraResult] = useState( null );
// We track this because we only want to navigate away when the permission
// gate is completely closed, because there's a good chance another will
// try to open when the user lands on the next screen, e.g. the location
Expand All @@ -57,7 +58,7 @@ const CameraWithDevice = ( {

const {
prepareStateForObsEdit
} = usePrepareStateForObsEdit( addPhotoPermissionResult, addEvidence );
} = usePrepareStoreAndNavigate( addPhotoPermissionResult, addEvidence, checkmarkTapped );

const isLandscapeMode = [LANDSCAPE_LEFT, LANDSCAPE_RIGHT].includes( deviceOrientation );

Expand All @@ -72,26 +73,22 @@ const CameraWithDevice = ( {
? "flex-row"
: "flex-col";

const navToObsEdit = useCallback( async ( ) => {
await prepareStateForObsEdit( taxonResult );
if ( taxonResult ) {
setTaxonResult( null );
}
navigation.navigate( "ObsEdit" );
}, [taxonResult, prepareStateForObsEdit, navigation] );
const storeCurrentObservation = useCallback( async ( ) => {
await prepareStateForObsEdit( visionCameraResult );
}, [visionCameraResult, prepareStateForObsEdit] );

const handleCheckmarkPress = localTaxon => {
setTaxonResult( localTaxon?.id
? localTaxon
const handleCheckmarkPress = visionResult => {
setVisionCameraResult( visionResult?.taxon
? visionResult
: null );
setCheckmarkTapped( true );
};

const onPermissionGranted = ( ) => {
const onPhotoPermissionGranted = ( ) => {
setAddPhotoPermissionResult( "granted" );
};

const onPermissionDenied = ( ) => {
const onPhotoPermissionDenied = ( ) => {
setAddPhotoPermissionResult( "denied" );
};

Expand All @@ -105,10 +102,10 @@ const CameraWithDevice = ( {
) {
setCheckmarkTapped( false );
setAddPhotoPermissionGateWasClosed( false );
navToObsEdit( );
storeCurrentObservation( );
}
}, [
navToObsEdit,
storeCurrentObservation,
checkmarkTapped,
addPhotoPermissionGateWasClosed,
addPhotoPermissionResult
Expand Down Expand Up @@ -136,19 +133,28 @@ const CameraWithDevice = ( {

return (
<View className={`flex-1 bg-black ${flexDirection}`}>
<PermissionGateContainer
permissions={WRITE_MEDIA_PERMISSIONS}
titleDenied={t( "Save-photos-to-your-gallery" )}
body={t( "iNaturalist-can-save-photos-you-take-in-the-app-to-your-devices-gallery" )}
buttonText={t( "SAVE-PHOTOS" )}
icon="gallery"
image={require( "images/birger-strahl-ksiGE4hMiso-unsplash.jpg" )}
onModalHide={( ) => setAddPhotoPermissionGateWasClosed( true )}
onPermissionGranted={onPermissionGranted}
onPermissionDenied={onPermissionDenied}
withoutNavigation
{/* a weird quirk of react-native-modal is you can show subsequent modals
when a modal is nested in another modal. location permission is shown first
because the save photo modal pops up a second system alert on iOS asking
how much access to give */}
<LocationPermissionGate
permissionNeeded={checkmarkTapped}
/>
withoutNavigation
>
<PermissionGateContainer
permissions={WRITE_MEDIA_PERMISSIONS}
titleDenied={t( "Save-photos-to-your-gallery" )}
body={t( "iNaturalist-can-save-photos-you-take-in-the-app-to-your-devices-gallery" )}
buttonText={t( "SAVE-PHOTOS" )}
icon="gallery"
image={require( "images/birger-strahl-ksiGE4hMiso-unsplash.jpg" )}
onModalHide={( ) => setAddPhotoPermissionGateWasClosed( true )}
onPermissionGranted={onPhotoPermissionGranted}
onPermissionDenied={onPhotoPermissionDenied}
withoutNavigation
permissionNeeded={checkmarkTapped}
/>
</LocationPermissionGate>
{cameraType === "Standard"
? (
<StandardCamera
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
// @flow

import { CameraRoll } from "@react-native-camera-roll/camera-roll";
import { useNavigation } from "@react-navigation/native";
import {
useCallback
} from "react";
import Observation from "realmModels/Observation";
import ObservationPhoto from "realmModels/ObservationPhoto";
import fetchUserLocation from "sharedHelpers/fetchUserLocation";
import { log } from "sharedHelpers/logger";
import useStore from "stores/useStore";

const logger = log.extend( "usePrepareStateForObsEdit" );
const logger = log.extend( "usePrepareStoreAndNavigate" );

const usePrepareStateForObsEdit = (
const usePrepareStoreAndNavigate = (
permissionGranted: ?string,
addEvidence: ?boolean
addEvidence: ?boolean,
checkmarkTapped: boolean
): Object => {
const navigation = useNavigation( );
const setObservations = useStore( state => state.setObservations );
const updateObservations = useStore( state => state.updateObservations );
const evidenceToAdd = useStore( state => state.evidenceToAdd );
Expand All @@ -30,48 +34,71 @@ const usePrepareStateForObsEdit = (
// Save URIs to camera gallery (if a photo was taken using the app,
// we want it accessible in the camera's folder, as if the user has taken those photos
// via their own camera app).
const savePhotosToCameraGallery = useCallback( async uris => {
if ( permissionGranted !== "granted" ) { return; }
const savedUris = await Promise.all( uris.map( async uri => {
const savePhotosToCameraGallery = useCallback( async ( uris, visionResult ) => {
if ( permissionGranted === "granted" ) {
const savedUris = await Promise.all( uris.map( async uri => {
// Find original camera URI of each scaled-down photo
const cameraUri = originalCameraUrisMap[uri];
const cameraUri = originalCameraUrisMap[uri];

if ( !cameraUri ) {
console.error( `Couldn't find original camera URI for: ${uri}` );
}
logger.info( "savePhotosToCameraGallery, saving cameraUri: ", cameraUri );
return CameraRoll.save( cameraUri, { type: "photo", album: "Camera" } );
} ) );
if ( !cameraUri ) {
console.error( `Couldn't find original camera URI for: ${uri}` );
}
logger.info( "savePhotosToCameraGallery, saving cameraUri: ", cameraUri );
return CameraRoll.save( cameraUri, { type: "photo", album: "Camera" } );
} ) );

logger.info( "savePhotosToCameraGallery, savedUris: ", savedUris );
// Save these camera roll URIs, so later on observation editor can update
// the EXIF metadata of these photos, once we retrieve a location.
setCameraRollUris( savedUris );
}, [originalCameraUrisMap, setCameraRollUris, permissionGranted] );
logger.info( "savePhotosToCameraGallery, savedUris: ", savedUris );
// Save these camera roll URIs, so later on observation editor can update
// the EXIF metadata of these photos, once we retrieve a location.
setCameraRollUris( savedUris );
}
navigation.push( "Suggestions", {
lastScreen: "CameraWithDevice",
hasVisionSuggestion: visionResult
} );
}, [
navigation,
originalCameraUrisMap,
permissionGranted,
setCameraRollUris
] );

const createObsWithCameraPhotos = useCallback( async ( localFilePaths, localTaxon ) => {
const createObsWithCameraPhotos = useCallback( async ( localFilePaths, visionResult ) => {
const newObservation = await Observation.new( );

// location is needed for fetching online Suggestions on the next screen
const location = await fetchUserLocation( );
if ( location?.latitude ) {
newObservation.latitude = location?.latitude;
newObservation.longitude = location?.longitude;
}
newObservation.observationPhotos = await ObservationPhoto
.createObsPhotosWithPosition( localFilePaths, {
position: 0,
local: true
} );

if ( localTaxon ) {
newObservation.taxon = localTaxon;
if ( visionResult ) {
// make sure taxon id is stored as a number, not a string, from ARCamera
visionResult.taxon.id = Number( visionResult.taxon.id );
newObservation.taxon = visionResult.taxon;
newObservation.owners_identification_from_vision = true;
newObservation.score = visionResult.score;
}
setObservations( [newObservation] );
logger.info(
"createObsWithCameraPhotos, calling savePhotosToCameraGallery with paths: ",
localFilePaths
);

return savePhotosToCameraGallery( localFilePaths );
return savePhotosToCameraGallery( localFilePaths, visionResult );
}, [savePhotosToCameraGallery, setObservations] );

const prepareStateForObsEdit = useCallback( async localTaxon => {
if ( addEvidence ) {
const prepareStateForObsEdit = useCallback( async visionResult => {
if ( !checkmarkTapped ) { return null; }
// handle case where user backs out from ObsEdit -> Suggestions -> Camera
// and already has a taxon selected
if ( addEvidence || currentObservation?.observationPhotos?.length > 0 ) {
const obsPhotos = await ObservationPhoto
.createObsPhotosWithPosition( evidenceToAdd, {
position: numOfObsPhotos,
Expand All @@ -85,12 +112,13 @@ const usePrepareStateForObsEdit = (
"addCameraPhotosToCurrentObservation, calling savePhotosToCameraGallery with paths: ",
evidenceToAdd
);
return savePhotosToCameraGallery( evidenceToAdd );
return savePhotosToCameraGallery( evidenceToAdd, visionResult );
}
return createObsWithCameraPhotos( cameraPreviewUris, localTaxon );
return createObsWithCameraPhotos( cameraPreviewUris, visionResult );
}, [
addEvidence,
cameraPreviewUris,
checkmarkTapped,
createObsWithCameraPhotos,
currentObservation,
currentObservationIndex,
Expand All @@ -102,8 +130,8 @@ const usePrepareStateForObsEdit = (
] );

return {
prepareStateForObsEdit: localTaxon => prepareStateForObsEdit( localTaxon )
prepareStateForObsEdit: visionResult => prepareStateForObsEdit( visionResult )
};
};

export default usePrepareStateForObsEdit;
export default usePrepareStoreAndNavigate;
3 changes: 2 additions & 1 deletion src/components/Camera/hooks/useTakePhoto.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useDeviceOrientation from "sharedHooks/useDeviceOrientation";
import useStore from "stores/useStore";

const useTakePhoto = ( camera: Object, addEvidence: ?boolean, device: Object ): Object => {
const currentObservation = useStore( state => state.currentObservation );
const { deviceOrientation } = useDeviceOrientation( );
const hasFlash = device?.hasFlash;
const initialPhotoOptions = {
Expand Down Expand Up @@ -46,7 +47,7 @@ const useTakePhoto = ( camera: Object, addEvidence: ?boolean, device: Object ):
} );
const uri = newPhoto.localFilePath;

if ( addEvidence ) {
if ( addEvidence || currentObservation?.observationPhotos?.length > 0 ) {
setCameraState( {
cameraPreviewUris: cameraPreviewUris.concat( [uri] ),
evidenceToAdd: [...evidenceToAdd, uri],
Expand Down
Loading
Loading