Skip to content

Commit

Permalink
Add suggestions to match screen (#2609)
Browse files Browse the repository at this point in the history
* Create useSuggestions hook with all online/offline logic for reuse in Match screen

* Show real online/offline suggestions on Match screen

* Code improvements

* Display top suggestion from useSuggestions, not AICamera
  • Loading branch information
albullington authored Jan 15, 2025
1 parent 5aa1608 commit 94ac9f2
Show file tree
Hide file tree
Showing 16 changed files with 532 additions and 258 deletions.
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
import calculateConfidence from "components/Match/calculateConfidence";
import { CustomFlashList, Heading3 } from "components/SharedComponents";
import { View } from "components/styledComponents";
import React from "react";
import {
convertOfflineScoreToConfidence,
convertOnlineScoreToConfidence
} from "sharedHelpers/convertScores.ts";
import { useTranslation } from "sharedHooks";

import SuggestionsResult from "./SuggestionsResult";

const AdditionalSuggestionsScroll = ( { suggestions } ) => {
const AdditionalSuggestionsScroll = ( { otherSuggestions, onTaxonChosen } ) => {
const { t } = useTranslation( );

const onTaxonChosen = ( ) => {
console.log( "Taxon chosen" );
};

const renderItem = ( { item: suggestion } ) => (
<SuggestionsResult
confidence={suggestion?.score
? convertOfflineScoreToConfidence( suggestion?.score )
: convertOnlineScoreToConfidence( suggestion?.combined_score )}
confidence={calculateConfidence( suggestion )}
fetchRemote={false}
handlePress={onTaxonChosen}
taxon={suggestion?.taxon}
/>
);

if ( otherSuggestions?.length === 0 ) {
return null;
}

return (
<View className="mt-4 mb-7">
<Heading3 className="mb-3">{t( "It-might-also-be" )}</Heading3>
<View className="flex-1">
<View className="h-36">
<CustomFlashList
horizontal
renderItem={renderItem}
estimatedItemSize={100}
estimatedItemSize={160}
keyExtractor={item => item?.taxon?.id}
data={suggestions}
data={otherSuggestions}
/>
</View>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const SuggestionsResult = ( {
}
onPress={handlePress}
testID={testID}
key={testID}
>
<View className="w-[62px] h-[62px] mr-3">
<ObsImagePreview
Expand Down
38 changes: 13 additions & 25 deletions src/components/Match/Match.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,44 @@ import { View } from "components/styledComponents";
import React from "react";
import { useTranslation } from "sharedHooks";

import AdditionalSuggestionsScroll from "./AdditionalSuggestions/AdditionalSuggestionsScroll";
import AdditionalSuggestionsScroll
from "./AdditionalSuggestions/AdditionalSuggestionsScroll";
import EmptyMapSection from "./EmptyMapSection";
import MatchHeader from "./MatchHeader";
import PhotosSection from "./PhotosSection";
import SaveDiscardButtons from "./SaveDiscardButtons";

// example data
const secondTaxon = {
name: "Aves",
preferred_common_name: "Birds",
id: 3
};

type Props = {
observation: Object,
observationPhoto: string,
handleSaveOrDiscardPress: ( ) => void,
navToTaxonDetails: ( ) => void,
taxon: Object,
confidence: number,
handleLocationPickerPressed: ( ) => void
handleLocationPickerPressed: ( ) => void,
suggestions: Object,
onTaxonChosen: ( ) => void
}

const Match = ( {
observation,
observationPhoto,
handleSaveOrDiscardPress,
navToTaxonDetails,
taxon,
confidence,
handleLocationPickerPressed
handleLocationPickerPressed,
suggestions,
onTaxonChosen
}: Props ) => {
const { t } = useTranslation( );

const latitude = observation?.privateLatitude || observation?.latitude;
const observationPhoto = observation?.observationPhotos?.[0]?.photo?.url
|| observation?.observationPhotos?.[0]?.photo?.localFilePath;

return (
<>
<ScrollViewWrapper>
<Divider />
<View className="p-5">
<MatchHeader
taxon={taxon}
confidence={confidence}
/>
<MatchHeader topSuggestion={suggestions?.topSuggestion} />
</View>
<PhotosSection
taxon={taxon}
Expand Down Expand Up @@ -81,13 +74,8 @@ const Match = ( {
accessibilityHint={t( "Navigates-to-taxon-details" )}
/>
<AdditionalSuggestionsScroll
suggestions={[{
score: 0.99,
taxon
}, {
score: 0.86,
taxon: secondTaxon
}]}
onTaxonChosen={onTaxonChosen}
otherSuggestions={suggestions?.otherSuggestions}
/>
{!latitude && (
<Button
Expand Down
190 changes: 176 additions & 14 deletions src/components/Match/MatchContainer.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,94 @@
import {
useNetInfo
} from "@react-native-community/netinfo";
import { useNavigation } from "@react-navigation/native";
import flattenUploadParams from "components/Suggestions/helpers/flattenUploadParams.ts";
import {
FETCH_STATUS_LOADING,
FETCH_STATUS_OFFLINE_ERROR,
FETCH_STATUS_OFFLINE_FETCHED,
FETCH_STATUS_ONLINE_ERROR,
FETCH_STATUS_ONLINE_FETCHED,
initialSuggestions
} from "components/Suggestions/SuggestionsContainer.tsx";
import _ from "lodash";
import { RealmContext } from "providers/contexts.ts";
import React from "react";
import React, {
useCallback, useEffect, useReducer, useRef
} from "react";
import saveObservation from "sharedHelpers/saveObservation.ts";
import {
useExitObservationFlow,
useLocationPermission,
useTaxon
useExitObservationFlow, useLocationPermission, useSuggestions, useTaxon
} from "sharedHooks";
import useStore from "stores/useStore";

import Match from "./Match";

const setQueryKey = ( selectedPhotoUri, shouldUseEvidenceLocation ) => [
"scoreImage",
selectedPhotoUri,
{ shouldUseEvidenceLocation }
];

const initialState = {
fetchStatus: FETCH_STATUS_LOADING,
scoreImageParams: null,
queryKey: [],
selectedTaxon: null,
shouldUseEvidenceLocation: false
};

const reducer = ( state, action ) => {
switch ( action.type ) {
case "SET_UPLOAD_PARAMS":
return {
...state,
scoreImageParams: action.scoreImageParams,
queryKey: setQueryKey( state.selectedPhotoUri, state.shouldUseEvidenceLocation )
};
case "SELECT_TAXON":
return {
...state,
selectedTaxon: action.selectedTaxon
};
case "SET_FETCH_STATUS":
return {
...state,
fetchStatus: action.fetchStatus
};
case "TOGGLE_LOCATION":
return {
...state,
fetchStatus: FETCH_STATUS_LOADING,
scoreImageParams: action.scoreImageParams,
shouldUseEvidenceLocation: action.shouldUseEvidenceLocation,
queryKey: setQueryKey( state.selectedPhotoUri, action.shouldUseEvidenceLocation )
};
default:
throw new Error( );
}
};
const { useRealm } = RealmContext;

const MatchContainer = ( ) => {
const hasLoadedRef = useRef( false );
const currentObservation = useStore( state => state.currentObservation );
const getCurrentObservation = useStore( state => state.getCurrentObservation );
const cameraRollUris = useStore( state => state.cameraRollUris );
const matchScreenSuggestion = useStore( state => state.matchScreenSuggestion );
const updateObservationKeys = useStore( state => state.updateObservationKeys );
const navigation = useNavigation( );
const { hasPermissions, renderPermissionsGate, requestPermissions } = useLocationPermission( );

const obsPhotos = currentObservation?.observationPhotos;

const observationPhoto = obsPhotos?.[0]?.photo?.url
|| obsPhotos?.[0]?.photo?.localFilePath;

const { taxon } = useTaxon( matchScreenSuggestion?.taxon );
const realm = useRealm( );
const exitObservationFlow = useExitObservationFlow( );

// This might happen when this component is still mounted in the background
// but the state is getting torn down b/c the user exited the obs flow. If
// we render the match screen in that scenario we'll get some errors due to
// the missing taxon.
if ( !matchScreenSuggestion ) return null;

const navToTaxonDetails = ( ) => {
navigation.push( "TaxonDetails", { id: taxon?.id } );
};
Expand Down Expand Up @@ -58,19 +116,123 @@ const MatchContainer = ( ) => {
}
};

const confidence = matchScreenSuggestion
? Math.round( matchScreenSuggestion.score * 100 )
: null;
const { isConnected } = useNetInfo( );

const evidenceHasLocation = !!currentObservation?.latitude;

const [state, dispatch] = useReducer( reducer, {
...initialState,
shouldUseEvidenceLocation: evidenceHasLocation
} );

const {
scoreImageParams,
fetchStatus,
queryKey,
shouldUseEvidenceLocation
} = state;

const shouldFetchOnlineSuggestions = ( hasPermissions !== undefined )
&& fetchStatus === FETCH_STATUS_LOADING;

const onlineSuggestionsAttempted = fetchStatus === FETCH_STATUS_ONLINE_FETCHED
|| fetchStatus === FETCH_STATUS_ONLINE_ERROR;

const onFetchError = useCallback( ( { isOnline } ) => dispatch( {
type: "SET_FETCH_STATUS",
fetchStatus: isOnline
? FETCH_STATUS_ONLINE_ERROR
: FETCH_STATUS_OFFLINE_ERROR
} ), [] );

const onFetched = useCallback( ( { isOnline } ) => dispatch( {
type: "SET_FETCH_STATUS",
fetchStatus: isOnline
? FETCH_STATUS_ONLINE_FETCHED
: FETCH_STATUS_OFFLINE_FETCHED
} ), [] );

const {
suggestions
} = useSuggestions( observationPhoto, {
shouldFetchOnlineSuggestions,
onFetchError,
onFetched,
scoreImageParams,
queryKey,
onlineSuggestionsAttempted
} );

const onTaxonChosen = useCallback( selectedTaxon => {
dispatch( {
type: "SELECT_TAXON",
selectedTaxon
} );
}, [] );

const createUploadParams = useCallback( async ( uri, showLocation ) => {
const newImageParams = await flattenUploadParams( uri );
if ( showLocation && currentObservation?.latitude ) {
newImageParams.lat = currentObservation?.latitude;
newImageParams.lng = currentObservation?.longitude;
}
return newImageParams;
}, [
currentObservation
] );

const setImageParams = useCallback( async ( ) => {
if ( isConnected === false ) {
return;
}
const newImageParams = await createUploadParams( observationPhoto, shouldUseEvidenceLocation );
dispatch( { type: "SET_UPLOAD_PARAMS", scoreImageParams: newImageParams } );
}, [
createUploadParams,
isConnected,
observationPhoto,
shouldUseEvidenceLocation
] );

useEffect( ( ) => {
const onFocus = navigation.addListener( "focus", ( ) => {
// resizeImage crashes if trying to resize an https:// photo while there is no internet
// in this situation, we can skip creating upload parameters since we're loading
// offline suggestions anyway
if ( !hasLoadedRef.current && _.isEqual( initialSuggestions, suggestions ) ) {
hasLoadedRef.current = true;
setImageParams( );
}
} );
return onFocus;
}, [navigation, setImageParams, suggestions] );

if ( fetchStatus === FETCH_STATUS_LOADING ) {
return null;
}

if ( suggestions?.otherSuggestions?.length > 0 && !suggestions?.topSuggestion ) {
const newTopSuggestion = suggestions?.otherSuggestions.pop( );
suggestions.topSuggestion = newTopSuggestion;
}

// This might happen when this component is still mounted in the background
// but the state is getting torn down b/c the user exited the obs flow. If
// we render the match screen in that scenario we'll get some errors due to
// the missing taxon.
if ( !matchScreenSuggestion ) return null;

return (
<>
<Match
observation={currentObservation}
observationPhoto={observationPhoto}
onTaxonChosen={onTaxonChosen}
handleSaveOrDiscardPress={handleSaveOrDiscardPress}
navToTaxonDetails={navToTaxonDetails}
taxon={taxon}
confidence={confidence}
handleLocationPickerPressed={handleLocationPickerPressed}
suggestions={suggestions}
/>
{renderPermissionsGate( { onPermissionGranted: openLocationPicker } )}
</>
Expand Down
Loading

0 comments on commit 94ac9f2

Please sign in to comment.