diff --git a/src/api/types.d.ts b/src/api/types.d.ts index 768a63509..ffa77e626 100644 --- a/src/api/types.d.ts +++ b/src/api/types.d.ts @@ -83,6 +83,7 @@ export interface ApiObservationSound { export interface ApiTaxon { default_photo?: ApiPhoto; + representative_photo?: ApiPhoto; iconic_taxon_name?: string; id?: number; name?: string; diff --git a/src/components/Match/AdditionalSuggestions/SuggestionsResult.js b/src/components/Match/AdditionalSuggestions/SuggestionsResult.js index e0f86e6a0..8e76aa240 100644 --- a/src/components/Match/AdditionalSuggestions/SuggestionsResult.js +++ b/src/components/Match/AdditionalSuggestions/SuggestionsResult.js @@ -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 }; diff --git a/src/components/Match/MatchContainer.js b/src/components/Match/MatchContainer.js index d1db449d5..fc0476298 100644 --- a/src/components/Match/MatchContainer.js +++ b/src/components/Match/MatchContainer.js @@ -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 => { diff --git a/src/components/Match/PhotosSection.js b/src/components/Match/PhotosSection.js index 7b731f8f1..b490bc720 100644 --- a/src/components/Match/PhotosSection.js +++ b/src/components/Match/PhotosSection.js @@ -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, - 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 => ( navToTaxonDetails( photo )} accessibilityState={{ disabled: false }} key={photo.id} className={classnames( @@ -180,12 +191,12 @@ const PhotosSection = ( { return ( {renderObservationPhoto( )} - {uniqueTaxonPhotos.length > 0 && renderTaxonPhotos( )} + {bestTaxonPhotos.length > 0 && renderTaxonPhotos( )} setMediaViewerVisible( false )} uri={observationPhoto} - photos={observationPhotos} + bestTaxonPhotos={observationPhotos} /> ); diff --git a/src/components/SharedComponents/TaxonResult.tsx b/src/components/SharedComponents/TaxonResult.tsx index 7f23de19b..0311de44c 100644 --- a/src/components/SharedComponents/TaxonResult.tsx +++ b/src/components/SharedComponents/TaxonResult.tsx @@ -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 = ( { { // 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 && ( - +