mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-06-22 22:49:01 -04:00
MOB-722 merge main
This commit is contained in:
@@ -170,6 +170,12 @@ module.exports = {
|
||||
"import/consistent-type-specifier-style": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["*.test.js", "*.test.tsx"],
|
||||
rules: {
|
||||
"react/jsx-props-no-spreading": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["**/__mocks__/**/*", "**/*mock*", "**/*.mock.*"],
|
||||
rules: {
|
||||
|
||||
8
src/api/types.d.ts
vendored
8
src/api/types.d.ts
vendored
@@ -105,12 +105,17 @@ export interface ApiUser {
|
||||
export interface ApiComment {
|
||||
body?: string;
|
||||
user?: ApiUser;
|
||||
id: number;
|
||||
hidden?: boolean;
|
||||
uuid: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ApiIdentification {
|
||||
body?: string;
|
||||
taxon?: ApiTaxon;
|
||||
user?: ApiUser;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiNotification {
|
||||
@@ -137,6 +142,9 @@ export interface ApiObservation extends ApiRecord {
|
||||
time_observed_at?: string;
|
||||
user?: ApiUser;
|
||||
uuid: string;
|
||||
comments?: ApiComment[];
|
||||
identifications?: ApiIdentification[];
|
||||
taxon?: ApiTaxon;
|
||||
}
|
||||
|
||||
export interface ApiSuggestion {
|
||||
|
||||
@@ -15,9 +15,10 @@ import { useTranslation } from "sharedHooks";
|
||||
|
||||
interface Props {
|
||||
topSuggestion?: ApiSuggestion;
|
||||
hideObservationStatus?: boolean
|
||||
}
|
||||
|
||||
const MatchHeader = ( { topSuggestion }: Props ) => {
|
||||
const MatchHeader = ( { topSuggestion, hideObservationStatus }: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const taxon = topSuggestion?.taxon;
|
||||
|
||||
@@ -87,17 +88,19 @@ const MatchHeader = ( { topSuggestion }: Props ) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Body2 className="mb-2">{generateCongratulatoryText( )}</Body2>
|
||||
{!hideObservationStatus && <Body2 className="mb-2">{generateCongratulatoryText( )}</Body2>}
|
||||
<View className="flex-row justify-between items-center">
|
||||
{showSuggestedTaxon( )}
|
||||
<View className="justify-end items-center ml-5">
|
||||
<Subheading2 className="text-inatGreen mb-2">
|
||||
{t( "X-percent", { count: confidence } )}
|
||||
</Subheading2>
|
||||
<Body4 className="text-inatGreen">
|
||||
{t( "Confidence--label" )}
|
||||
</Body4>
|
||||
</View>
|
||||
{ !hideObservationStatus && (
|
||||
<View className="justify-end items-center ml-5">
|
||||
<Subheading2 className="text-inatGreen mb-2">
|
||||
{t( "X-percent", { count: confidence } )}
|
||||
</Subheading2>
|
||||
<Body4 className="text-inatGreen">
|
||||
{t( "Confidence--label" )}
|
||||
</Body4>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -17,14 +17,16 @@ type Props = {
|
||||
representativePhoto: ApiPhoto,
|
||||
taxon?: ApiTaxon | RealmTaxon,
|
||||
obsPhotos: RealmObservationPhoto[],
|
||||
navToTaxonDetails: ( photo: ApiPhoto ) => void
|
||||
navToTaxonDetails: ( photo: ApiPhoto ) => void,
|
||||
hideTaxonPhotos?: boolean
|
||||
}
|
||||
|
||||
const PhotosSection = ( {
|
||||
representativePhoto,
|
||||
taxon,
|
||||
obsPhotos,
|
||||
navToTaxonDetails
|
||||
navToTaxonDetails,
|
||||
hideTaxonPhotos
|
||||
}: Props ) => {
|
||||
const [displayPortraitLayout, setDisplayPortraitLayout] = useState<boolean | null>( null );
|
||||
const [mediaViewerVisible, setMediaViewerVisible] = useState( false );
|
||||
@@ -88,34 +90,36 @@ const PhotosSection = ( {
|
||||
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 ( 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 ( bestTaxonPhotos.length > 1 ) {
|
||||
if ( displayPortraitLayout ) {
|
||||
containerClass = "flex-row";
|
||||
observationPhotoClass = "w-2/3 h-full pr-[3px]";
|
||||
if ( bestTaxonPhotos.length === 2 ) {
|
||||
taxonPhotosContainerClass = "flex-col w-1/3 h-full space-y-[3px]";
|
||||
taxonPhotoClass = "w-full h-1/2";
|
||||
if ( !hideTaxonPhotos ) {
|
||||
// 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 ( 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 ( bestTaxonPhotos.length > 1 ) {
|
||||
if ( displayPortraitLayout ) {
|
||||
containerClass = "flex-row";
|
||||
observationPhotoClass = "w-2/3 h-full pr-[3px]";
|
||||
if ( bestTaxonPhotos.length === 2 ) {
|
||||
taxonPhotosContainerClass = "flex-col w-1/3 h-full space-y-[3px]";
|
||||
taxonPhotoClass = "w-full h-1/2";
|
||||
} else {
|
||||
taxonPhotosContainerClass = "flex-col w-1/3 h-full space-y-[3px]";
|
||||
taxonPhotoClass = "w-full h-1/3";
|
||||
}
|
||||
} else {
|
||||
taxonPhotosContainerClass = "flex-col w-1/3 h-full space-y-[3px]";
|
||||
taxonPhotoClass = "w-full h-1/3";
|
||||
}
|
||||
} else {
|
||||
containerClass = "flex-col";
|
||||
observationPhotoClass = "w-full h-2/3 pb-[3px]";
|
||||
if ( bestTaxonPhotos.length === 2 ) {
|
||||
taxonPhotosContainerClass = "flex-row w-full h-1/3 space-x-[3px]";
|
||||
taxonPhotoClass = "w-1/2 h-full";
|
||||
} else {
|
||||
taxonPhotosContainerClass = "flex-row w-full h-1/3 space-x-[3px]";
|
||||
taxonPhotoClass = "w-1/3 h-full";
|
||||
containerClass = "flex-col";
|
||||
observationPhotoClass = "w-full h-2/3 pb-[3px]";
|
||||
if ( bestTaxonPhotos.length === 2 ) {
|
||||
taxonPhotosContainerClass = "flex-row w-full h-1/3 space-x-[3px]";
|
||||
taxonPhotoClass = "w-1/2 h-full";
|
||||
} else {
|
||||
taxonPhotosContainerClass = "flex-row w-full h-1/3 space-x-[3px]";
|
||||
taxonPhotoClass = "w-1/3 h-full";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,9 +195,9 @@ const PhotosSection = ( {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={classnames( "h-[390px]", layoutClasses.containerClass )}>
|
||||
<View className={classnames( "h-[390px] overflow-hidden", layoutClasses.containerClass )}>
|
||||
{renderObservationPhoto( )}
|
||||
{bestTaxonPhotos.length > 0 && renderTaxonPhotos( )}
|
||||
{!hideTaxonPhotos && bestTaxonPhotos.length > 0 && renderTaxonPhotos( )}
|
||||
<MediaViewerModal
|
||||
showModal={mediaViewerVisible}
|
||||
onClose={( ) => setMediaViewerVisible( false )}
|
||||
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
activityItems: Array<Object>,
|
||||
openAgreeWithIdSheet: Function,
|
||||
isConnected: boolean,
|
||||
targetItemID: number,
|
||||
targetItemID: ?number,
|
||||
// TODO change to LayoutEvent from react-native if/when switching to TS
|
||||
onLayoutTargetItem: ( event: Object ) => void
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const MapSection = ( { observation, taxon }: Props ) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="h-[200px]">
|
||||
<View testID="MapSection" className="h-[200px]">
|
||||
<Map
|
||||
mapHeight={200}
|
||||
observation={observation}
|
||||
|
||||
@@ -32,7 +32,6 @@ type Props = {
|
||||
belongsToCurrentUser: boolean,
|
||||
currentUser: RealmUser,
|
||||
isConnected: boolean,
|
||||
isSimpleMode: boolean,
|
||||
navToSuggestions: () => void,
|
||||
observation: RealmObservation & Observation & { id: number },
|
||||
openAddCommentSheet: () => void,
|
||||
@@ -52,7 +51,6 @@ const ObsDetailsDefaultMode = ( {
|
||||
belongsToCurrentUser,
|
||||
currentUser,
|
||||
isConnected,
|
||||
isSimpleMode,
|
||||
navToSuggestions,
|
||||
observation,
|
||||
openAddCommentSheet,
|
||||
@@ -79,7 +77,7 @@ const ObsDetailsDefaultMode = ( {
|
||||
const showFloatingButtons = currentUser && !isSavedObservationByCurrentUser;
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-1 bg-white" testID="ObsDetails.container">
|
||||
<ObsDetailsDefaultModeHeaderRight
|
||||
belongsToCurrentUser={belongsToCurrentUser}
|
||||
observationId={observation?.id}
|
||||
@@ -98,13 +96,11 @@ const ObsDetailsDefaultMode = ( {
|
||||
setHeightOfContentAboveCommunitySection( layout );
|
||||
}}
|
||||
>
|
||||
{!isSimpleMode && (
|
||||
<ObserverDetails
|
||||
belongsToCurrentUser={belongsToCurrentUser}
|
||||
isConnected={isConnected}
|
||||
observation={observation}
|
||||
/>
|
||||
)}
|
||||
<ObserverDetails
|
||||
belongsToCurrentUser={belongsToCurrentUser}
|
||||
isConnected={isConnected}
|
||||
observation={observation}
|
||||
/>
|
||||
<View>
|
||||
<ObsMediaDisplayContainer observation={observation} />
|
||||
</View>
|
||||
@@ -113,10 +109,7 @@ const ObsDetailsDefaultMode = ( {
|
||||
observation={observation}
|
||||
isSimpleMode
|
||||
/>
|
||||
<View className={isSimpleMode
|
||||
? "mt-[15px]"
|
||||
: "mt-5"}
|
||||
>
|
||||
<View className="mt-5">
|
||||
<MapSection observation={observation} />
|
||||
</View>
|
||||
<LocationSection
|
||||
@@ -124,29 +117,25 @@ const ObsDetailsDefaultMode = ( {
|
||||
observation={observation}
|
||||
/>
|
||||
<NotesSection description={observation.description} />
|
||||
{!isSimpleMode && <View className={cardClassBottom} />}
|
||||
<View className={cardClassBottom} />
|
||||
</View>
|
||||
{!isSimpleMode && (
|
||||
<>
|
||||
<CommunitySection
|
||||
activityItems={activityItems}
|
||||
isConnected={isConnected}
|
||||
targetItemID={targetActivityItemID}
|
||||
observation={observation}
|
||||
openAgreeWithIdSheet={openAgreeWithIdSheet}
|
||||
refetchRemoteObservation={refetchRemoteObservation}
|
||||
onLayoutTargetItem={setOffsetToActivityItem}
|
||||
/>
|
||||
{addingActivityItem && (
|
||||
<View className="flex-row items-center justify-center p-10">
|
||||
<ActivityIndicator size={50} />
|
||||
</View>
|
||||
)}
|
||||
<StatusSection observation={observation} />
|
||||
<DetailsSection observation={observation} />
|
||||
<MoreSection observation={observation} />
|
||||
</>
|
||||
<CommunitySection
|
||||
activityItems={activityItems}
|
||||
isConnected={isConnected}
|
||||
targetItemID={targetActivityItemID}
|
||||
observation={observation}
|
||||
openAgreeWithIdSheet={openAgreeWithIdSheet}
|
||||
refetchRemoteObservation={refetchRemoteObservation}
|
||||
onLayoutTargetItem={setOffsetToActivityItem}
|
||||
/>
|
||||
{addingActivityItem && (
|
||||
<View className="flex-row items-center justify-center p-10">
|
||||
<ActivityIndicator size={50} />
|
||||
</View>
|
||||
)}
|
||||
<StatusSection observation={observation} />
|
||||
<DetailsSection observation={observation} />
|
||||
<MoreSection observation={observation} />
|
||||
</ScrollView>
|
||||
{showFloatingButtons && (
|
||||
<FloatingButtons
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// @flow
|
||||
import {
|
||||
useNetInfo
|
||||
} from "@react-native-community/netinfo";
|
||||
import { useFocusEffect, useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchSubscriptions } from "api/observations";
|
||||
import IdentificationSheets from "components/ObsDetailsDefaultMode/IdentificationSheets";
|
||||
@@ -11,20 +8,16 @@ import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useState
|
||||
useReducer
|
||||
} from "react";
|
||||
import { LogBox } from "react-native";
|
||||
import Observation from "realmModels/Observation";
|
||||
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
|
||||
import {
|
||||
useAuthenticatedQuery,
|
||||
useCurrentUser,
|
||||
useLocalObservation,
|
||||
useObservationsUpdates
|
||||
} from "sharedHooks";
|
||||
import useRemoteObservation, {
|
||||
import {
|
||||
fetchRemoteObservationKey
|
||||
} from "sharedHooks/useRemoteObservation";
|
||||
import useStore from "stores/useStore";
|
||||
@@ -109,19 +102,47 @@ const reducer = ( state, action ) => {
|
||||
}
|
||||
};
|
||||
|
||||
const ObsDetailsDefaultModeContainer = ( ): Node => {
|
||||
type Props = {
|
||||
belongsToCurrentUser: boolean,
|
||||
currentUser: ?Object,
|
||||
fetchRemoteObservationError: ?Object,
|
||||
isConnected: boolean,
|
||||
isRefetching: boolean,
|
||||
localObservation: ?Object,
|
||||
markDeletedLocally: Function,
|
||||
markViewedLocally: Function,
|
||||
observation: Object,
|
||||
refetchRemoteObservation: Function,
|
||||
remoteObservation: ?Object,
|
||||
remoteObsWasDeleted: boolean,
|
||||
setRemoteObsWasDeleted: Function,
|
||||
targetActivityItemID?: ?number,
|
||||
uuid: string
|
||||
}
|
||||
|
||||
const ObsDetailsDefaultModeContainer = ( props: Props ): Node => {
|
||||
const setObservations = useStore( state => state.setObservations );
|
||||
const currentUser = useCurrentUser( );
|
||||
const { params } = useRoute();
|
||||
const {
|
||||
targetActivityItemID,
|
||||
uuid
|
||||
} = params;
|
||||
const navigation = useNavigation( );
|
||||
const realm = useRealm( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
const [state, dispatch] = useReducer( reducer, initialState );
|
||||
const [remoteObsWasDeleted, setRemoteObsWasDeleted] = useState( false );
|
||||
|
||||
const {
|
||||
observation,
|
||||
targetActivityItemID,
|
||||
uuid,
|
||||
localObservation,
|
||||
markViewedLocally,
|
||||
markDeletedLocally,
|
||||
remoteObservation,
|
||||
setRemoteObsWasDeleted,
|
||||
fetchRemoteObservationError,
|
||||
currentUser,
|
||||
belongsToCurrentUser,
|
||||
isRefetching,
|
||||
refetchRemoteObservation,
|
||||
isConnected,
|
||||
remoteObsWasDeleted
|
||||
} = props;
|
||||
|
||||
const {
|
||||
activityItems,
|
||||
@@ -133,26 +154,6 @@ const ObsDetailsDefaultModeContainer = ( ): Node => {
|
||||
} = state;
|
||||
const queryClient = useQueryClient( );
|
||||
|
||||
const {
|
||||
localObservation,
|
||||
markDeletedLocally,
|
||||
markViewedLocally
|
||||
} = useLocalObservation( uuid );
|
||||
const wasSynced = localObservation && localObservation?.wasSynced();
|
||||
|
||||
const fetchRemoteObservationEnabled = !!(
|
||||
!remoteObsWasDeleted
|
||||
&& ( !localObservation || localObservation?.wasSynced( ) )
|
||||
&& isConnected
|
||||
);
|
||||
|
||||
const {
|
||||
remoteObservation,
|
||||
refetchRemoteObservation,
|
||||
isRefetching,
|
||||
fetchRemoteObservationError
|
||||
} = useRemoteObservation( uuid, fetchRemoteObservationEnabled );
|
||||
|
||||
useMarkViewedMutation( localObservation, markViewedLocally, remoteObservation );
|
||||
|
||||
// If we tried to get a remote observation but it no longer exists, the user
|
||||
@@ -160,7 +161,8 @@ const ObsDetailsDefaultModeContainer = ( ): Node => {
|
||||
// copy of this observation
|
||||
useEffect( ( ) => {
|
||||
setRemoteObsWasDeleted( fetchRemoteObservationError?.status === 404 );
|
||||
}, [fetchRemoteObservationError?.status] );
|
||||
}, [fetchRemoteObservationError?.status, setRemoteObsWasDeleted] );
|
||||
|
||||
const confirmRemoteObsWasDeleted = useCallback( ( ) => {
|
||||
if ( localObservation ) {
|
||||
markDeletedLocally( );
|
||||
@@ -172,27 +174,10 @@ const ObsDetailsDefaultModeContainer = ( ): Node => {
|
||||
navigation
|
||||
] );
|
||||
|
||||
const observation = localObservation || Observation.mapApiToRealm( remoteObservation );
|
||||
const wasSynced = !!( localObservation && localObservation?.wasSynced() );
|
||||
|
||||
const hasPhotos = observation?.observationPhotos?.length > 0;
|
||||
|
||||
// In theory the only situation in which an observation would not have a
|
||||
// user is when a user is not signed but has made a new observation in the
|
||||
// app. Also in theory that user should not be able to get to ObsDetail for
|
||||
// those observations, just ObsEdit. But.... let's be safe.
|
||||
const belongsToCurrentUser = (
|
||||
observation?.user?.id === currentUser?.id
|
||||
|| ( !observation?.user && !observation?.id )
|
||||
);
|
||||
|
||||
const isSimpleMode = useMemo( () => (
|
||||
// Simple mode applies only when:
|
||||
// 1. It's the current user's observation (or an observation being created)
|
||||
// 2. AND the observation hasn't been synced yet
|
||||
( belongsToCurrentUser || !observation?.user )
|
||||
&& localObservation
|
||||
&& !localObservation.wasSynced()
|
||||
), [belongsToCurrentUser, localObservation, observation?.user] );
|
||||
|
||||
const { data: subscriptions, refetch: refetchSubscriptions } = useAuthenticatedQuery(
|
||||
[
|
||||
"fetchSubscriptions"
|
||||
@@ -325,7 +310,7 @@ const ObsDetailsDefaultModeContainer = ( ): Node => {
|
||||
const localComments = localObservation?.comments;
|
||||
const newComment = data[0];
|
||||
newComment.user = currentUser;
|
||||
localComments.push( newComment );
|
||||
localComments?.push( newComment );
|
||||
}, "setting local comment in ObsDetailsContainer" );
|
||||
const updatedLocalObservation = realm.objectForPrimaryKey( "Observation", uuid );
|
||||
dispatch( { type: "ADD_ACTIVITY_ITEM", observationShown: updatedLocalObservation } );
|
||||
@@ -355,7 +340,6 @@ const ObsDetailsDefaultModeContainer = ( ): Node => {
|
||||
belongsToCurrentUser={belongsToCurrentUser}
|
||||
currentUser={currentUser}
|
||||
isConnected={isConnected}
|
||||
isSimpleMode={isSimpleMode}
|
||||
navToSuggestions={navToSuggestions}
|
||||
observation={observationShown}
|
||||
openAddCommentSheet={openAddCommentSheet}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import ObsDetailsDefaultModeContainer
|
||||
from "components/ObsDetailsDefaultMode/ObsDetailsDefaultModeContainer";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import Observation from "realmModels/Observation";
|
||||
import {
|
||||
useCurrentUser,
|
||||
useLocalObservation,
|
||||
useRemoteObservation
|
||||
} from "sharedHooks";
|
||||
|
||||
import SavedMatchContainer from "./SavedMatch/SavedMatchContainer";
|
||||
|
||||
type RouteParams = {
|
||||
targetActivityItemID?: number,
|
||||
uuid: string,
|
||||
}
|
||||
|
||||
const ObsDetailsDefaultModeScreensWrapper = () => {
|
||||
const { params } = useRoute();
|
||||
const {
|
||||
targetActivityItemID,
|
||||
uuid
|
||||
} = params as RouteParams;
|
||||
const currentUser = useCurrentUser( );
|
||||
const isConnected = !!useNetInfo( ).isConnected;
|
||||
|
||||
const [remoteObsWasDeleted, setRemoteObsWasDeleted] = useState( false );
|
||||
|
||||
const {
|
||||
localObservation,
|
||||
markDeletedLocally,
|
||||
markViewedLocally
|
||||
} = useLocalObservation( uuid );
|
||||
|
||||
const fetchRemoteObservationEnabled = !!(
|
||||
!remoteObsWasDeleted
|
||||
&& ( !localObservation || localObservation?.wasSynced( ) )
|
||||
&& isConnected
|
||||
);
|
||||
|
||||
const {
|
||||
remoteObservation,
|
||||
refetchRemoteObservation,
|
||||
isRefetching,
|
||||
fetchRemoteObservationError
|
||||
} = useRemoteObservation( uuid, fetchRemoteObservationEnabled );
|
||||
|
||||
const observation = localObservation || Observation.mapApiToRealm( remoteObservation );
|
||||
|
||||
// In theory the only situation in which an observation would not have a
|
||||
// user is when a user is not signed but has made a new observation in the
|
||||
// app. Also in theory that user should not be able to get to ObsDetail for
|
||||
// those observations, just ObsEdit. But.... let's be safe.
|
||||
const belongsToCurrentUser = (
|
||||
observation?.user?.id === currentUser?.id
|
||||
|| ( !observation?.user && !observation?.id )
|
||||
);
|
||||
|
||||
const showSavedMatch = useMemo( () => (
|
||||
// Saved match screen is used when:
|
||||
// 1. It's the current user's observation (or an observation being created)
|
||||
// 2. AND the observation hasn't been synced yet
|
||||
!!( ( belongsToCurrentUser || !observation?.user )
|
||||
&& localObservation
|
||||
&& !localObservation.wasSynced() )
|
||||
), [belongsToCurrentUser, localObservation, observation?.user] );
|
||||
|
||||
if ( showSavedMatch ) {
|
||||
return (
|
||||
<SavedMatchContainer
|
||||
observation={observation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ObsDetailsDefaultModeContainer
|
||||
observation={observation}
|
||||
targetActivityItemID={targetActivityItemID}
|
||||
uuid={uuid}
|
||||
localObservation={localObservation}
|
||||
markViewedLocally={markViewedLocally}
|
||||
markDeletedLocally={markDeletedLocally}
|
||||
remoteObservation={remoteObservation}
|
||||
remoteObsWasDeleted={remoteObsWasDeleted}
|
||||
setRemoteObsWasDeleted={setRemoteObsWasDeleted}
|
||||
fetchRemoteObservationError={fetchRemoteObservationError}
|
||||
currentUser={currentUser}
|
||||
belongsToCurrentUser={belongsToCurrentUser}
|
||||
isRefetching={isRefetching}
|
||||
refetchRemoteObservation={refetchRemoteObservation}
|
||||
isConnected={isConnected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObsDetailsDefaultModeScreensWrapper;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import { matchCardClassBottom, matchCardClassTop } from "components/Match/Match";
|
||||
import MatchHeader from "components/Match/MatchHeader";
|
||||
import PhotosSection from "components/Match/PhotosSection";
|
||||
import LocationSection from "components/ObsDetailsDefaultMode/LocationSection/LocationSection";
|
||||
import MapSection from "components/ObsDetailsDefaultMode/MapSection/MapSection";
|
||||
import { Button, ScrollViewWrapper } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import type { RealmObservation } from "realmModels/types";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
import SavedMatchHeaderRight from "./SavedMatchHeaderRight";
|
||||
|
||||
interface Props {
|
||||
observation: RealmObservation,
|
||||
navToTaxonDetails: ( ) => void,
|
||||
}
|
||||
|
||||
const SavedMatch = ( {
|
||||
observation,
|
||||
navToTaxonDetails
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
|
||||
const latitude = observation?.privateLatitude || observation?.latitude;
|
||||
const { taxon } = observation;
|
||||
|
||||
return (
|
||||
<ScrollViewWrapper testID="SavedMatch.container">
|
||||
<SavedMatchHeaderRight observation={observation} />
|
||||
<View className={`${matchCardClassTop} mt-[10px]`}>
|
||||
<MatchHeader hideObservationStatus topSuggestion={observation} />
|
||||
</View>
|
||||
<PhotosSection
|
||||
taxon={taxon}
|
||||
obsPhotos={observation.observationPhotos}
|
||||
navToTaxonDetails={navToTaxonDetails}
|
||||
hideTaxonPhotos={!isConnected}
|
||||
/>
|
||||
<View className="border-[1.5px] border-white" />
|
||||
{latitude && (
|
||||
<MapSection observation={observation} taxon={taxon} />
|
||||
)}
|
||||
<LocationSection
|
||||
belongsToCurrentUser
|
||||
observation={observation}
|
||||
/>
|
||||
<View className={matchCardClassBottom} />
|
||||
{
|
||||
isConnected && (
|
||||
<Button
|
||||
className="mx-4 mb-[30px]"
|
||||
level="primary"
|
||||
text={taxon?.rank_level === 10
|
||||
? t( "LEARN-MORE-ABOUT-THIS-SPECIES" )
|
||||
: t( "LEARN-MORE-ABOUT-THIS-GROUP" )}
|
||||
onPress={navToTaxonDetails}
|
||||
accessibilityHint={t( "Navigates-to-taxon-details" )}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ScrollViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SavedMatch;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import SavedMatch from "components/ObsDetailsDefaultMode/SavedMatch/SavedMatch";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import type { RealmObservation } from "realmModels/types";
|
||||
|
||||
interface Props {
|
||||
observation: RealmObservation,
|
||||
}
|
||||
|
||||
const SavedMatchContainer = ( { observation }: Props ) => {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<Record<string, { id?: number }>>>( );
|
||||
|
||||
const navToTaxonDetails = () => {
|
||||
const navParams = { id: observation.taxon?.id };
|
||||
navigation.push( "TaxonDetails", navParams );
|
||||
};
|
||||
|
||||
return (
|
||||
<SavedMatch
|
||||
observation={observation}
|
||||
navToTaxonDetails={navToTaxonDetails}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SavedMatchContainer;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {
|
||||
INatIconButton
|
||||
} from "components/SharedComponents";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import type { RealmObservation } from "realmModels/types";
|
||||
import {
|
||||
useNavigateToObsEdit,
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
interface Props {
|
||||
observation: RealmObservation;
|
||||
}
|
||||
|
||||
const SavedMatchHeaderRight = ( {
|
||||
observation
|
||||
}: Props ) => {
|
||||
const navigation = useNavigation( );
|
||||
const { t } = useTranslation( );
|
||||
const navigateToObsEdit = useNavigateToObsEdit( );
|
||||
|
||||
const headerRight = useCallback(
|
||||
( ) => (
|
||||
<INatIconButton
|
||||
testID="SavedMatch.editButton"
|
||||
onPress={() => navigateToObsEdit( observation )}
|
||||
icon="pencil"
|
||||
color={String( colors?.darkGray )}
|
||||
accessibilityLabel={t( "Edit" )}
|
||||
size={22}
|
||||
/>
|
||||
),
|
||||
[
|
||||
observation,
|
||||
navigateToObsEdit,
|
||||
t
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
( ) => navigation.setOptions( { headerRight } ),
|
||||
[headerRight, navigation]
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SavedMatchHeaderRight;
|
||||
@@ -333,7 +333,7 @@ Highest = Höchste
|
||||
HIGHEST-RANK = HÖCHSTER RANG
|
||||
I-agree-to-the-Terms-of-Use = <0>Ich stimme den Nutzungsbedingungen und der Datenschutzrichtlinie zu und habe die Richtlinien der Gemeinschaft gelesen (</0><1>erforderlich</1><0>).</0>
|
||||
Iconic-taxon-name = Ikonischer Taxonname: { $iconicTaxon }
|
||||
ID-in-Camera = ID in Camera
|
||||
ID-in-Camera = ID in der Kamera
|
||||
ID-Suggestions = ID Vorschläge
|
||||
ID-Withdrawn = ID zurückgezogen
|
||||
IDENTIFICATION = BESTIMMUNG
|
||||
@@ -345,7 +345,7 @@ IDENTIFICATIONS-WITHOUT-NUMBER =
|
||||
}
|
||||
Identifiers = Bestimmer
|
||||
Identifiers-View = Bestimmer-Ansicht
|
||||
IDENTIFY = IDENTIFY
|
||||
IDENTIFY = BESTIMMUNG
|
||||
Identify-organisms-in-real-time-with-your-camera = Bestimmung von Lebewesen in Echtzeit mit der Kamera
|
||||
Identify-species-anywhere = Arten bestimmen, egal wo auf der Welt
|
||||
If-an-account-with-that-email-exists = Wenn ein Konto mit dieser E-Mail-Adresse existiert, wurden Anweisungen zum Zurücksetzen des Passworts an deine E-Mail-Adresse gesendet.
|
||||
|
||||
@@ -311,7 +311,7 @@
|
||||
"HIGHEST-RANK": "HÖCHSTER RANG",
|
||||
"I-agree-to-the-Terms-of-Use": "<0>Ich stimme den Nutzungsbedingungen und der Datenschutzrichtlinie zu und habe die Richtlinien der Gemeinschaft gelesen (</0><1>erforderlich</1><0>).</0>",
|
||||
"Iconic-taxon-name": "Ikonischer Taxonname: { $iconicTaxon }",
|
||||
"ID-in-Camera": "ID in Camera",
|
||||
"ID-in-Camera": "ID in der Kamera",
|
||||
"ID-Suggestions": "ID Vorschläge",
|
||||
"ID-Withdrawn": "ID zurückgezogen",
|
||||
"IDENTIFICATION": "BESTIMMUNG",
|
||||
@@ -319,7 +319,7 @@
|
||||
"IDENTIFICATIONS-WITHOUT-NUMBER": "{ $count ->\n [one] BESTIMMUNG\n *[other] BESTIMMUNGEN\n}",
|
||||
"Identifiers": "Bestimmer",
|
||||
"Identifiers-View": "Bestimmer-Ansicht",
|
||||
"IDENTIFY": "IDENTIFY",
|
||||
"IDENTIFY": "BESTIMMUNG",
|
||||
"Identify-organisms-in-real-time-with-your-camera": "Bestimmung von Lebewesen in Echtzeit mit der Kamera",
|
||||
"Identify-species-anywhere": "Arten bestimmen, egal wo auf der Welt",
|
||||
"If-an-account-with-that-email-exists": "Wenn ein Konto mit dieser E-Mail-Adresse existiert, wurden Anweisungen zum Zurücksetzen des Passworts an deine E-Mail-Adresse gesendet.",
|
||||
|
||||
@@ -148,7 +148,7 @@ Couldnt-create-comment = לא נוצרה תגובה
|
||||
Couldnt-create-identification-error = לא היתה אפשרות ליצור זיהוי { $error }
|
||||
Couldnt-create-identification-unknown-error = לא היתה אפשרות ליצור זיהוי, שגיאה לא ידועה.
|
||||
CREATE-AN-ACCOUNT = פתיחת חשבון
|
||||
Create-observation-with-no-evidence = Create observation with no evidence
|
||||
Create-observation-with-no-evidence = יצירת תצפית ללא ראיות
|
||||
DATA-QUALITY = איכות הנתונים
|
||||
DATA-QUALITY-ASSESSMENT = הערכת איכות נתונים
|
||||
Data-Quality-Assessment = הערכת איכות הנתונים
|
||||
@@ -333,7 +333,7 @@ Highest = הכי גבוה
|
||||
HIGHEST-RANK = הדרגה הגבוהה ביותר
|
||||
I-agree-to-the-Terms-of-Use = <0>אני מסכימ.ה לתנאי השימוש ולמדיניות הפרטיות, ועיינתי בהנחיות הקהילה (חובה</0><1></1><0>).</0>
|
||||
Iconic-taxon-name = שם טקסון איקוני: { $iconicTaxon }
|
||||
ID-in-Camera = ID in Camera
|
||||
ID-in-Camera = זיהוי במצלמה
|
||||
ID-Suggestions = הצעות לזיהוי
|
||||
ID-Withdrawn = זיהוי נמשך
|
||||
IDENTIFICATION = זיהוי
|
||||
@@ -345,7 +345,7 @@ IDENTIFICATIONS-WITHOUT-NUMBER =
|
||||
}
|
||||
Identifiers = מזהות.ים
|
||||
Identifiers-View = תצוגת מזהים
|
||||
IDENTIFY = IDENTIFY
|
||||
IDENTIFY = לזהות
|
||||
Identify-organisms-in-real-time-with-your-camera = זהו אורגניזמים בזמן אמת באמצעות המצלמה שלך
|
||||
Identify-species-anywhere = זהו מינים בכל מקום
|
||||
If-an-account-with-that-email-exists = אם קיים חשבון עם דוא"ל זה, שלחנו הוראות לאיפוס הסיסמה לדוא"ל שלך.
|
||||
@@ -638,7 +638,7 @@ Ranks-INFRAORDER = אינפרא-סדרה
|
||||
Ranks-Infraorder = אנפרא-סדרה
|
||||
Ranks-KINGDOM = ממלכה
|
||||
Ranks-Kingdom = ממלכה
|
||||
Ranks-ORDER = הזמנה
|
||||
Ranks-ORDER = סדרה
|
||||
Ranks-Order = סדרה
|
||||
Ranks-PARVORDER = תת-תת-סדרה
|
||||
Ranks-Parvorder = תת-תת-סדרה
|
||||
@@ -815,7 +815,7 @@ Switches-to-tab = עובר ללשונית { $tab }.
|
||||
Sync-observations = סנכרון תצפיות
|
||||
Syncing = מסנכרן...
|
||||
Take-photo = צלמ.י תמונה
|
||||
Take-photos = Take photos
|
||||
Take-photos = צלמ.י תמונות
|
||||
Taxa = טקסונים
|
||||
TAXON = טקסון
|
||||
TAXON-NAMES-DISPLAY = תצוגת שמות טקסונים
|
||||
@@ -870,7 +870,7 @@ Unreviewed-observations-only = תצפיות שלא נבדקו בלבד
|
||||
Upload-Complete = ההעלאה הושלמה
|
||||
Upload-in-progress = ההעלאה מתבצעת
|
||||
UPLOAD-NOW = העלאה עכשיו
|
||||
Upload-photos = Upload photos
|
||||
Upload-photos = העלאת תמונות
|
||||
Upload-Progress = ההעלאה הושלמה ב-{ $uploadProgress } אחוזים
|
||||
UPLOAD-TO-INATURALIST = העלאה ל-INATURALIST
|
||||
Upload-x-observations =
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
"Couldnt-create-identification-error": "לא היתה אפשרות ליצור זיהוי { $error }",
|
||||
"Couldnt-create-identification-unknown-error": "לא היתה אפשרות ליצור זיהוי, שגיאה לא ידועה.",
|
||||
"CREATE-AN-ACCOUNT": "פתיחת חשבון",
|
||||
"Create-observation-with-no-evidence": "Create observation with no evidence",
|
||||
"Create-observation-with-no-evidence": "יצירת תצפית ללא ראיות",
|
||||
"DATA-QUALITY": "איכות הנתונים",
|
||||
"DATA-QUALITY-ASSESSMENT": "הערכת איכות נתונים",
|
||||
"Data-Quality-Assessment": "הערכת איכות הנתונים",
|
||||
@@ -311,7 +311,7 @@
|
||||
"HIGHEST-RANK": "הדרגה הגבוהה ביותר",
|
||||
"I-agree-to-the-Terms-of-Use": "<0>אני מסכימ.ה לתנאי השימוש ולמדיניות הפרטיות, ועיינתי בהנחיות הקהילה (חובה</0><1></1><0>).</0>",
|
||||
"Iconic-taxon-name": "שם טקסון איקוני: { $iconicTaxon }",
|
||||
"ID-in-Camera": "ID in Camera",
|
||||
"ID-in-Camera": "זיהוי במצלמה",
|
||||
"ID-Suggestions": "הצעות לזיהוי",
|
||||
"ID-Withdrawn": "זיהוי נמשך",
|
||||
"IDENTIFICATION": "זיהוי",
|
||||
@@ -319,7 +319,7 @@
|
||||
"IDENTIFICATIONS-WITHOUT-NUMBER": "{ $count ->\n [one] זיהוי\n *[other] זיהויים\n}",
|
||||
"Identifiers": "מזהות.ים",
|
||||
"Identifiers-View": "תצוגת מזהים",
|
||||
"IDENTIFY": "IDENTIFY",
|
||||
"IDENTIFY": "לזהות",
|
||||
"Identify-organisms-in-real-time-with-your-camera": "זהו אורגניזמים בזמן אמת באמצעות המצלמה שלך",
|
||||
"Identify-species-anywhere": "זהו מינים בכל מקום",
|
||||
"If-an-account-with-that-email-exists": "אם קיים חשבון עם דוא\"ל זה, שלחנו הוראות לאיפוס הסיסמה לדוא\"ל שלך.",
|
||||
@@ -588,7 +588,7 @@
|
||||
"Ranks-Infraorder": "אנפרא-סדרה",
|
||||
"Ranks-KINGDOM": "ממלכה",
|
||||
"Ranks-Kingdom": "ממלכה",
|
||||
"Ranks-ORDER": "הזמנה",
|
||||
"Ranks-ORDER": "סדרה",
|
||||
"Ranks-Order": "סדרה",
|
||||
"Ranks-PARVORDER": "תת-תת-סדרה",
|
||||
"Ranks-Parvorder": "תת-תת-סדרה",
|
||||
@@ -761,7 +761,7 @@
|
||||
"Sync-observations": "סנכרון תצפיות",
|
||||
"Syncing": "מסנכרן...",
|
||||
"Take-photo": "צלמ.י תמונה",
|
||||
"Take-photos": "Take photos",
|
||||
"Take-photos": "צלמ.י תמונות",
|
||||
"Taxa": "טקסונים",
|
||||
"TAXON": "טקסון",
|
||||
"TAXON-NAMES-DISPLAY": "תצוגת שמות טקסונים",
|
||||
@@ -816,7 +816,7 @@
|
||||
"Upload-Complete": "ההעלאה הושלמה",
|
||||
"Upload-in-progress": "ההעלאה מתבצעת",
|
||||
"UPLOAD-NOW": "העלאה עכשיו",
|
||||
"Upload-photos": "Upload photos",
|
||||
"Upload-photos": "העלאת תמונות",
|
||||
"Upload-Progress": "ההעלאה הושלמה ב-{ $uploadProgress } אחוזים",
|
||||
"UPLOAD-TO-INATURALIST": "העלאה ל-INATURALIST",
|
||||
"Upload-x-observations": "העלאת { $count ->\n [one] תצפית אחת\n *[other] { $count } תצפיות\n}",
|
||||
|
||||
@@ -19,8 +19,8 @@ import MyObservationsContainer from "components/MyObservations/MyObservationsCon
|
||||
import Notifications from "components/Notifications/Notifications";
|
||||
import DQAContainer from "components/ObsDetails/DQAContainer";
|
||||
import ObsDetailsContainer from "components/ObsDetails/ObsDetailsContainer";
|
||||
import ObsDetailsDefaultModeContainer
|
||||
from "components/ObsDetailsDefaultMode/ObsDetailsDefaultModeContainer";
|
||||
import ObsDetailsDefaultModeScreensWrapper
|
||||
from "components/ObsDetailsDefaultMode/ObsDetailsDefaultModeScreensWrapper";
|
||||
import ProjectDetailsContainer from "components/ProjectDetails/ProjectDetailsContainer";
|
||||
import ProjectMembers from "components/ProjectDetails/ProjectMembers";
|
||||
import ProjectRequirements from "components/ProjectDetails/ProjectRequirements";
|
||||
@@ -129,8 +129,8 @@ const FadeInRootExplore = ( ) => fadeInComponent( <RootExploreContainer /> );
|
||||
const FadeInMyObservations = ( ) => fadeInComponent( <MyObservationsContainer /> );
|
||||
const FadeInUserProfile = ( ) => fadeInComponent( <UserProfile /> );
|
||||
const FadeInExploreContainer = ( ) => fadeInComponent( <ExploreContainer /> );
|
||||
const FadeInObsDetailsDefaultModeContainer = ( ) => fadeInComponent(
|
||||
<ObsDetailsDefaultModeContainer />
|
||||
const FadeInObsDetailsDefaultModeScreensWrapper = ( ) => fadeInComponent(
|
||||
<ObsDetailsDefaultModeScreensWrapper />
|
||||
);
|
||||
const FadeInObsDetailsContainer = ( ) => fadeInComponent(
|
||||
<ObsDetailsContainer />
|
||||
@@ -226,7 +226,7 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
|
||||
? (
|
||||
<Stack.Screen
|
||||
name="ObsDetails"
|
||||
component={FadeInObsDetailsDefaultModeContainer}
|
||||
component={FadeInObsDetailsDefaultModeScreensWrapper}
|
||||
options={OBS_DETAILS_OPTIONS}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import { fetchRemoteObservation } from "api/observations";
|
||||
import type { ApiObservation } from "api/types";
|
||||
import i18n from "i18next";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
@@ -10,7 +10,15 @@ const { useRealm } = RealmContext;
|
||||
|
||||
export const fetchRemoteObservationKey = "fetchRemoteObservation";
|
||||
|
||||
const filterHiddenContent = observation => {
|
||||
interface UseRemoteObservationReturn {
|
||||
remoteObservation: ApiObservation | null | undefined;
|
||||
refetchRemoteObservation: () => void;
|
||||
isRefetching: boolean;
|
||||
fetchRemoteObservationError: Error | null;
|
||||
}
|
||||
|
||||
const filterHiddenContent
|
||||
= ( observation?: ApiObservation | null ): ApiObservation | null | undefined => {
|
||||
if ( observation === undefined || observation === null ) {
|
||||
return observation;
|
||||
}
|
||||
@@ -24,7 +32,7 @@ const filterHiddenContent = observation => {
|
||||
return filteredObservation;
|
||||
};
|
||||
|
||||
const useRemoteObservation = ( uuid: string, enabled: boolean ): Object => {
|
||||
const useRemoteObservation = ( uuid: string, enabled: boolean ): UseRemoteObservationReturn => {
|
||||
const fetchRemoteObservationQueryKey = useMemo(
|
||||
( ) => ( [fetchRemoteObservationKey, uuid] ),
|
||||
[uuid]
|
||||
@@ -40,7 +48,7 @@ const useRemoteObservation = ( uuid: string, enabled: boolean ): Object => {
|
||||
refetch: refetchRemoteObservation,
|
||||
isRefetching,
|
||||
error: fetchRemoteObservationError
|
||||
} = useAuthenticatedQuery(
|
||||
} = useAuthenticatedQuery<ApiObservation | null>(
|
||||
fetchRemoteObservationQueryKey,
|
||||
optsWithAuth => fetchRemoteObservation(
|
||||
uuid,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { screen, waitFor } from "@testing-library/react-native";
|
||||
import ObsDetailsContainer from "components/ObsDetails/ObsDetailsContainer";
|
||||
import DefaultModeObsDetailsContainer
|
||||
from "components/ObsDetailsDefaultMode/ObsDetailsDefaultModeContainer";
|
||||
import ObsDetailsDefaultModeScreensWrapper
|
||||
from "components/ObsDetailsDefaultMode/ObsDetailsDefaultModeScreensWrapper";
|
||||
import inatjs from "inaturalistjs";
|
||||
import React from "react";
|
||||
import Observation from "realmModels/Observation";
|
||||
@@ -71,7 +71,7 @@ jest.mock( "@react-navigation/native", () => {
|
||||
// Run the same suite of tests for multiple ObsDetails container
|
||||
describe.each( [
|
||||
{ Container: ObsDetailsContainer, name: "ObsDetailsContainer" },
|
||||
{ Container: DefaultModeObsDetailsContainer, name: "DefaultModeObsDetailsContainer" }
|
||||
{ Container: ObsDetailsDefaultModeScreensWrapper, name: "ObsDetailsDefaultModeScreensWrapper" }
|
||||
] )( "ObsDetails", ( { Container, name } ) => {
|
||||
beforeAll( async () => {
|
||||
jest.useFakeTimers( );
|
||||
|
||||
106
tests/integration/SavedMatch.test.js
Normal file
106
tests/integration/SavedMatch.test.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import SavedMatchContainer from "components/ObsDetailsDefaultMode/SavedMatch/SavedMatchContainer";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
import faker from "tests/helpers/faker";
|
||||
import { renderAppWithComponent } from "tests/helpers/render";
|
||||
import setupUniqueRealm from "tests/helpers/uniqueRealm";
|
||||
|
||||
// UNIQUE REALM SETUP
|
||||
const mockRealmIdentifier = __filename;
|
||||
const { mockRealmModelsIndex, uniqueRealmBeforeAll, uniqueRealmAfterAll } = setupUniqueRealm(
|
||||
mockRealmIdentifier
|
||||
);
|
||||
jest.mock( "realmModels/index", ( ) => mockRealmModelsIndex );
|
||||
jest.mock( "providers/contexts", ( ) => {
|
||||
const originalModule = jest.requireActual( "providers/contexts" );
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
RealmContext: {
|
||||
...originalModule.RealmContext,
|
||||
useRealm: ( ) => global.mockRealms[mockRealmIdentifier],
|
||||
useQuery: ( ) => []
|
||||
}
|
||||
};
|
||||
} );
|
||||
beforeAll( uniqueRealmBeforeAll );
|
||||
afterAll( uniqueRealmAfterAll );
|
||||
// /UNIQUE REALM SETUP
|
||||
|
||||
const mockObservation = factory( "LocalObservation", {
|
||||
_created_at: faker.date.past( ),
|
||||
created_at: "2022-11-27T19:07:41-08:00",
|
||||
time_observed_at: "2023-12-14T21:07:41-09:30",
|
||||
observationPhotos: [
|
||||
factory( "LocalObservationPhoto", {
|
||||
photo: {
|
||||
id: faker.number.int( ),
|
||||
attribution: faker.lorem.sentence( ),
|
||||
licenseCode: "cc-by-nc",
|
||||
url: faker.image.url( )
|
||||
}
|
||||
} )
|
||||
],
|
||||
taxon: factory( "LocalTaxon", {
|
||||
name: faker.person.firstName( ),
|
||||
rank: "species",
|
||||
rank_level: 10,
|
||||
preferred_common_name: faker.person.fullName( ),
|
||||
defaultPhoto: {
|
||||
id: faker.number.int( ),
|
||||
attribution: faker.lorem.sentence( ),
|
||||
licenseCode: "cc-by-nc",
|
||||
url: faker.image.url( )
|
||||
}
|
||||
} ),
|
||||
user: factory( "LocalUser", {
|
||||
login: faker.internet.userName( ),
|
||||
iconUrl: faker.image.url( ),
|
||||
locale: "en"
|
||||
} ),
|
||||
identifications: []
|
||||
} );
|
||||
|
||||
const mockPush = jest.fn();
|
||||
jest.mock( "@react-navigation/native", () => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useNavigation: () => ( {
|
||||
push: mockPush,
|
||||
setOptions: jest.fn()
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
describe( "SavedMatch", ( ) => {
|
||||
beforeEach( async ( ) => {
|
||||
jest.clearAllMocks( );
|
||||
} );
|
||||
|
||||
it( "should not show learn more button when offline", async ( ) => {
|
||||
useNetInfo.mockImplementation( ( ) => ( { isConnected: false } ) );
|
||||
renderAppWithComponent( <SavedMatchContainer observation={mockObservation} /> );
|
||||
const learnMoreButton = screen.queryByText( t( "LEARN-MORE-ABOUT-THIS-SPECIES" ) );
|
||||
expect( learnMoreButton ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it( "should not show map section for no latitude", async ( ) => {
|
||||
renderAppWithComponent( <SavedMatchContainer observation={mockObservation} /> );
|
||||
const mapSection = screen.queryByTestId( "MapSection" );
|
||||
expect( mapSection ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
const mockObservationWithLatitude = factory( "LocalObservation", {
|
||||
latitude: faker.location.latitude( )
|
||||
} );
|
||||
|
||||
it( "should show map section when latitude is present", async ( ) => {
|
||||
renderAppWithComponent( <SavedMatchContainer observation={mockObservationWithLatitude} /> );
|
||||
const mapSection = screen.queryByTestId( "MapSection" );
|
||||
expect( mapSection ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
@@ -275,6 +275,14 @@ describe( "DisplayTaxonName", ( ) => {
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "when taxon is undefined", ( ) => {
|
||||
it( "it displays fallback text", ( ) => {
|
||||
let taxon;
|
||||
render( <DisplayTaxonName taxon={taxon} /> );
|
||||
expect( screen.getByText( /Unknown/ ) ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "when displayed as plain text within a Trans component", ( ) => {
|
||||
it( "it displays common name followed by scientific name", async ( ) => {
|
||||
render( <DisplayTaxonName taxon={subspeciesTaxon} removeStyling layout="horizontal" /> );
|
||||
|
||||
@@ -128,8 +128,32 @@ jest.mock( "sharedHooks/useObservationsUpdates", () => ( {
|
||||
} ) )
|
||||
} ) );
|
||||
|
||||
const renderObsDetails = ( ) => renderComponent(
|
||||
<ObsDetailsContainer />
|
||||
const mockRefetchRemoteObservation = jest.fn();
|
||||
const mockMarkViewedLocally = jest.fn();
|
||||
const mockMarkDeletedLocally = jest.fn();
|
||||
const mockSetRemoteObsWasDeleted = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
belongsToCurrentUser: false,
|
||||
currentUser: mockUser,
|
||||
fetchRemoteObservationError: null,
|
||||
isConnected: true,
|
||||
isRefetching: false,
|
||||
isSimpleMode: false,
|
||||
localObservation: null,
|
||||
markDeletedLocally: mockMarkDeletedLocally,
|
||||
markViewedLocally: mockMarkViewedLocally,
|
||||
observation: mockObservation,
|
||||
refetchRemoteObservation: mockRefetchRemoteObservation,
|
||||
remoteObservation: mockObservation,
|
||||
remoteObsWasDeleted: false,
|
||||
setRemoteObsWasDeleted: mockSetRemoteObsWasDeleted,
|
||||
targetActivityItemID: null,
|
||||
uuid: mockObservation.uuid
|
||||
};
|
||||
|
||||
const renderObsDetails = ( props = {} ) => renderComponent(
|
||||
<ObsDetailsContainer {...defaultProps} {...props} />
|
||||
);
|
||||
|
||||
describe( "ObsDetails", () => {
|
||||
@@ -156,7 +180,11 @@ describe( "ObsDetails", () => {
|
||||
} );
|
||||
|
||||
it( "should render fallback image icon instead of photos", async () => {
|
||||
renderObsDetails( );
|
||||
renderObsDetails( {
|
||||
observation: mockNoEvidenceObservation,
|
||||
remoteObservation: mockNoEvidenceObservation,
|
||||
uuid: mockNoEvidenceObservation.uuid
|
||||
} );
|
||||
|
||||
const labelText = t( "Observation-has-no-photos-and-no-sounds" );
|
||||
const fallbackImage = await screen.findByLabelText( labelText );
|
||||
@@ -194,7 +222,13 @@ describe( "ObsDetails", () => {
|
||||
} );
|
||||
|
||||
jest.spyOn( useCurrentUser, "default" ).mockImplementation( () => mockUser );
|
||||
renderObsDetails( );
|
||||
renderObsDetails( {
|
||||
observation: otherUserObservation,
|
||||
remoteObservation: otherUserObservation,
|
||||
uuid: otherUserObservation.uuid,
|
||||
belongsToCurrentUser: false,
|
||||
localObservation: null
|
||||
} );
|
||||
const agreeButton = screen.getByTestId(
|
||||
`ActivityItem.AgreeIdButton.${firstIdentification.taxon.id}`
|
||||
);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import ObsDetailsDefaultModeScreensWrapper
|
||||
from "components/ObsDetailsDefaultMode/ObsDetailsDefaultModeScreensWrapper";
|
||||
import React from "react";
|
||||
import * as useLocalObservation from "sharedHooks/useLocalObservation";
|
||||
import factory from "tests/factory";
|
||||
import faker from "tests/helpers/faker";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
const mockCurrentUser = factory( "LocalUser", {
|
||||
id: 123,
|
||||
login: faker.internet.userName( )
|
||||
} );
|
||||
|
||||
const mockLocalObservation = factory( "LocalObservation", {
|
||||
user: factory( "LocalUser", {
|
||||
id: 123
|
||||
} ),
|
||||
uuid: "test-123"
|
||||
} );
|
||||
|
||||
jest.mock( "@react-native-community/netinfo", () => ( {
|
||||
useNetInfo: () => ( { isConnected: true } ),
|
||||
configure: jest.fn( )
|
||||
} ) );
|
||||
|
||||
jest.mock( "sharedHooks/useCurrentUser", () => ( {
|
||||
__esModule: true,
|
||||
default: () => mockCurrentUser
|
||||
} ) );
|
||||
jest.mock( "sharedHooks/useLocalObservation", () => ( {
|
||||
__esModule: true,
|
||||
default: jest.fn( () => ( {
|
||||
localObservation: null
|
||||
} ) )
|
||||
} ) );
|
||||
jest.mock( "sharedHooks/useRemoteObservation", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( _uuid, _fetchRemoteEnabled ) => ( {
|
||||
remoteObservation: null,
|
||||
refetchRemoteObservation: jest.fn( ),
|
||||
isRefetching: false,
|
||||
fetchRemoteObservationError: null
|
||||
} )
|
||||
} ) );
|
||||
|
||||
describe( "ObsDetailsDefaultModeScreensWrapper", ( ) => {
|
||||
describe( "when showSavedMatch is true", ( ) => {
|
||||
it(
|
||||
"renders SavedMatchContainer when observation belongs to current user and is not synced",
|
||||
( ) => {
|
||||
const unsyncedLocalObservation = {
|
||||
...mockLocalObservation,
|
||||
wasSynced: jest.fn( () => false )
|
||||
};
|
||||
|
||||
jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => ( {
|
||||
localObservation: unsyncedLocalObservation,
|
||||
markDeletedLocally: jest.fn( ),
|
||||
markViewedLocally: jest.fn( )
|
||||
} ) );
|
||||
|
||||
renderComponent( <ObsDetailsDefaultModeScreensWrapper /> );
|
||||
|
||||
expect( screen.getByTestId( "SavedMatch.container" ) ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "ObsDetails.container" ) ).toBeFalsy( );
|
||||
}
|
||||
);
|
||||
|
||||
it( "renders SavedMatchContainer when observation has no user and is not synced", ( ) => {
|
||||
const unsyncedLocalObservationNoUser = factory( "LocalObservation", {
|
||||
user: null,
|
||||
uuid: "test-123",
|
||||
wasSynced: jest.fn( () => false )
|
||||
} );
|
||||
|
||||
jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => ( {
|
||||
localObservation: unsyncedLocalObservationNoUser,
|
||||
markDeletedLocally: jest.fn( ),
|
||||
markViewedLocally: jest.fn( )
|
||||
} ) );
|
||||
|
||||
renderComponent( <ObsDetailsDefaultModeScreensWrapper /> );
|
||||
|
||||
expect( screen.getByTestId( "SavedMatch.container" ) ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "ObsDetails.container" ) ).toBeFalsy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "when showSavedMatch is false", ( ) => {
|
||||
it( "renders ObsDetailsDefaultModeContainer when observation is synced", ( ) => {
|
||||
const syncedLocalObservation = {
|
||||
...mockLocalObservation,
|
||||
wasSynced: jest.fn( () => true )
|
||||
};
|
||||
|
||||
jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => ( {
|
||||
localObservation: syncedLocalObservation,
|
||||
markDeletedLocally: jest.fn( ),
|
||||
markViewedLocally: jest.fn( )
|
||||
} ) );
|
||||
|
||||
renderComponent( <ObsDetailsDefaultModeScreensWrapper /> );
|
||||
|
||||
expect( screen.getByTestId( "ObsDetails.container" ) ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "SavedMatch.container" ) ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it(
|
||||
"renders ObsDetailsDefaultModeContainer when observation does not belong to current user",
|
||||
( ) => {
|
||||
const otherUserObservation = factory( "LocalObservation", {
|
||||
user: factory( "LocalUser", {
|
||||
id: 456
|
||||
} ),
|
||||
uuid: "test-123",
|
||||
wasSynced: jest.fn( () => false )
|
||||
} );
|
||||
|
||||
jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => ( {
|
||||
localObservation: otherUserObservation,
|
||||
markDeletedLocally: jest.fn( ),
|
||||
markViewedLocally: jest.fn( )
|
||||
} ) );
|
||||
|
||||
renderComponent( <ObsDetailsDefaultModeScreensWrapper /> );
|
||||
|
||||
expect( screen.getByTestId( "ObsDetails.container" ) ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "SavedMatch.container" ) ).toBeFalsy( );
|
||||
}
|
||||
);
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user