mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-04 21:53:59 -04:00
Photo similarity representative photo result sent to taxon details (#2699)
* A key like this does not exist because we don't save rep Photo to realm * Show rep photo on suggestions * Add comment * Update types.d.ts * Move code around * Send rep photo ID to taxon result * Send rep photo to taxon result * Add white background to box on taxon details Closes MOB-491 * Comments * Only send ID around and move photo to top * Send first photo id also from match screen * If rep photo is of same taxon, only send ID, else send entire photo to taxon details * Refactor taxon image * Updated realm types * If rep photo is of other taxon send entire object * Create copy of realm taxon Avoids an error "object has been invalidated and deleted" which I didn't have tome to track down.
This commit is contained in:
1
src/api/types.d.ts
vendored
1
src/api/types.d.ts
vendored
@@ -83,6 +83,7 @@ export interface ApiObservationSound {
|
||||
|
||||
export interface ApiTaxon {
|
||||
default_photo?: ApiPhoto;
|
||||
representative_photo?: ApiPhoto;
|
||||
iconic_taxon_name?: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
|
||||
@@ -52,8 +52,7 @@ const SuggestionsResult = ( {
|
||||
// and is currently not added to the taxon realm. So, if it is available directly from the
|
||||
// suggestion, i.e. taxonProp, use it. Otherwise, use the default photo from the taxon.
|
||||
const taxonImage = {
|
||||
uri: taxonProp?.representativePhoto?.url
|
||||
|| taxonProp?.representative_photo?.url
|
||||
uri: taxonProp?.representative_photo?.url
|
||||
|| usableTaxon?.default_photo?.url
|
||||
|| usableTaxon?.defaultPhoto?.url
|
||||
};
|
||||
|
||||
@@ -254,8 +254,14 @@ const MatchContainer = ( ) => {
|
||||
const otherSuggestions = orderedSuggestions
|
||||
.filter( suggestion => suggestion.taxon.id !== taxonId );
|
||||
|
||||
const navToTaxonDetails = ( ) => {
|
||||
navigation.push( "TaxonDetails", { id: taxonId } );
|
||||
const navToTaxonDetails = photo => {
|
||||
const params = { id: taxonId };
|
||||
if ( !photo?.isRepresentativeButOtherTaxon ) {
|
||||
params.firstPhotoID = photo.id;
|
||||
} else {
|
||||
params.representativePhoto = photo;
|
||||
}
|
||||
navigation.push( "TaxonDetails", params );
|
||||
};
|
||||
|
||||
const handleSaveOrDiscardPress = async action => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import {
|
||||
Image, Pressable, View
|
||||
} from "components/styledComponents";
|
||||
import _, { compact, uniqBy } from "lodash";
|
||||
import _, { compact } from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Photo from "realmModels/Photo";
|
||||
import getImageDimensions from "sharedHelpers/getImageDimensions";
|
||||
@@ -16,7 +16,7 @@ type Props = {
|
||||
representativePhoto: Object,
|
||||
taxon: Object,
|
||||
obsPhotos: Array<Object>,
|
||||
navToTaxonDetails: ( ) => void
|
||||
navToTaxonDetails: ( photo: Object ) => void
|
||||
}
|
||||
|
||||
const PhotosSection = ( {
|
||||
@@ -35,22 +35,33 @@ const PhotosSection = ( {
|
||||
|
||||
const taxonPhotos = compact(
|
||||
localTaxonPhotos
|
||||
? localTaxonPhotos.map( taxonPhoto => taxonPhoto.photo )
|
||||
? localTaxonPhotos.map( taxonPhoto => ( { ...taxonPhoto.photo } ) )
|
||||
: [taxon?.defaultPhoto]
|
||||
);
|
||||
// don't show the iconic taxon photo which is a mashup of 9 photos
|
||||
const taxonPhotosNoIconic = localTaxon?.isIconic
|
||||
? taxonPhotos.slice( 1, 4 )
|
||||
: taxonPhotos.slice( 0, 3 );
|
||||
|
||||
// Add the representative photo at the start of the list of taxon photos.
|
||||
const taxonPhotosWithRepPhoto = compact( [representativePhoto, ...taxonPhotosNoIconic] );
|
||||
// The representative photo might be already included in taxonPhotosNoIconic
|
||||
const uniqueTaxonPhotos = uniqBy( taxonPhotosWithRepPhoto, "id" );
|
||||
if ( uniqueTaxonPhotos.length > 3 ) {
|
||||
uniqueTaxonPhotos.pop( );
|
||||
// don't show the iconic taxon photo which is a mashup of 9 bestTaxonPhotos
|
||||
if ( localTaxon?.isIconic ) {
|
||||
taxonPhotos.splice( 0 );
|
||||
}
|
||||
|
||||
// If the representative photo is already included in taxonPhotos, don't add it but move
|
||||
// it to the start of the list.
|
||||
let firstPhoto;
|
||||
if ( representativePhoto && taxonPhotos.some( photo => photo.id === representativePhoto.id ) ) {
|
||||
const repPhotoIndex = taxonPhotos.findIndex( photo => photo.id === representativePhoto.id );
|
||||
// The first photo to show is the realm version of the representative photo
|
||||
firstPhoto = taxonPhotos.splice( repPhotoIndex, 1 )[0];
|
||||
} else if ( representativePhoto ) {
|
||||
// This is possible because a representative photo can be from a different taxon, e.g. children
|
||||
// of common ancestors. In this case, the representative photo is not included in taxonPhotos.
|
||||
firstPhoto = { ...representativePhoto, isRepresentativeButOtherTaxon: true };
|
||||
}
|
||||
// Add the representative photo at the start of the list of taxon bestTaxonPhotos.
|
||||
const taxonPhotosWithRepPhoto = compact( [
|
||||
firstPhoto,
|
||||
...taxonPhotos
|
||||
] );
|
||||
const bestTaxonPhotos = taxonPhotosWithRepPhoto.slice( 0, 3 );
|
||||
|
||||
const observationPhotos = compact(
|
||||
obsPhotos
|
||||
? obsPhotos.map( obsPhoto => obsPhoto.photo )
|
||||
@@ -70,24 +81,24 @@ const PhotosSection = ( {
|
||||
}, [observationPhoto] );
|
||||
|
||||
const getLayoutClasses = ( ) => {
|
||||
// Basic layout: no taxon photos + obs photo a square
|
||||
// Basic layout: no taxon bestTaxonPhotos + obs photo a square
|
||||
let containerClass = "flex-row";
|
||||
let observationPhotoClass = "w-full h-full";
|
||||
let taxonPhotosContainerClass;
|
||||
let taxonPhotoClass;
|
||||
// If there is only one taxon photo: obs photo a square,
|
||||
// taxon photo a square in the lower right corner of the obs photo
|
||||
if ( uniqueTaxonPhotos.length === 1 ) {
|
||||
if ( bestTaxonPhotos.length === 1 ) {
|
||||
containerClass = "flex-row relative";
|
||||
observationPhotoClass = "w-full h-full";
|
||||
taxonPhotosContainerClass = "absolute bottom-0 right-0 w-1/3 h-1/3";
|
||||
taxonPhotoClass = "w-full h-full border-l-[3px] border-t-[3px] border-white";
|
||||
}
|
||||
if ( uniqueTaxonPhotos.length > 1 ) {
|
||||
if ( bestTaxonPhotos.length > 1 ) {
|
||||
if ( displayPortraitLayout ) {
|
||||
containerClass = "flex-row";
|
||||
observationPhotoClass = "w-2/3 h-full pr-[3px]";
|
||||
if ( uniqueTaxonPhotos.length === 2 ) {
|
||||
if ( bestTaxonPhotos.length === 2 ) {
|
||||
taxonPhotosContainerClass = "flex-col w-1/3 h-full space-y-[3px]";
|
||||
taxonPhotoClass = "w-full h-1/2";
|
||||
} else {
|
||||
@@ -97,7 +108,7 @@ const PhotosSection = ( {
|
||||
} else {
|
||||
containerClass = "flex-col";
|
||||
observationPhotoClass = "w-full h-2/3 pb-[3px]";
|
||||
if ( uniqueTaxonPhotos.length === 2 ) {
|
||||
if ( bestTaxonPhotos.length === 2 ) {
|
||||
taxonPhotosContainerClass = "flex-row w-full h-1/3 space-x-[3px]";
|
||||
taxonPhotoClass = "w-1/2 h-full";
|
||||
} else {
|
||||
@@ -147,10 +158,10 @@ const PhotosSection = ( {
|
||||
layoutClasses?.taxonPhotosContainerClass
|
||||
)}
|
||||
>
|
||||
{uniqueTaxonPhotos.map( photo => (
|
||||
{bestTaxonPhotos.map( photo => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={navToTaxonDetails}
|
||||
onPress={() => navToTaxonDetails( photo )}
|
||||
accessibilityState={{ disabled: false }}
|
||||
key={photo.id}
|
||||
className={classnames(
|
||||
@@ -180,12 +191,12 @@ const PhotosSection = ( {
|
||||
return (
|
||||
<View className={classnames( "h-[390px]", layoutClasses.containerClass )}>
|
||||
{renderObservationPhoto( )}
|
||||
{uniqueTaxonPhotos.length > 0 && renderTaxonPhotos( )}
|
||||
{bestTaxonPhotos.length > 0 && renderTaxonPhotos( )}
|
||||
<MediaViewerModal
|
||||
showModal={mediaViewerVisible}
|
||||
onClose={( ) => setMediaViewerVisible( false )}
|
||||
uri={observationPhoto}
|
||||
photos={observationPhotos}
|
||||
bestTaxonPhotos={observationPhotos}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import type { GestureResponderEvent } from "react-native";
|
||||
import type { RealmTaxon } from "realmModels/types";
|
||||
import type { RealmTaxon, RealmTaxonPhoto } from "realmModels/types";
|
||||
import { accessibleTaxonName } from "sharedHelpers/taxon";
|
||||
import { useCurrentUser, useTaxon, useTranslation } from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
@@ -89,19 +89,46 @@ const TaxonResult = ( {
|
||||
? localTaxon
|
||||
: taxonProp;
|
||||
const accessibleName = accessibleTaxonName( usableTaxon, currentUser, t );
|
||||
// A representative photo is dependant on the actual image that was scored by computer vision
|
||||
// and is currently not added to the taxon realm. So, if it is available directly from the
|
||||
// suggestion, i.e. taxonProp, use it. Otherwise, use the default photo from the taxon.
|
||||
const representativePhoto = ( taxonProp as ApiTaxon )?.representative_photo;
|
||||
// I have seen the RealmTaxon that is accessed here get invalidated and deleted
|
||||
// while this screen is still in stack and therefore the app erroring out.
|
||||
// Have not had time to investigate further, but this is a workaround for now.
|
||||
const taxonImagePointer = representativePhoto
|
||||
|| ( usableTaxon as ApiTaxon )?.default_photo
|
||||
|| ( usableTaxon as RealmTaxon )?.defaultPhoto;
|
||||
const taxonImage = React.useMemo( () => ( { ...taxonImagePointer } ), [taxonImagePointer] );
|
||||
|
||||
const taxonImageSource = { uri: taxonImage?.url };
|
||||
|
||||
const isRepresentativeButOtherTaxon = representativePhoto
|
||||
&& !localTaxon?.taxonPhotos?.some(
|
||||
( tp: RealmTaxonPhoto ) => tp.photo.id === representativePhoto.id
|
||||
);
|
||||
|
||||
const navToTaxonDetails = React.useCallback( ( ) => {
|
||||
navigation.push( "TaxonDetails", {
|
||||
const params = {
|
||||
id: usableTaxon?.id,
|
||||
hideNavButtons,
|
||||
lastScreen,
|
||||
vision
|
||||
} );
|
||||
};
|
||||
if ( !isRepresentativeButOtherTaxon ) {
|
||||
params.firstPhotoID = taxonImage?.id;
|
||||
} else {
|
||||
params.representativePhoto = taxonImage;
|
||||
}
|
||||
navigation.push( "TaxonDetails", params );
|
||||
}, [
|
||||
hideNavButtons,
|
||||
lastScreen,
|
||||
navigation,
|
||||
usableTaxon?.id,
|
||||
vision
|
||||
vision,
|
||||
taxonImage,
|
||||
isRepresentativeButOtherTaxon
|
||||
] );
|
||||
const TaxonResultMain = React.useCallback( ( props: TaxonResultMainProps ) => (
|
||||
unpressable
|
||||
@@ -131,11 +158,6 @@ const TaxonResult = ( {
|
||||
// useTaxon could return null, and it's at least remotely possible taxonProp is null
|
||||
if ( !usableTaxon ) return null;
|
||||
|
||||
const taxonImage = {
|
||||
uri: ( usableTaxon as ApiTaxon )?.default_photo?.url
|
||||
|| ( usableTaxon as RealmTaxon )?.defaultPhoto?.url
|
||||
};
|
||||
|
||||
const renderCheckmark = () => {
|
||||
if ( checkmarkFocused ) {
|
||||
return (
|
||||
@@ -191,7 +213,7 @@ const TaxonResult = ( {
|
||||
<View className="w-[62px] h-[62px] justify-center relative">
|
||||
<ObsImagePreview
|
||||
// TODO fix when ObsImagePreview typed
|
||||
source={taxonImage}
|
||||
source={taxonImageSource}
|
||||
testID={`${testID}.photo`}
|
||||
iconicTaxonName={usableTaxon?.iconic_taxon_name}
|
||||
className="rounded-xl"
|
||||
|
||||
@@ -88,7 +88,9 @@ const TaxonDetails = ( ): Node => {
|
||||
// Hooks
|
||||
const navigation = useNavigation( );
|
||||
const { params } = useRoute( );
|
||||
const { id, hideNavButtons } = params;
|
||||
const {
|
||||
id, hideNavButtons, firstPhotoID, representativePhoto
|
||||
} = params;
|
||||
const { t } = useTranslation( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
const { remoteUser } = useUserMe( );
|
||||
@@ -193,6 +195,18 @@ const TaxonDetails = ( ): Node => {
|
||||
? taxon.taxonPhotos.map( taxonPhoto => taxonPhoto.photo )
|
||||
: [taxon?.defaultPhoto]
|
||||
);
|
||||
// Move the first photo to top if it was passed in as a prop
|
||||
if ( firstPhotoID ) {
|
||||
const firstPhotoIndex = photos.findIndex( photo => photo.id === firstPhotoID );
|
||||
if ( firstPhotoIndex > 0 ) {
|
||||
const firstPhoto = photos.splice( firstPhotoIndex, 1 );
|
||||
photos.unshift( firstPhoto[0] );
|
||||
}
|
||||
}
|
||||
// Add the representative photo to the top of the list
|
||||
if ( representativePhoto ) {
|
||||
photos.unshift( representativePhoto );
|
||||
}
|
||||
|
||||
const updateTaxon = useCallback( ( ) => {
|
||||
updateObservationKeys( {
|
||||
@@ -450,7 +464,7 @@ const TaxonDetails = ( ): Node => {
|
||||
/>
|
||||
)}
|
||||
{showSelectButton && (
|
||||
<ButtonBar containerClass="items-center z-50">
|
||||
<ButtonBar containerClass="items-center z-50 bg-white">
|
||||
<Button
|
||||
testID="TaxonDetails.SelectButton"
|
||||
className="max-w-[500px] w-full"
|
||||
|
||||
11
src/realmModels/types.d.ts
vendored
11
src/realmModels/types.d.ts
vendored
@@ -42,9 +42,17 @@ export interface RealmObservationSound extends RealmObject {
|
||||
wasSynced: ( ) => boolean;
|
||||
}
|
||||
|
||||
export interface RealmTaxonPhoto extends RealmObject {
|
||||
_created_at?: Date;
|
||||
_synced_at?: Date;
|
||||
_updated_at?: Date;
|
||||
id: number;
|
||||
photo: RealmPhoto;
|
||||
}
|
||||
|
||||
export interface RealmTaxon extends RealmObject {
|
||||
id: number;
|
||||
defaultPhoto?: RealmPhoto,
|
||||
defaultPhoto?: RealmPhoto;
|
||||
name?: string;
|
||||
preferredCommonName?: string;
|
||||
rank?: string;
|
||||
@@ -53,6 +61,7 @@ export interface RealmTaxon extends RealmObject {
|
||||
iconic_taxon_name?: string;
|
||||
ancestor_ids?: number[];
|
||||
_synced_at?: Date;
|
||||
taxonPhotos?: RealmTaxonPhoto[];
|
||||
}
|
||||
|
||||
export interface RealmObservationPojo {
|
||||
|
||||
Reference in New Issue
Block a user