Skip to content

Commit

Permalink
Use fields to limit response from search API; save taxon results in r…
Browse files Browse the repository at this point in the history
…ealm (#1905)
  • Loading branch information
albullington authored Aug 1, 2024
1 parent 9b0029c commit 7f2ed32
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 37 deletions.
4 changes: 2 additions & 2 deletions src/components/Explore/SearchScreens/ExploreTaxonSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const ExploreTaxonSearch = ( {
const [taxonQuery, setTaxonQuery] = useState( "" );

const iconicTaxa = useIconicTaxa( { reload: false } );
const { taxonList, isLoading } = useTaxonSearch( taxonQuery );
const { taxaSearchResults, isLoading } = useTaxonSearch( taxonQuery );

const onTaxonSelected = useCallback( async newTaxon => {
updateTaxon( newTaxon );
Expand Down Expand Up @@ -75,7 +75,7 @@ const ExploreTaxonSearch = ( {

let data = iconicTaxa;
if ( taxonQuery.length > 0 ) {
data = taxonList;
data = taxaSearchResults;
}

return (
Expand Down
7 changes: 4 additions & 3 deletions src/components/ObsEdit/BottomButtons.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const { useRealm } = RealmContext;

type Props = {
passesEvidenceTest: boolean,
passesIdentificationTest: boolean,
observations: Array<Object>,
currentObservation: Object,
currentObservationIndex: number,
Expand All @@ -38,7 +37,6 @@ const logger = log.extend( "ObsEditBottomButtons" );

const BottomButtons = ( {
passesEvidenceTest,
passesIdentificationTest,
currentObservation,
currentObservationIndex,
observations,
Expand All @@ -62,7 +60,10 @@ const BottomButtons = ( {
const [buttonPressed, setButtonPressed] = useState( null );
const [loading, setLoading] = useState( false );

const passesTests = passesEvidenceTest && passesIdentificationTest;
const hasIdentification = currentObservation?.taxon
&& currentObservation?.taxon.rank_level !== 100;

const passesTests = passesEvidenceTest && hasIdentification;

const writeExifToCameraRollPhotos = useCallback( async exif => {
if ( !cameraRollUris || cameraRollUris.length === 0 || !currentObservation ) {
Expand Down
11 changes: 1 addition & 10 deletions src/components/ObsEdit/IdentificationSection.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,13 @@ type Props = {
currentObservation: Object,
resetScreen: boolean,
setResetScreen: Function,
passesIdentificationTest: boolean,
setPassesIdentificationTest: Function,
updateObservationKeys: Function
}

const IdentificationSection = ( {
currentObservation,
resetScreen,
setResetScreen,
passesIdentificationTest,
setPassesIdentificationTest,
updateObservationKeys
}: Props ): Node => {
const { t } = useTranslation( );
Expand Down Expand Up @@ -72,12 +68,6 @@ const IdentificationSection = ( {
navigation
] );

useEffect( ( ) => {
if ( hasIdentification && !passesIdentificationTest ) {
setPassesIdentificationTest( true );
}
}, [hasIdentification, setPassesIdentificationTest, passesIdentificationTest] );

useEffect( ( ) => {
// by adding resetScreen as a key in renderIconicTaxonChooser,
// we force React to rerender and reset the horizontal scroll position
Expand Down Expand Up @@ -152,6 +142,7 @@ const IdentificationSection = ( {
<TaxonResult
accessibilityLabel={t( "Edits-this-observations-taxon" )}
asListItem={false}
fetchRemote={false}
handleTaxonOrEditPress={navToSuggestions}
handleRemovePress={removeTaxon}
hideInfoButton
Expand Down
4 changes: 0 additions & 4 deletions src/components/ObsEdit/ObsEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const ObsEdit = ( ): Node => {
const setCurrentObservationIndex = useStore( state => state.setCurrentObservationIndex );
const updateObservationKeys = useStore( state => state.updateObservationKeys );
const [passesEvidenceTest, setPassesEvidenceTest] = useState( false );
const [passesIdentificationTest, setPassesIdentificationTest] = useState( false );
const [resetScreen, setResetScreen] = useState( false );
const isFocused = useIsFocused( );
const currentUser = useCurrentUser( );
Expand Down Expand Up @@ -113,9 +112,7 @@ const ObsEdit = ( ): Node => {
/>
<IdentificationSection
currentObservation={currentObservation}
passesIdentificationTest={passesIdentificationTest}
resetScreen={resetScreen}
setPassesIdentificationTest={setPassesIdentificationTest}
setResetScreen={setResetScreen}
updateObservationKeys={updateObservationKeys}
/>
Expand All @@ -133,7 +130,6 @@ const ObsEdit = ( ): Node => {
currentObservationIndex={currentObservationIndex}
observations={observations}
passesEvidenceTest={passesEvidenceTest}
passesIdentificationTest={passesIdentificationTest}
setCurrentObservationIndex={setCurrentObservationIndex}
/>
{renderLocationPermissionGate( {
Expand Down
4 changes: 1 addition & 3 deletions src/components/Suggestions/Suggestion.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import {

type Props = {
accessibilityLabel: string,
fetchRemote: boolean,
onTaxonChosen: Function,
suggestion: Object,
isTopSuggestion?: boolean
};

const Suggestion = ( {
accessibilityLabel,
fetchRemote,
suggestion,
onTaxonChosen,
isTopSuggestion = false
Expand All @@ -32,7 +30,7 @@ const Suggestion = ( {
? convertOfflineScoreToConfidence( suggestion?.score )
: convertOnlineScoreToConfidence( suggestion?.combined_score )}
confidencePosition="text"
fetchRemote={fetchRemote}
fetchRemote={false}
first
isTopSuggestion={isTopSuggestion}
handleCheckmarkPress={onTaxonChosen}
Expand Down
4 changes: 1 addition & 3 deletions src/components/Suggestions/Suggestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,10 @@ const Suggestions = ( {
const renderSuggestion = useCallback( ( { item: suggestion } ) => (
<Suggestion
accessibilityLabel={t( "Choose-taxon" )}
fetchRemote={!usingOfflineSuggestions}
suggestion={suggestion}
onTaxonChosen={onTaxonChosen}
/>
), [onTaxonChosen, t, usingOfflineSuggestions] );
), [onTaxonChosen, t] );

const renderEmptyList = useCallback( ( ) => (
<SuggestionsEmpty
Expand Down Expand Up @@ -148,7 +147,6 @@ const Suggestions = ( {
<View className="bg-inatGreen/[.13]">
<Suggestion
accessibilityLabel={t( "Choose-taxon" )}
fetchRemote={!usingOfflineSuggestions}
suggestion={item}
isTopSuggestion
onTaxonChosen={onTaxonChosen}
Expand Down
1 change: 0 additions & 1 deletion src/components/Suggestions/SuggestionsLoading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const SuggestionsLoading = ( {
<View className="pt-6" />
<Suggestion
accessibilityLabel={t( "Choose-taxon" )}
fetchRemote={false}
suggestion={aiCameraSuggestion}
onTaxonChosen={onTaxonChosen}
/>
Expand Down
5 changes: 2 additions & 3 deletions src/components/Suggestions/TaxonSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DROP_SHADOW = getShadowForColor( colors.darkGray, {
const TaxonSearch = ( ): Node => {
const [taxonQuery, setTaxonQuery] = useState( "" );
const [selectedTaxon, setSelectedTaxon] = useState( null );
const { taxonList } = useTaxonSearch( taxonQuery );
const { taxaSearchResults } = useTaxonSearch( taxonQuery );
const { t } = useTranslation( );

useNavigateWithTaxonSelected(
Expand Down Expand Up @@ -65,10 +65,9 @@ const TaxonSearch = ( ): Node => {
autoFocus={taxonQuery === ""}
/>
</View>

<FlatList
keyboardShouldPersistTaps="always"
data={taxonList}
data={taxaSearchResults}
renderItem={renderTaxonResult}
keyExtractor={item => item.id}
ListFooterComponent={renderFooter}
Expand Down
32 changes: 31 additions & 1 deletion src/components/Suggestions/hooks/useOnlineSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import {
} from "@react-native-community/netinfo";
import { useQueryClient } from "@tanstack/react-query";
import scoreImage from "api/computerVision";
import { RealmContext } from "providers/contexts";
import {
useCallback, useEffect, useState
} from "react";
import Taxon from "realmModels/Taxon";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import {
useAuthenticatedQuery
} from "sharedHooks";

const SCORE_IMAGE_TIMEOUT = 5_000;

const { useRealm } = RealmContext;

type OnlineSuggestionsResponse = {
dataUpdatedAt: Date,
onlineSuggestions: Object,
Expand All @@ -25,6 +30,7 @@ type OnlineSuggestionsResponse = {
const useOnlineSuggestions = (
options: Object
): OnlineSuggestionsResponse => {
const realm = useRealm( );
const {
dispatch,
flattenedUploadParams,
Expand Down Expand Up @@ -84,13 +90,37 @@ const useOnlineSuggestions = (
}
}, [isConnected, dispatch] );

const saveTaxaToRealm = useCallback( ( ) => {
// we're already getting all this taxon information anytime we make this API
// call, so we might as well store it in realm immediately instead of waiting
// for useTaxon to fetch individual taxon results
const mappedTaxa = onlineSuggestions?.results?.map(
suggestion => Taxon.mapApiToRealm( suggestion.taxon, realm )
);
if ( onlineSuggestions?.common_ancestor ) {
const mappedCommonAncestor = Taxon
.mapApiToRealm( onlineSuggestions?.common_ancestor.taxon, realm );
mappedTaxa.push( mappedCommonAncestor );
}
safeRealmWrite( realm, ( ) => {
mappedTaxa.forEach( remoteTaxon => {
realm.create(
"Taxon",
{ ...remoteTaxon, _synced_at: new Date( ) },
"modified"
);
} );
}, "saving remote taxon from onlineSuggestions" );
}, [realm, onlineSuggestions] );

useEffect( ( ) => {
if ( onlineSuggestions !== undefined ) {
saveTaxaToRealm( );
dispatch( { type: "SET_FETCH_STATUS", fetchStatus: "online-fetched" } );
} else if ( error ) {
dispatch( { type: "SET_FETCH_STATUS", fetchStatus: "online-error" } );
}
}, [dispatch, onlineSuggestions, error] );
}, [dispatch, onlineSuggestions, error, saveTaxaToRealm] );

const queryObject = {
dataUpdatedAt,
Expand Down
4 changes: 3 additions & 1 deletion src/sharedHooks/useTaxon.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const useTaxon = ( taxon: Object, fetchRemote = true ): Object => {
// Sync if the local copy hasn't been synced in a week
localTaxon._synced_at && ( Date.now( ) - localTaxon._synced_at > ONE_WEEK_MS )
)
// Sync if missing a common name or default photo from being saved in Realm while offline
|| ( !localTaxon.preferred_common_name || !localTaxon.default_photo?.url )
);
const enabled = !!( canFetchTaxon && fetchRemote && localTaxonNeedsSync );

Expand All @@ -40,7 +42,7 @@ const useTaxon = ( taxon: Object, fetchRemote = true ): Object => {
isLoading
} = useAuthenticatedQuery(
["fetchTaxon", taxonId],
optsWithAuth => fetchTaxon( taxonId, { fields: Taxon.TAXON_FIELDS }, optsWithAuth ),
optsWithAuth => fetchTaxon( taxonId, { fields: Taxon.SCORE_IMAGE_FIELDS }, optsWithAuth ),
{
enabled
}
Expand Down
42 changes: 39 additions & 3 deletions src/sharedHooks/useTaxonSearch.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
// @flow

import fetchSearchResults from "api/search";
import { RealmContext } from "providers/contexts";
import {
useCallback, useEffect
} from "react";
import Taxon from "realmModels/Taxon";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import { useAuthenticatedQuery } from "sharedHooks";

const { useRealm } = RealmContext;

const useTaxonSearch = ( taxonQuery: string ): Object => {
const { data: taxonList, isLoading } = useAuthenticatedQuery(
const realm = useRealm( );
const { data: taxaSearchResults, isLoading } = useAuthenticatedQuery(
["fetchTaxonSuggestions", taxonQuery],
optsWithAuth => fetchSearchResults(
{
q: taxonQuery,
sources: "taxa"
sources: "taxa",
fields: {
taxon: Taxon.SCORE_IMAGE_FIELDS
}
},
optsWithAuth
),
Expand All @@ -18,7 +30,31 @@ const useTaxonSearch = ( taxonQuery: string ): Object => {
}
);

return { taxonList, isLoading };
const saveTaxaToRealm = useCallback( ( ) => {
// we're already getting all this taxon information anytime we make this API
// call, so we might as well store it in realm. we can remove this if
// we're worried about the cache getting too large
const mappedTaxa = taxaSearchResults?.map(
result => Taxon.mapApiToRealm( result, realm )
);
safeRealmWrite( realm, ( ) => {
mappedTaxa.forEach( remoteTaxon => {
realm.create(
"Taxon",
{ ...remoteTaxon, _synced_at: new Date( ) },
"modified"
);
} );
}, "saving remote taxon from useTaxonSearch" );
}, [realm, taxaSearchResults] );

useEffect( ( ) => {
if ( taxaSearchResults?.length > 0 ) {
saveTaxaToRealm( );
}
}, [saveTaxaToRealm, taxaSearchResults] );

return { taxaSearchResults, isLoading };
};

export default useTaxonSearch;
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const renderIdentificationSection = ( obs, index = 0, resetState = false ) => re
<IdentificationSection
currentObservation={obs[index]}
observations={obs}
passesIdentificationTest
resetState={resetState}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/components/Suggestions/TaxonSearch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const mockTaxaList = [

jest.mock( "sharedHooks/useTaxonSearch", () => ( {
__esModule: true,
default: ( ) => ( { taxonList: mockTaxaList, isLoading: false } )
default: ( ) => ( { taxaSearchResults: mockTaxaList, isLoading: false } )
} ) );

jest.mock( "sharedHooks/useTaxon", () => ( {
Expand Down Expand Up @@ -70,7 +70,7 @@ describe( "TaxonSearch", ( ) => {

it( "show taxon search results", async ( ) => {
inatjs.taxa.search.mockResolvedValue(
makeResponse( { taxonList: mockTaxaList, isLoading: false } )
makeResponse( { taxaSearchResults: mockTaxaList, isLoading: false } )
);
renderComponent( <TaxonSearch /> );
const input = screen.getByTestId( "SearchTaxon" );
Expand Down

0 comments on commit 7f2ed32

Please sign in to comment.