mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Make MyObservationsSimple the standard UI across default/advanced mode (#2788)
* Update TypeScript * Fix some tests with new default MyObservationsSimple * Show upload toolbar on MyObs advanced * Update tests for simple mode * Fix deletions popping back up on MyObs * Fix e2e test, which also means fixing our deletion process * Fix useSyncObservations test * Requested changes to better fit latest designs * Add tests to check for hidden upload banner
This commit is contained in:
committed by
GitHub
parent
146886fbeb
commit
70ffa9112a
@@ -8,6 +8,10 @@ import deleteObservation from "./sharedFlows/deleteObservation";
|
||||
import signIn from "./sharedFlows/signIn";
|
||||
import uploadObservation from "./sharedFlows/uploadObservation";
|
||||
|
||||
function delay( ms ) {
|
||||
return new Promise( resolve => { setTimeout( resolve, ms ); } );
|
||||
}
|
||||
|
||||
describe( "Signed in user", () => {
|
||||
beforeAll( async ( ) => iNatE2eBeforeAll( device ) );
|
||||
beforeEach( async ( ) => iNatE2eBeforeEach( device ) );
|
||||
@@ -65,7 +69,7 @@ describe( "Signed in user", () => {
|
||||
const username = await signIn( );
|
||||
|
||||
// Switch to list view as well
|
||||
const listToggle = element( by.id( "MyObservationsToolbar.toggleListView" ) );
|
||||
const listToggle = element( by.id( "SegmentedButton.list" ) );
|
||||
await waitFor( listToggle ).toBeVisible( ).withTimeout( 10000 );
|
||||
await listToggle.tap();
|
||||
|
||||
@@ -116,6 +120,11 @@ describe( "Signed in user", () => {
|
||||
// point, and we can confirm deletion by testing for the absence of the
|
||||
// list item for the observation we deleted.
|
||||
await waitFor( element( by.text( /Upload 1 observation/ ) ) ).toBeVisible( ).withTimeout( 20_000 );
|
||||
|
||||
// the timing of syncing deletions seems to be different in the actual app versus these
|
||||
// e2e tests, so deleting an observation here still shows the observation
|
||||
// in the list unless this delay( ) is added
|
||||
await delay( 3000 );
|
||||
await expect( obsListItem ).toBeNotVisible( );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -24,7 +24,6 @@ const ITEMS = sortBy( [
|
||||
{ title: "TaxonResult", component: "TaxonResultDemo" },
|
||||
{ title: "ObsGridItem", component: "ObsGridItemDemo" },
|
||||
{ title: "TaxonGridItem", component: "TaxonGridItemDemo" },
|
||||
{ title: "Toolbar", component: "ToolbarDemo" },
|
||||
{ title: "PivotCards", component: "PivotCardsDemo" }
|
||||
], item => item.title );
|
||||
ITEMS.push( { title: "Everything Else", component: "Misc" } );
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import Toolbar from "components/MyObservations/Toolbar";
|
||||
import {
|
||||
Heading1
|
||||
} from "components/SharedComponents";
|
||||
import {
|
||||
ScrollView
|
||||
} from "components/styledComponents";
|
||||
import React from "react";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
const ToolbarDemo = ( ) => (
|
||||
<ScrollView>
|
||||
<Heading1>Basic</Heading1>
|
||||
<Toolbar
|
||||
error=""
|
||||
handleSyncButtonPress={noop}
|
||||
layout="grid"
|
||||
navToExplore={noop}
|
||||
progress={0}
|
||||
statusText=""
|
||||
stopAllUploads={noop}
|
||||
syncIconColor={colors.darkGray}
|
||||
toggleLayout={noop}
|
||||
/>
|
||||
<Heading1>Needs Upload</Heading1>
|
||||
<Toolbar
|
||||
error=""
|
||||
handleSyncButtonPress={noop}
|
||||
layout="grid"
|
||||
navToExplore={noop}
|
||||
progress={0}
|
||||
showsExclamation
|
||||
showsExploreIcon
|
||||
statusText="Upload 3 observations"
|
||||
stopAllUploads={noop}
|
||||
syncIconColor={colors.inatGreen}
|
||||
toggleLayout={noop}
|
||||
/>
|
||||
<Heading1>Uploading</Heading1>
|
||||
<Toolbar
|
||||
error=""
|
||||
handleSyncButtonPress={noop}
|
||||
layout="grid"
|
||||
navToExplore={noop}
|
||||
progress={0.2}
|
||||
rotating
|
||||
showsCancelUploadButton
|
||||
showsExploreIcon
|
||||
statusText="doing some uploading"
|
||||
stopAllUploads={noop}
|
||||
syncIconColor={colors.inatGreen}
|
||||
toggleLayout={noop}
|
||||
/>
|
||||
<Heading1>Uploading w/error</Heading1>
|
||||
<Toolbar
|
||||
error="something went terribly wrong"
|
||||
handleSyncButtonPress={noop}
|
||||
layout="grid"
|
||||
navToExplore={noop}
|
||||
progress={0.5}
|
||||
rotating
|
||||
showsCancelUploadButton
|
||||
showsExploreIcon
|
||||
statusText="doing some uploading"
|
||||
stopAllUploads={noop}
|
||||
syncIconColor={colors.inatGreen}
|
||||
toggleLayout={noop}
|
||||
/>
|
||||
<Heading1>Uploading w/ long status</Heading1>
|
||||
<Toolbar
|
||||
error=""
|
||||
handleSyncButtonPress={noop}
|
||||
layout="grid"
|
||||
navToExplore={noop}
|
||||
progress={0.2}
|
||||
rotating
|
||||
showsCancelUploadButton
|
||||
showsExploreIcon
|
||||
statusText="doing some uploading that will be really great i promise"
|
||||
stopAllUploads={noop}
|
||||
syncIconColor={colors.inatGreen}
|
||||
toggleLayout={noop}
|
||||
/>
|
||||
<Heading1>Finished w/error</Heading1>
|
||||
<Toolbar
|
||||
error="something went terribly wrong"
|
||||
handleSyncButtonPress={noop}
|
||||
layout="grid"
|
||||
navToExplore={noop}
|
||||
progress={1}
|
||||
showsExploreIcon
|
||||
showsExclamation
|
||||
showsCheckmark
|
||||
statusText="3 observations uploaded"
|
||||
stopAllUploads={noop}
|
||||
syncIconColor={colors.warningRed}
|
||||
toggleLayout={noop}
|
||||
/>
|
||||
<Heading1>Error w/o status</Heading1>
|
||||
<Toolbar
|
||||
error="something went terribly, terribly, terribly wrong"
|
||||
handleSyncButtonPress={noop}
|
||||
layout="grid"
|
||||
navToExplore={noop}
|
||||
progress={1}
|
||||
showsExploreIcon
|
||||
showsExclamation
|
||||
showsCheckmark
|
||||
statusText=""
|
||||
stopAllUploads={noop}
|
||||
syncIconColor={colors.warningRed}
|
||||
toggleLayout={noop}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
export default ToolbarDemo;
|
||||
@@ -12,7 +12,6 @@ import ObsListItemDemo from "./UiLibrary/ObsListItemDemo";
|
||||
import PivotCardsDemo from "./UiLibrary/PivotCardsDemo";
|
||||
import TaxonGridItemDemo from "./UiLibrary/TaxonGridItemDemo";
|
||||
import TaxonResultDemo from "./UiLibrary/TaxonResultDemo";
|
||||
import ToolbarDemo from "./UiLibrary/ToolbarDemo";
|
||||
import Typography from "./UiLibrary/Typography";
|
||||
|
||||
const LIBRARY = {
|
||||
@@ -26,7 +25,6 @@ const LIBRARY = {
|
||||
PivotCardsDemo,
|
||||
TaxonGridItemDemo,
|
||||
TaxonResultDemo,
|
||||
ToolbarDemo,
|
||||
Typography
|
||||
};
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import MyObservationsHeader from "components/MyObservations/MyObservationsHeader";
|
||||
import ObservationsFlashList from "components/ObservationsFlashList/ObservationsFlashList";
|
||||
import OnboardingCarouselModal from "components/Onboarding/OnboardingCarouselModal";
|
||||
import {
|
||||
ScrollableWithStickyHeader,
|
||||
ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useOnboardingShown } from "sharedHelpers/installData.ts";
|
||||
|
||||
import Announcements from "./Announcements";
|
||||
import LoginSheet from "./LoginSheet";
|
||||
|
||||
type Props = {
|
||||
currentUser: Object,
|
||||
handleIndividualUploadPress: Function,
|
||||
handleSyncButtonPress: Function,
|
||||
handlePullToRefresh: Function,
|
||||
isConnected: boolean,
|
||||
isFetchingNextPage: boolean,
|
||||
layout: "list" | "grid",
|
||||
listRef?: Object,
|
||||
numUnuploadedObservations: number,
|
||||
observations: Array<Object>,
|
||||
onEndReached: Function,
|
||||
onListLayout?: Function,
|
||||
onScroll?: Function,
|
||||
setShowLoginSheet: Function,
|
||||
showLoginSheet: boolean,
|
||||
showNoResults: boolean,
|
||||
toggleLayout: Function
|
||||
};
|
||||
|
||||
const MyObservations = ( {
|
||||
currentUser,
|
||||
handleIndividualUploadPress,
|
||||
handleSyncButtonPress,
|
||||
handlePullToRefresh,
|
||||
isConnected,
|
||||
isFetchingNextPage,
|
||||
layout,
|
||||
listRef,
|
||||
numUnuploadedObservations,
|
||||
observations,
|
||||
onEndReached,
|
||||
onListLayout,
|
||||
onScroll,
|
||||
setShowLoginSheet,
|
||||
showLoginSheet,
|
||||
showNoResults,
|
||||
toggleLayout
|
||||
}: Props ): Node => {
|
||||
const [onboardingShown, setOnboardingShown] = useOnboardingShown( );
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<OnboardingCarouselModal
|
||||
showModal={!onboardingShown}
|
||||
closeModal={() => setOnboardingShown( true )}
|
||||
/>
|
||||
<ScrollableWithStickyHeader
|
||||
onScroll={onScroll}
|
||||
renderHeader={setStickyAt => (
|
||||
<MyObservationsHeader
|
||||
currentUser={currentUser}
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
hideToolbar={observations.length === 0}
|
||||
layout={layout}
|
||||
logInButtonNeutral={observations.length === 0}
|
||||
numUnuploadedObservations={numUnuploadedObservations}
|
||||
setHeightAboveToolbar={setStickyAt}
|
||||
toggleLayout={toggleLayout}
|
||||
/>
|
||||
)}
|
||||
renderScrollable={animatedScrollEvent => (
|
||||
<ObservationsFlashList
|
||||
dataCanBeFetched={!!currentUser}
|
||||
data={observations.filter( o => o.isValid() )}
|
||||
handlePullToRefresh={handlePullToRefresh}
|
||||
handleIndividualUploadPress={handleIndividualUploadPress}
|
||||
onScroll={animatedScrollEvent}
|
||||
hideLoadingWheel={!isFetchingNextPage || !currentUser}
|
||||
isConnected={isConnected}
|
||||
obsListKey="MyObservations"
|
||||
layout={layout}
|
||||
onEndReached={onEndReached}
|
||||
onLayout={onListLayout}
|
||||
ref={listRef}
|
||||
showObservationsEmptyScreen
|
||||
showNoResults={showNoResults}
|
||||
testID="MyObservationsAnimatedList"
|
||||
renderHeader={(
|
||||
<Announcements isConnected={isConnected} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ViewWrapper>
|
||||
{showLoginSheet && <LoginSheet setShowLoginSheet={setShowLoginSheet} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyObservations;
|
||||
@@ -1,21 +1,23 @@
|
||||
// @flow
|
||||
import {
|
||||
useNetInfo
|
||||
} from "@react-native-community/netinfo";
|
||||
import { useFocusEffect, useRoute } from "@react-navigation/native";
|
||||
import { fetchSpeciesCounts } from "api/observations";
|
||||
import { RealmContext } from "providers/contexts.ts";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
useEffect, useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { Alert } from "react-native";
|
||||
import Observation from "realmModels/Observation";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
import type { RealmObservation, RealmTaxon } from "realmModels/types";
|
||||
import { log } from "sharedHelpers/logger";
|
||||
import {
|
||||
useCurrentUser,
|
||||
useInfiniteObservationsScroll,
|
||||
useInfiniteScroll,
|
||||
useLayoutPrefs,
|
||||
useLocalObservations,
|
||||
useNavigateToObsEdit,
|
||||
@@ -28,20 +30,37 @@ import { isDebugMode } from "sharedHooks/useDebugMode";
|
||||
import {
|
||||
UPLOAD_PENDING
|
||||
} from "stores/createUploadObservationsSlice.ts";
|
||||
import useStore from "stores/useStore";
|
||||
import useStore, { zustandStorage } from "stores/useStore";
|
||||
|
||||
import FullScreenActivityIndicator from "./FullScreenActivityIndicator";
|
||||
import useSyncObservations from "./hooks/useSyncObservations";
|
||||
import useUploadObservations from "./hooks/useUploadObservations";
|
||||
import MyObservations from "./MyObservations";
|
||||
import MyObservationsEmptySimple from "./MyObservationsEmptySimple";
|
||||
import MyObservationsSimpleContainer from "./MyObservationsSimpleContainer";
|
||||
import MyObservationsSimple, {
|
||||
OBSERVATIONS_TAB,
|
||||
TAXA_TAB
|
||||
} from "./MyObservationsSimple";
|
||||
|
||||
const logger = log.extend( "MyObservationsContainer" );
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
const MyObservationsContainer = ( ): Node => {
|
||||
interface SpeciesCount {
|
||||
count: number,
|
||||
taxon: RealmTaxon
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
justFinishedSignup?: boolean;
|
||||
}
|
||||
|
||||
interface SyncOptions {
|
||||
unuploadedObsMissingBasicsIDs?: string[];
|
||||
skipUploads?: boolean;
|
||||
skipSomeUploads?: string[];
|
||||
}
|
||||
|
||||
const MyObservationsContainer = ( ): React.FC => {
|
||||
const { loadTime } = usePerformance( {
|
||||
screenName: "MyObservations"
|
||||
} );
|
||||
@@ -51,12 +70,12 @@ const MyObservationsContainer = ( ): Node => {
|
||||
const { isDefaultMode, loggedInWhileInDefaultMode } = useLayoutPrefs();
|
||||
const { t } = useTranslation( );
|
||||
const realm = useRealm( );
|
||||
const listRef = useRef( );
|
||||
const listRef = useRef( null );
|
||||
const navigateToObsEdit = useNavigateToObsEdit( );
|
||||
|
||||
// Get navigation params
|
||||
const { params } = useRoute( );
|
||||
const { justFinishedSignup } = params || {};
|
||||
const { justFinishedSignup } = ( params as RouteParams ) || {};
|
||||
|
||||
const setStartUploadObservations = useStore( state => state.setStartUploadObservations );
|
||||
const uploadQueue = useStore( state => state.uploadQueue );
|
||||
@@ -79,7 +98,7 @@ const MyObservationsContainer = ( ): Node => {
|
||||
const { isConnected } = useNetInfo( );
|
||||
const currentUser = useCurrentUser( );
|
||||
const currentUserId = currentUser?.id;
|
||||
const canUpload = currentUser && isConnected;
|
||||
const canUpload = !!currentUser && !!isConnected;
|
||||
|
||||
const { startUploadObservations } = useUploadObservations( canUpload );
|
||||
const { syncManually } = useSyncObservations(
|
||||
@@ -125,24 +144,28 @@ const MyObservationsContainer = ( ): Node => {
|
||||
return currentUser;
|
||||
}, [currentUser] );
|
||||
|
||||
const handleSyncButtonPress = useCallback( options => {
|
||||
const handleSyncButtonPress = useCallback( ( options?: SyncOptions ) => {
|
||||
const { unuploadedObsMissingBasicsIDs } = options || { };
|
||||
if ( !confirmLoggedIn( ) ) { return; }
|
||||
if ( !confirmInternetConnection( ) ) { return; }
|
||||
|
||||
startManualSync( );
|
||||
syncManually( { skipSomeUploads: unuploadedObsMissingBasicsIDs } );
|
||||
const syncOptions = isDefaultMode
|
||||
? { skipSomeUploads: unuploadedObsMissingBasicsIDs }
|
||||
: { };
|
||||
syncManually( syncOptions );
|
||||
}, [
|
||||
startManualSync,
|
||||
syncManually,
|
||||
confirmInternetConnection,
|
||||
confirmLoggedIn
|
||||
confirmLoggedIn,
|
||||
isDefaultMode
|
||||
] );
|
||||
|
||||
const handleIndividualUploadPress = useCallback( uuid => {
|
||||
const uploadExists = uploadQueue.includes( uuid );
|
||||
if ( uploadExists ) return;
|
||||
const observation = realm.objectForPrimaryKey( "Observation", uuid );
|
||||
const observation = realm.objectForPrimaryKey<RealmObservation>( "Observation", uuid );
|
||||
if ( isDefaultMode && observation.missingBasics( ) ) {
|
||||
navigateToObsEdit( observation );
|
||||
return;
|
||||
@@ -206,11 +229,9 @@ const MyObservationsContainer = ( ): Node => {
|
||||
myObsOffsetToRestore
|
||||
] );
|
||||
|
||||
if ( !layout ) { return null; }
|
||||
|
||||
// API call fetching obs has completed but results are not yet stored in realm
|
||||
// for display here
|
||||
const showLoading = totalResultsRemote > 0 && observations.length === 0;
|
||||
const showLoading = ( totalResultsRemote || 0 ) > 0 && observations.length === 0;
|
||||
|
||||
// show empty screen instead of loading wheel...
|
||||
const showNoResults = !showLoading && (
|
||||
@@ -225,7 +246,93 @@ const MyObservationsContainer = ( ): Node => {
|
||||
|
||||
// Keep track of the scroll offset so we can restore it when we mount
|
||||
// this component again after returning from ObsEdit
|
||||
const onScroll = scrollEvent => setMyObsOffset( scrollEvent.nativeEvent.contentOffset.y );
|
||||
const onScroll = ( scrollEvent: {
|
||||
nativeEvent: {
|
||||
contentOffset: {
|
||||
y: number
|
||||
}
|
||||
}
|
||||
} ) => setMyObsOffset( scrollEvent.nativeEvent.contentOffset.y );
|
||||
|
||||
const numOfUserObservations = zustandStorage.getItem( "numOfUserObservations" );
|
||||
const numOfUserSpecies = zustandStorage.getItem( "numOfUserSpecies" );
|
||||
const [activeTab, setActiveTab] = useState( OBSERVATIONS_TAB );
|
||||
|
||||
let numTotalTaxaLocal: number | undefined;
|
||||
const localObservedSpeciesCount: Array<SpeciesCount> = [];
|
||||
if ( !currentUser ) {
|
||||
// Calculate obs and leaf taxa counts from local observations
|
||||
const distinctTaxonObs = realm.objects<RealmObservation>( "Observation" )
|
||||
.filtered( "taxon != null DISTINCT(taxon.id)" );
|
||||
const taxonIds: number[] = distinctTaxonObs
|
||||
.map( o => o.taxon?.id || 0 )
|
||||
.filter( Boolean );
|
||||
const ancestorIds = distinctTaxonObs.map( o => {
|
||||
// We're filtering b/c for taxa above species level, the taxon's own
|
||||
// ID is included in ancestor ids for some reason (this is a bug...
|
||||
// somewhere)
|
||||
const taxonAncestorIds = (
|
||||
o.taxon?.ancestor_ids || []
|
||||
).filter( id => Number( id ) !== Number( o.taxon?.id ) );
|
||||
return taxonAncestorIds;
|
||||
} ).flat( );
|
||||
const leafTaxonIds = taxonIds.filter( taxonId => !ancestorIds.includes( taxonId ) );
|
||||
numTotalTaxaLocal = leafTaxonIds.length;
|
||||
|
||||
// Get leaf taxa if we're viewing the species tab
|
||||
if ( activeTab === TAXA_TAB ) {
|
||||
const localObs = realm.objects<RealmObservation>( "Observation" )
|
||||
.filtered( "taxon.id IN $0", leafTaxonIds );
|
||||
leafTaxonIds.forEach( id => {
|
||||
const obs = localObs.filter( o => o.taxon.id === id );
|
||||
localObservedSpeciesCount.push( { count: obs.length, taxon: obs[0].taxon } );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
data: remoteObservedTaxaCounts,
|
||||
isFetchingNextPage: isFetchingTaxa,
|
||||
fetchNextPage: fetchMoreTaxa,
|
||||
totalResults: numTotalTaxaRemote,
|
||||
refetch: refetchTaxa
|
||||
} = useInfiniteScroll(
|
||||
"MyObsSimple-fetchSpeciesCounts",
|
||||
fetchSpeciesCounts,
|
||||
{
|
||||
user_id: currentUser?.id,
|
||||
fields: {
|
||||
taxon: Taxon.LIMITED_TAXON_FIELDS
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: !!currentUser
|
||||
}
|
||||
);
|
||||
|
||||
const numTotalTaxa = typeof ( numTotalTaxaRemote ) === "number"
|
||||
? numTotalTaxaRemote
|
||||
: numTotalTaxaLocal;
|
||||
|
||||
const numTotalObservations = totalResultsRemote || totalResultsLocal;
|
||||
|
||||
useEffect( ( ) => {
|
||||
// persist this number in zustand so a user can see their latest observations count
|
||||
// even if they're offline
|
||||
if ( numTotalObservations > numOfUserObservations ) {
|
||||
zustandStorage.setItem( "numOfUserObservations", numTotalObservations );
|
||||
}
|
||||
}, [numTotalObservations, numOfUserObservations] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
// persist this number in zustand so a user can see their latest species count
|
||||
// even if they're offline
|
||||
if ( numTotalTaxa > numOfUserSpecies ) {
|
||||
zustandStorage.setItem( "numOfUserSpecies", numTotalTaxa );
|
||||
}
|
||||
}, [numTotalTaxa, numOfUserSpecies] );
|
||||
|
||||
if ( !layout ) { return null; }
|
||||
|
||||
if ( observations.length === 0 ) {
|
||||
return showNoResults
|
||||
@@ -240,51 +347,38 @@ const MyObservationsContainer = ( ): Node => {
|
||||
);
|
||||
}
|
||||
|
||||
if ( isDefaultMode ) {
|
||||
return (
|
||||
<MyObservationsSimpleContainer
|
||||
currentUser={currentUser}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
isConnected={isConnected}
|
||||
handleIndividualUploadPress={handleIndividualUploadPress}
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
handlePullToRefresh={handlePullToRefresh}
|
||||
layout={layout}
|
||||
listRef={listRef}
|
||||
numUnuploadedObservations={numUnuploadedObservations}
|
||||
numTotalObservations={totalResultsRemote || totalResultsLocal}
|
||||
observations={observations}
|
||||
onEndReached={fetchNextPage}
|
||||
onListLayout={restoreScrollOffset}
|
||||
onScroll={onScroll}
|
||||
setShowLoginSheet={setShowLoginSheet}
|
||||
showLoginSheet={showLoginSheet}
|
||||
showNoResults={showNoResults}
|
||||
toggleLayout={toggleLayout}
|
||||
justFinishedSignup={justFinishedSignup}
|
||||
loggedInWhileInDefaultMode={loggedInWhileInDefaultMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const taxa = currentUser
|
||||
? remoteObservedTaxaCounts
|
||||
: localObservedSpeciesCount;
|
||||
|
||||
return (
|
||||
<MyObservations
|
||||
<MyObservationsSimple
|
||||
activeTab={activeTab}
|
||||
currentUser={currentUser}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
isConnected={isConnected}
|
||||
fetchMoreTaxa={fetchMoreTaxa}
|
||||
handleIndividualUploadPress={handleIndividualUploadPress}
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
handlePullToRefresh={handlePullToRefresh}
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
isConnected={isConnected}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
isFetchingTaxa={isFetchingTaxa}
|
||||
justFinishedSignup={justFinishedSignup}
|
||||
layout={layout}
|
||||
listRef={listRef}
|
||||
loggedInWhileInDefaultMode={loggedInWhileInDefaultMode}
|
||||
numTotalObservations={numOfUserObservations}
|
||||
numTotalTaxa={numOfUserSpecies}
|
||||
numUnuploadedObservations={numUnuploadedObservations}
|
||||
observations={observations}
|
||||
onEndReached={fetchNextPage}
|
||||
onListLayout={restoreScrollOffset}
|
||||
onScroll={onScroll}
|
||||
refetchTaxa={refetchTaxa}
|
||||
setActiveTab={setActiveTab}
|
||||
setShowLoginSheet={setShowLoginSheet}
|
||||
showLoginSheet={showLoginSheet}
|
||||
showNoResults={showNoResults}
|
||||
taxa={taxa}
|
||||
toggleLayout={toggleLayout}
|
||||
/>
|
||||
);
|
||||
@@ -1,146 +0,0 @@
|
||||
// @flow
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import ToolbarContainer from "components/MyObservations/ToolbarContainer";
|
||||
import {
|
||||
Button,
|
||||
Heading1,
|
||||
INatIconButton,
|
||||
Subheading1
|
||||
} from "components/SharedComponents";
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
import Onboarding from "./Onboarding";
|
||||
|
||||
type Props = {
|
||||
currentUser: ?Object,
|
||||
handleSyncButtonPress: Function,
|
||||
hideToolbar: boolean,
|
||||
layout: string,
|
||||
logInButtonNeutral: boolean,
|
||||
numUnuploadedObservations: number,
|
||||
setHeightAboveToolbar?: Function,
|
||||
toggleLayout: Function
|
||||
}
|
||||
|
||||
const MyObservationsHeader = ( {
|
||||
currentUser,
|
||||
handleSyncButtonPress,
|
||||
hideToolbar,
|
||||
layout,
|
||||
logInButtonNeutral,
|
||||
numUnuploadedObservations,
|
||||
setHeightAboveToolbar,
|
||||
toggleLayout
|
||||
}: Props ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const signedInContent = ( ) => (
|
||||
<Pressable
|
||||
className="flex flex-row items-center"
|
||||
accessibilityRole="link"
|
||||
accessibilityHint={t( "Navigates-to-user-profile" )}
|
||||
onPress={() => {
|
||||
navigation.push( "UserProfile", { userId: currentUser?.id } );
|
||||
}}
|
||||
>
|
||||
<Trans
|
||||
className="my-5"
|
||||
i18nKey="Welcome-user"
|
||||
parent={View}
|
||||
values={{ userHandle: currentUser?.login }}
|
||||
components={[
|
||||
<Subheading1 />,
|
||||
<Heading1 />
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const signedOutContent = ( ) => (
|
||||
<View className="my-5 flex-col items-center">
|
||||
<View className="flex-row items-center justify-center mb-5">
|
||||
<INatIconButton
|
||||
className="mr-5"
|
||||
icon="inaturalist"
|
||||
size={41}
|
||||
color={colors.white}
|
||||
backgroundColor={colors.inatGreen}
|
||||
accessibilityLabel="iNaturalist"
|
||||
mode="contained"
|
||||
width={67}
|
||||
height={67}
|
||||
/>
|
||||
{numUnuploadedObservations > 0
|
||||
? (
|
||||
<View className="shrink">
|
||||
<Subheading1
|
||||
maxFontSizeMultiplier={1.5}
|
||||
testID="log-in-to-iNaturalist-text"
|
||||
>
|
||||
{t( "Log-in-to-contribute-and-sync" )}
|
||||
</Subheading1>
|
||||
<Heading1 maxFontSizeMultiplier={1.5}>
|
||||
{ t( "X-observations", { count: numUnuploadedObservations } ) }
|
||||
</Heading1>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<Subheading1
|
||||
maxFontSizeMultiplier={1.5}
|
||||
className="shrink m-0"
|
||||
testID="log-in-to-iNaturalist-text-no-observations"
|
||||
>
|
||||
{t( "Log-in-to-contribute-your-observations" )}
|
||||
</Subheading1>
|
||||
)}
|
||||
</View>
|
||||
<Button
|
||||
onPress={( ) => navigation.navigate( "LoginStackNavigator" )}
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel={t( "Log-in" )}
|
||||
className="w-full"
|
||||
text={t( "LOG-IN-TO-INATURALIST" )}
|
||||
level={logInButtonNeutral
|
||||
? "neutral"
|
||||
: "focus"}
|
||||
testID="log-in-to-iNaturalist-button"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
className="px-5 bg-white w-full"
|
||||
onLayout={event => {
|
||||
const {
|
||||
height
|
||||
} = event.nativeEvent.layout;
|
||||
if ( setHeightAboveToolbar ) {
|
||||
setHeightAboveToolbar( height );
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentUser
|
||||
? signedInContent( )
|
||||
: signedOutContent( )}
|
||||
<Onboarding />
|
||||
</View>
|
||||
{!hideToolbar && (
|
||||
<ToolbarContainer
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
layout={layout}
|
||||
toggleLayout={toggleLayout}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyObservationsHeader;
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
RealmUser
|
||||
} from "realmModels/types";
|
||||
import { accessibleTaxonName } from "sharedHelpers/taxon";
|
||||
import { useGridLayout, useTranslation } from "sharedHooks";
|
||||
import { useGridLayout, useLayoutPrefs, useTranslation } from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
import Announcements from "./Announcements";
|
||||
@@ -108,6 +108,7 @@ const MyObservationsSimple = ( {
|
||||
loggedInWhileInDefaultMode = false,
|
||||
refetchTaxa
|
||||
}: Props ) => {
|
||||
const { isDefaultMode } = useLayoutPrefs( );
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
const route = useRoute( );
|
||||
@@ -201,7 +202,9 @@ const MyObservationsSimple = ( {
|
||||
numUnuploadedObservations > 0 && numUnuploadedObsMissingBasics > 0
|
||||
), [numUnuploadedObservations, numUnuploadedObsMissingBasics] );
|
||||
|
||||
const numUploadableObservations = numUnuploadedObservations - numUnuploadedObsMissingBasics;
|
||||
const numUploadableObservations = isDefaultMode
|
||||
? numUnuploadedObservations - numUnuploadedObsMissingBasics
|
||||
: numUnuploadedObservations;
|
||||
|
||||
const renderTabComponent = ( { id } ) => (
|
||||
<StatTab
|
||||
@@ -276,7 +279,7 @@ const MyObservationsSimple = ( {
|
||||
hideLoadingWheel
|
||||
hideMetadata
|
||||
hideObsUploadStatus={!currentUser}
|
||||
hideObsStatus
|
||||
hideObsStatus={isDefaultMode}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
isConnected={isConnected}
|
||||
obsListKey="MyObservations"
|
||||
@@ -327,19 +330,23 @@ const MyObservationsSimple = ( {
|
||||
{ ( activeTab === TAXA_TAB && taxa.length === 0 ) && renderOfflineNotice( )}
|
||||
</ViewWrapper>
|
||||
{showLoginSheet && <LoginSheet setShowLoginSheet={setShowLoginSheet} />}
|
||||
{/* These four cards should show only in default mode */}
|
||||
<FirstObservationCard triggerCondition={numTotalObservations === 1} />
|
||||
<SecondObservationCard triggerCondition={numTotalObservations === 2} />
|
||||
<FiftyObservationCard
|
||||
triggerCondition={
|
||||
loggedInWhileInDefaultMode && !!currentUser && numTotalObservations >= 50
|
||||
}
|
||||
/>
|
||||
<AccountCreationCard
|
||||
triggerCondition={
|
||||
justFinishedSignup && !!currentUser && numTotalObservations < 20
|
||||
}
|
||||
/>
|
||||
{isDefaultMode && (
|
||||
<>
|
||||
{/* These four cards should show only in default mode */}
|
||||
<FirstObservationCard triggerCondition={numTotalObservations === 1} />
|
||||
<SecondObservationCard triggerCondition={numTotalObservations === 2} />
|
||||
<FiftyObservationCard
|
||||
triggerCondition={
|
||||
loggedInWhileInDefaultMode && !!currentUser && numTotalObservations >= 50
|
||||
}
|
||||
/>
|
||||
<AccountCreationCard
|
||||
triggerCondition={
|
||||
justFinishedSignup && !!currentUser && numTotalObservations < 20
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import { fetchSpeciesCounts } from "api/observations";
|
||||
import { RealmContext } from "providers/contexts.ts";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
import type { RealmObservation, RealmTaxon } from "realmModels/types";
|
||||
import { useInfiniteScroll } from "sharedHooks";
|
||||
import { zustandStorage } from "stores/useStore";
|
||||
|
||||
import MyObservationsSimple, {
|
||||
OBSERVATIONS_TAB,
|
||||
Props,
|
||||
TAXA_TAB
|
||||
} from "./MyObservationsSimple";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
interface SpeciesCount {
|
||||
count: number,
|
||||
taxon: RealmTaxon
|
||||
}
|
||||
|
||||
const MyObservationsSimpleContainer = ( {
|
||||
currentUser,
|
||||
handleIndividualUploadPress,
|
||||
handleSyncButtonPress,
|
||||
handlePullToRefresh,
|
||||
isConnected,
|
||||
isFetchingNextPage,
|
||||
layout,
|
||||
listRef,
|
||||
numTotalObservations,
|
||||
numUnuploadedObservations,
|
||||
observations,
|
||||
onEndReached,
|
||||
onListLayout,
|
||||
onScroll,
|
||||
setShowLoginSheet,
|
||||
showLoginSheet,
|
||||
showNoResults,
|
||||
toggleLayout,
|
||||
justFinishedSignup,
|
||||
loggedInWhileInDefaultMode
|
||||
}: Props ) => {
|
||||
const numOfUserObservations = zustandStorage.getItem( "numOfUserObservations" );
|
||||
const numOfUserSpecies = zustandStorage.getItem( "numOfUserSpecies" );
|
||||
const [activeTab, setActiveTab] = useState( OBSERVATIONS_TAB );
|
||||
const realm = useRealm();
|
||||
|
||||
let numTotalTaxaLocal: number | undefined;
|
||||
const localObservedSpeciesCount: Array<SpeciesCount> = [];
|
||||
if ( !currentUser ) {
|
||||
// Calculate obs and leaf taxa counts from local observations
|
||||
const distinctTaxonObs = realm.objects<RealmObservation>( "Observation" )
|
||||
.filtered( "taxon != null DISTINCT(taxon.id)" );
|
||||
const taxonIds: number[] = distinctTaxonObs
|
||||
.map( o => o.taxon?.id || 0 )
|
||||
.filter( Boolean );
|
||||
const ancestorIds = distinctTaxonObs.map( o => {
|
||||
// We're filtering b/c for taxa above species level, the taxon's own
|
||||
// ID is included in ancestor ids for some reason (this is a bug...
|
||||
// somewhere)
|
||||
const taxonAncestorIds = (
|
||||
o.taxon?.ancestor_ids || []
|
||||
).filter( id => Number( id ) !== Number( o.taxon?.id ) );
|
||||
return taxonAncestorIds;
|
||||
} ).flat( Infinity );
|
||||
const leafTaxonIds = taxonIds.filter( taxonId => !ancestorIds.includes( taxonId ) );
|
||||
numTotalTaxaLocal = leafTaxonIds.length;
|
||||
|
||||
// Get leaf taxa if we're viewing the species tab
|
||||
if ( activeTab === TAXA_TAB ) {
|
||||
// IDK how to placate TypeScript here. ~~~kueda 20250108
|
||||
const localObs = realm.objects( "Observation" ).filtered( "taxon.id IN $0", leafTaxonIds );
|
||||
leafTaxonIds.forEach( id => {
|
||||
const obs = localObs.filter( o => o.taxon.id === id );
|
||||
localObservedSpeciesCount.push( { count: obs.length, taxon: obs[0].taxon } );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
data: remoteObservedTaxaCounts,
|
||||
isFetchingNextPage: isFetchingTaxa,
|
||||
fetchNextPage: fetchMoreTaxa,
|
||||
totalResults: numTotalTaxaRemote,
|
||||
refetch: refetchTaxa
|
||||
} = useInfiniteScroll(
|
||||
"MyObsSimple-fetchSpeciesCounts",
|
||||
fetchSpeciesCounts,
|
||||
{
|
||||
user_id: currentUser?.id,
|
||||
fields: {
|
||||
taxon: Taxon.LIMITED_TAXON_FIELDS
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: !!currentUser
|
||||
}
|
||||
);
|
||||
|
||||
const numTotalTaxa = typeof ( numTotalTaxaRemote ) === "number"
|
||||
? numTotalTaxaRemote
|
||||
: numTotalTaxaLocal;
|
||||
|
||||
useEffect( ( ) => {
|
||||
// persist this number in zustand so a user can see their latest observations count
|
||||
// even if they're offline
|
||||
if ( numTotalObservations > numOfUserObservations ) {
|
||||
zustandStorage.setItem( "numOfUserObservations", numTotalObservations );
|
||||
}
|
||||
}, [numTotalObservations, numOfUserObservations] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
// persist this number in zustand so a user can see their latest species count
|
||||
// even if they're offline
|
||||
if ( numTotalTaxa > numOfUserSpecies ) {
|
||||
zustandStorage.setItem( "numOfUserSpecies", numTotalTaxa );
|
||||
}
|
||||
}, [numTotalTaxa, numOfUserSpecies] );
|
||||
|
||||
return (
|
||||
<MyObservationsSimple
|
||||
currentUser={currentUser}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
isConnected={isConnected}
|
||||
handleIndividualUploadPress={handleIndividualUploadPress}
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
handlePullToRefresh={handlePullToRefresh}
|
||||
layout={layout}
|
||||
listRef={listRef}
|
||||
numUnuploadedObservations={numUnuploadedObservations}
|
||||
observations={observations}
|
||||
onEndReached={onEndReached}
|
||||
onListLayout={onListLayout}
|
||||
onScroll={onScroll}
|
||||
setShowLoginSheet={setShowLoginSheet}
|
||||
showLoginSheet={showLoginSheet}
|
||||
showNoResults={showNoResults}
|
||||
toggleLayout={toggleLayout}
|
||||
numTotalTaxa={numOfUserSpecies}
|
||||
numTotalObservations={numOfUserObservations}
|
||||
taxa={
|
||||
currentUser
|
||||
? remoteObservedTaxaCounts
|
||||
: localObservedSpeciesCount
|
||||
}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
fetchMoreTaxa={fetchMoreTaxa}
|
||||
isFetchingTaxa={isFetchingTaxa}
|
||||
justFinishedSignup={justFinishedSignup}
|
||||
loggedInWhileInDefaultMode={loggedInWhileInDefaultMode}
|
||||
refetchTaxa={refetchTaxa}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyObservationsSimpleContainer;
|
||||
@@ -73,6 +73,7 @@ const MyObservationsSimpleHeader = ( {
|
||||
<SimpleUploadBannerContainer
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
numUploadableObservations={numUploadableObservations}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<View className="flex-row justify-between items-center px-5 py-1">
|
||||
{currentUser
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// @flow
|
||||
import { Body3 } from "components/SharedComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useCurrentUser, useTranslation } from "sharedHooks";
|
||||
|
||||
const Onboarding = ( ): Node => {
|
||||
const currentUser = useCurrentUser( );
|
||||
const numObservations = currentUser?.observations_count || 0;
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const getOnboardingText = ( ) => {
|
||||
if ( numObservations <= 10 ) {
|
||||
return t( "As-you-upload-more-observations" );
|
||||
}
|
||||
if ( numObservations <= 50 ) {
|
||||
return t( "Observations-you-upload-to-iNaturalist" );
|
||||
}
|
||||
return t( "You-can-search-observations-of-any-plant-or-animal" );
|
||||
};
|
||||
|
||||
return numObservations <= 100 && numObservations > 0
|
||||
? <Body3 className="pb-5">{getOnboardingText( ) }</Body3>
|
||||
: null;
|
||||
};
|
||||
|
||||
export default Onboarding;
|
||||
@@ -18,6 +18,7 @@ const SimpleErrorHeader = ( {
|
||||
isConnected
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
return (
|
||||
<>
|
||||
<Announcements isConnected={isConnected} />
|
||||
|
||||
@@ -25,12 +25,14 @@ const DELETION_STARTED_PROGRESS = 0.25;
|
||||
|
||||
type Props = {
|
||||
numUploadableObservations: number,
|
||||
handleSyncButtonPress: Function
|
||||
handleSyncButtonPress: Function,
|
||||
currentUser: Object
|
||||
}
|
||||
|
||||
const SimpleUploadBannerContainer = ( {
|
||||
handleSyncButtonPress,
|
||||
numUploadableObservations
|
||||
numUploadableObservations,
|
||||
currentUser
|
||||
}: Props ): Node => {
|
||||
const numOfUserObservations = zustandStorage.getItem( "numOfUserObservations" );
|
||||
const currentDeleteCount = useStore( state => state.currentDeleteCount );
|
||||
@@ -176,7 +178,7 @@ const SimpleUploadBannerContainer = ( {
|
||||
|
||||
// hide when there are less than two observations to upload
|
||||
// but make sure completed status is displayed when uploads are finished
|
||||
if ( numOfUserObservations < 2 && progress === 0 ) {
|
||||
if ( !currentUser && numOfUserObservations < 2 && progress === 0 ) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
Body2,
|
||||
Body4,
|
||||
INatIcon,
|
||||
INatIconButton,
|
||||
RotatingINatIconButton,
|
||||
UploadProgressBar
|
||||
} from "components/SharedComponents";
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
type Props = {
|
||||
error: ?string,
|
||||
handleSyncButtonPress: Function,
|
||||
layout: string,
|
||||
navToExplore: Function,
|
||||
progress: number,
|
||||
rotating?: boolean,
|
||||
showsCancelUploadButton?: boolean,
|
||||
showsCheckmark?: boolean,
|
||||
showsExclamation?: boolean,
|
||||
showsExploreIcon?: boolean,
|
||||
statusText: ?string,
|
||||
stopAllUploads: Function,
|
||||
syncIconColor: string,
|
||||
toggleLayout: Function
|
||||
}
|
||||
|
||||
const Toolbar = ( {
|
||||
error,
|
||||
handleSyncButtonPress,
|
||||
layout,
|
||||
navToExplore,
|
||||
progress,
|
||||
rotating = false,
|
||||
showsCancelUploadButton = false,
|
||||
showsCheckmark = false,
|
||||
showsExclamation: showsExclamationProp = false,
|
||||
showsExploreIcon = false,
|
||||
statusText = "",
|
||||
stopAllUploads,
|
||||
syncIconColor,
|
||||
toggleLayout
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
// The exclamation mark should *never* appear while rotating, no matter what
|
||||
// the props say
|
||||
let showsExclamation = showsExclamationProp;
|
||||
if ( rotating ) showsExclamation = false;
|
||||
return (
|
||||
<View className={
|
||||
classNames(
|
||||
{ "border-b border-lightGray": layout !== "grid" }
|
||||
)
|
||||
}
|
||||
>
|
||||
<View className="flex-row mx-4">
|
||||
{/* First col */}
|
||||
{showsExploreIcon && (
|
||||
<INatIconButton
|
||||
icon="compass-rose-outline"
|
||||
onPress={navToExplore}
|
||||
accessibilityLabel={t( "See-all-your-observations-in-explore" )}
|
||||
accessibilityHint={t( "Navigates-to-explore" )}
|
||||
accessibilityRole="button"
|
||||
size={30}
|
||||
disabled={false}
|
||||
// FWIW, IconButton has a little margin we can control and a
|
||||
// little padding that we can't control, so the negative margin
|
||||
// here is to ensure the visible icon is flush with the edge of
|
||||
// the container
|
||||
className="-ml-[7px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Center col. Initial width of 0 seems to be a hack that works to get
|
||||
grow to work correctly with text content
|
||||
*/}
|
||||
<View
|
||||
className={classNames(
|
||||
"w-[0px] grow",
|
||||
!showsExploreIcon && "-ml-[11px]"
|
||||
)}
|
||||
>
|
||||
<View className="flex-row ml-1">
|
||||
<View className="mr-1">
|
||||
<RotatingINatIconButton
|
||||
icon={
|
||||
showsExclamation
|
||||
? "sync-unsynced"
|
||||
: "sync"
|
||||
}
|
||||
rotating={rotating}
|
||||
onPress={handleSyncButtonPress}
|
||||
color={syncIconColor}
|
||||
disabled={rotating}
|
||||
accessibilityLabel={t( "Sync-observations" )}
|
||||
size={30}
|
||||
testID="SyncButton"
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-row shrink">
|
||||
<View className="shrink pb-1 justify-center">
|
||||
{ statusText !== "" && (
|
||||
<Pressable
|
||||
onPress={handleSyncButtonPress}
|
||||
className="flex-row items-center grow"
|
||||
accessibilityRole="button"
|
||||
disabled={rotating || showsCheckmark}
|
||||
>
|
||||
<Body2>
|
||||
{statusText}
|
||||
</Body2>
|
||||
{showsCheckmark && (
|
||||
<View className="ml-2">
|
||||
<INatIcon name="checkmark" size={11} color={colors.inatGreen} />
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
{error && (
|
||||
statusText === ""
|
||||
? <Body2 className="color-warningRed">{error}</Body2>
|
||||
: <Body4 className="color-warningRed">{error}</Body4>
|
||||
)}
|
||||
</View>
|
||||
{showsCancelUploadButton && (
|
||||
<INatIconButton
|
||||
icon="close"
|
||||
size={11}
|
||||
accessibilityLabel={t( "Stop-upload" )}
|
||||
onPress={stopAllUploads}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Last col */}
|
||||
<INatIconButton
|
||||
icon={layout === "grid"
|
||||
? "listview"
|
||||
: "gridview"}
|
||||
size={30}
|
||||
disabled={false}
|
||||
accessibilityLabel={layout === "grid"
|
||||
? t( "List-layout" )
|
||||
: t( "Grid-layout" )}
|
||||
testID={
|
||||
layout === "list"
|
||||
? "MyObservationsToolbar.toggleGridView"
|
||||
: "MyObservationsToolbar.toggleListView"
|
||||
}
|
||||
onPress={toggleLayout}
|
||||
// Negative margin here is similar to above: trying to get the icon
|
||||
// flush with the container. ml-auto is a bit of a hack to pull
|
||||
// this button all the way to the end.
|
||||
className="-mr-[7px]"
|
||||
/>
|
||||
</View>
|
||||
<UploadProgressBar progress={progress} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
@@ -1,219 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { Dimensions, PixelRatio } from "react-native";
|
||||
import {
|
||||
useCurrentUser,
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
import {
|
||||
AUTOMATIC_SYNC_IN_PROGRESS,
|
||||
MANUAL_SYNC_IN_PROGRESS
|
||||
} from "stores/createSyncObservationsSlice.ts";
|
||||
import {
|
||||
UPLOAD_COMPLETE,
|
||||
UPLOAD_IN_PROGRESS,
|
||||
UPLOAD_PENDING
|
||||
} from "stores/createUploadObservationsSlice.ts";
|
||||
import useStore from "stores/useStore";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
import Toolbar from "./Toolbar";
|
||||
|
||||
const screenWidth = Dimensions.get( "window" ).width * PixelRatio.get( );
|
||||
|
||||
const DELETION_STARTED_PROGRESS = 0.25;
|
||||
|
||||
type Props = {
|
||||
handleSyncButtonPress: Function,
|
||||
layout: string,
|
||||
toggleLayout: Function
|
||||
}
|
||||
|
||||
const ToolbarContainer = ( {
|
||||
handleSyncButtonPress,
|
||||
layout,
|
||||
toggleLayout
|
||||
}: Props ): Node => {
|
||||
const setExploreView = useStore( state => state.setExploreView );
|
||||
const currentUser = useCurrentUser( );
|
||||
const navigation = useNavigation( );
|
||||
const currentDeleteCount = useStore( state => state.currentDeleteCount );
|
||||
const deleteError = useStore( state => state.deleteError );
|
||||
const uploadMultiError = useStore( state => state.multiError );
|
||||
const uploadErrorsByUuid = useStore( state => state.errorsByUuid );
|
||||
const initialNumObservationsInQueue = useStore( state => state.initialNumObservationsInQueue );
|
||||
const numUnuploadedObservations = useStore( state => state.numUnuploadedObservations );
|
||||
const totalToolbarProgress = useStore( state => state.totalToolbarProgress );
|
||||
const uploadStatus = useStore( state => state.uploadStatus );
|
||||
const syncingStatus = useStore( state => state.syncingStatus );
|
||||
const initialNumDeletionsInQueue = useStore( state => state.initialNumDeletionsInQueue );
|
||||
|
||||
const stopAllUploads = useStore( state => state.stopAllUploads );
|
||||
const numUploadsAttempted = useStore( state => state.numUploadsAttempted );
|
||||
|
||||
// Note that initialNumObservationsInQueue is the number of obs being uploaded in
|
||||
// the current upload session, so it might be 1 if a single obs is
|
||||
// being uploaded even though 5 obs need upload
|
||||
const translationParams = useMemo( ( ) => ( {
|
||||
total: initialNumObservationsInQueue,
|
||||
currentUploadCount: Math.min( numUploadsAttempted, initialNumObservationsInQueue )
|
||||
} ), [
|
||||
initialNumObservationsInQueue,
|
||||
numUploadsAttempted
|
||||
] );
|
||||
|
||||
const navToExplore = useCallback(
|
||||
( ) => {
|
||||
setExploreView( "observations" );
|
||||
navigation.navigate( "Explore", {
|
||||
user: currentUser,
|
||||
worldwide: true
|
||||
} );
|
||||
},
|
||||
[navigation, currentUser, setExploreView]
|
||||
);
|
||||
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const deletionsComplete = initialNumDeletionsInQueue === currentDeleteCount;
|
||||
const deletionsInProgress = initialNumDeletionsInQueue > 0 && !deletionsComplete;
|
||||
|
||||
const automaticSyncInProgress = syncingStatus === AUTOMATIC_SYNC_IN_PROGRESS;
|
||||
const manualSyncInProgress = syncingStatus === MANUAL_SYNC_IN_PROGRESS;
|
||||
const pendingUpload = uploadStatus === UPLOAD_PENDING && numUnuploadedObservations > 0;
|
||||
const uploadInProgress = uploadStatus === UPLOAD_IN_PROGRESS && numUploadsAttempted > 0;
|
||||
const uploadsComplete = uploadStatus === UPLOAD_COMPLETE && initialNumObservationsInQueue > 0;
|
||||
const totalUploadErrors = Object.keys( uploadErrorsByUuid ).length;
|
||||
|
||||
const setDeletionsProgress = ( ) => {
|
||||
// TODO: we should emit deletions progress like we do for uploads for an accurate progress
|
||||
// right now, a user can only delete a single local upload at a time from ObsEdit
|
||||
// so we don't need a more robust count here (20240607)
|
||||
if ( initialNumDeletionsInQueue === 0 ) {
|
||||
return 0;
|
||||
}
|
||||
if ( !deletionsComplete ) {
|
||||
return currentDeleteCount * DELETION_STARTED_PROGRESS;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
const deletionsProgress = setDeletionsProgress( );
|
||||
|
||||
const showFinalUploadError = ( totalUploadErrors > 0 && uploadsComplete )
|
||||
|| ( totalUploadErrors > 0 && ( numUploadsAttempted === initialNumObservationsInQueue ) );
|
||||
|
||||
const rotating = manualSyncInProgress || uploadInProgress || deletionsInProgress;
|
||||
const showsCheckmark = ( uploadsComplete && !uploadMultiError )
|
||||
|| ( deletionsComplete && !deleteError && initialNumDeletionsInQueue > 0 );
|
||||
|
||||
const showsExclamation = pendingUpload || showFinalUploadError;
|
||||
|
||||
const getStatusText = useCallback( ( ) => {
|
||||
if ( manualSyncInProgress ) { return t( "Syncing" ); }
|
||||
|
||||
const deletionParams = {
|
||||
total: initialNumDeletionsInQueue,
|
||||
currentDeleteCount
|
||||
};
|
||||
if ( initialNumDeletionsInQueue > 0 ) {
|
||||
if ( deletionsComplete ) {
|
||||
return t( "X-observations-deleted", { count: initialNumDeletionsInQueue } );
|
||||
}
|
||||
// iPhone 4 pixel width
|
||||
return screenWidth <= 640
|
||||
? t( "Deleting-x-of-y--observations", deletionParams )
|
||||
: t( "Deleting-x-of-y-observations-2", deletionParams );
|
||||
}
|
||||
|
||||
if ( pendingUpload ) {
|
||||
return t( "Upload-x-observations", { count: numUnuploadedObservations } );
|
||||
}
|
||||
|
||||
if ( uploadInProgress ) {
|
||||
// iPhone 4 pixel width
|
||||
return screenWidth <= 640
|
||||
? t( "Uploading-x-of-y", translationParams )
|
||||
: t( "Uploading-x-of-y-observations", translationParams );
|
||||
}
|
||||
|
||||
if ( uploadsComplete ) {
|
||||
return t( "X-observations-uploaded", { count: numUploadsAttempted } );
|
||||
}
|
||||
|
||||
return "";
|
||||
}, [
|
||||
currentDeleteCount,
|
||||
deletionsComplete,
|
||||
initialNumDeletionsInQueue,
|
||||
numUploadsAttempted,
|
||||
numUnuploadedObservations,
|
||||
pendingUpload,
|
||||
manualSyncInProgress,
|
||||
t,
|
||||
translationParams,
|
||||
uploadInProgress,
|
||||
uploadsComplete
|
||||
] );
|
||||
|
||||
const errorText = useMemo( ( ) => {
|
||||
if ( automaticSyncInProgress ) {
|
||||
return null;
|
||||
}
|
||||
let error;
|
||||
if ( deleteError ) {
|
||||
error = deleteError;
|
||||
} else if ( totalUploadErrors > 0 ) {
|
||||
error = t( "x-uploads-failed", { count: totalUploadErrors } );
|
||||
} else {
|
||||
error = uploadMultiError;
|
||||
}
|
||||
return error;
|
||||
}, [
|
||||
deleteError,
|
||||
automaticSyncInProgress,
|
||||
t,
|
||||
totalUploadErrors,
|
||||
uploadMultiError
|
||||
] );
|
||||
|
||||
const getSyncIconColor = useCallback( ( ) => {
|
||||
if ( showFinalUploadError ) {
|
||||
return colors.warningRed;
|
||||
}
|
||||
if ( pendingUpload || uploadInProgress ) {
|
||||
return colors.inatGreen;
|
||||
}
|
||||
return colors.darkGray;
|
||||
}, [
|
||||
showFinalUploadError,
|
||||
pendingUpload,
|
||||
uploadInProgress
|
||||
] );
|
||||
|
||||
const statusText = getStatusText( );
|
||||
const syncIconColor = getSyncIconColor( );
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
error={errorText}
|
||||
handleSyncButtonPress={handleSyncButtonPress}
|
||||
layout={layout}
|
||||
navToExplore={navToExplore}
|
||||
progress={totalToolbarProgress || deletionsProgress}
|
||||
rotating={rotating}
|
||||
showsCancelUploadButton={uploadInProgress}
|
||||
showsCheckmark={showsCheckmark}
|
||||
showsExclamation={showsExclamation}
|
||||
showsExploreIcon={currentUser}
|
||||
statusText={statusText}
|
||||
stopAllUploads={stopAllUploads}
|
||||
syncIconColor={syncIconColor}
|
||||
toggleLayout={toggleLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarContainer;
|
||||
@@ -64,12 +64,24 @@ const useSyncObservations = (
|
||||
deleteQueue.forEach( async ( uuid: string, i: number ) => {
|
||||
setCurrentDeletionUuid( uuid );
|
||||
const observation = realm.objectForPrimaryKey( "Observation", uuid );
|
||||
|
||||
// Mark as pending deletion first instead of immediately deleting
|
||||
Observation.markPendingDeletion( realm, uuid );
|
||||
|
||||
const hasBeenSyncedRemotely = observation?._synced_at;
|
||||
|
||||
if ( !hasBeenSyncedRemotely ) {
|
||||
Observation.deleteLocalObservation( realm, uuid );
|
||||
} else {
|
||||
handleRemoteDeletion.mutate( { uuid } );
|
||||
try {
|
||||
await handleRemoteDeletion.mutateAsync( { uuid } );
|
||||
} catch ( error ) {
|
||||
// In case of failure, clear the pending deletion flag after some time
|
||||
// to allow retrying later
|
||||
setTimeout( ( ) => {
|
||||
Observation.clearPendingDeletion( realm, uuid );
|
||||
}, 60000 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( i > 0 ) {
|
||||
|
||||
@@ -129,8 +129,6 @@ Apply-filters = Apply filters
|
||||
April = April
|
||||
Are-you-an-educator = Are you an educator wanting to use iNaturalist with your students?
|
||||
Are-you-sure-you-want-to-log-out = Are you sure you want to log out of your iNaturalist account? All observations that haven’t been uploaded to iNaturalist will be deleted.
|
||||
# Onboarding text on MyObservations: 0-10 observations
|
||||
As-you-upload-more-observations = As you upload more observations, others in our community may be able to help you identify them!
|
||||
attribution-cc-by = some rights reserved (CC BY)
|
||||
attribution-cc-by-nc = some rights reserved (CC BY-NC)
|
||||
attribution-cc-by-nc-nd = some rights reserved (CC BY-NC-ND)
|
||||
@@ -551,8 +549,6 @@ Google-Play-Services-Not-Installed = Google Play Services Not Installed
|
||||
GRANT-PERMISSION = GRANT PERMISSION
|
||||
# Title of a screen asking for permission
|
||||
Grant-Permission-title = Grant Permission
|
||||
# Label for button to switch to a grid layout of observations
|
||||
Grid-layout = Grid layout
|
||||
Group-Photos = Group Photos
|
||||
# Onboarding for users learning to group photos in the camera roll
|
||||
Group-photos-onboarding = Group photos into observations– make sure there is only one species per observation
|
||||
@@ -692,8 +688,6 @@ LEAVE-PROJECT = LEAVE PROJECT
|
||||
LEAVE-PROJECT--question = LEAVE PROJECT?
|
||||
LEAVE-US-A-REVIEW = LEAVE US A REVIEW!
|
||||
Lets-reset-your-password = Let’s reset your password.
|
||||
# Label for button to switch to a list layout of observations
|
||||
List-layout = List layout
|
||||
Loading-iNaturalists-AI-Camera = Loading iNaturalist's AI Camera
|
||||
Loads-content-that-requires-an-Internet-connection = Loads content that requires an Internet connection
|
||||
LOCATION = LOCATION
|
||||
@@ -701,10 +695,6 @@ Location = Location
|
||||
Location-accuracy-is-too-imprecise = Location accuracy is too imprecise to help identifiers. Please zoom in.
|
||||
LOCATION-TOO-IMPRECISE = LOCATION TOO IMPRECISE
|
||||
LOG-IN = LOG IN
|
||||
# Second person imperative label to go to log in screen
|
||||
Log-in = Log in
|
||||
Log-in-to-contribute-and-sync = Log in to contribute & sync
|
||||
Log-in-to-contribute-your-observations = Log in to contribute your observations to science!
|
||||
LOG-IN-TO-INATURALIST = LOG IN TO INATURALIST
|
||||
Log-in-to-iNaturalist = Log in to iNaturalist
|
||||
LOG-OUT = LOG OUT
|
||||
@@ -863,8 +853,6 @@ OBSERVATIONS-WITHOUT-NUMBER =
|
||||
[one] OBSERVATION
|
||||
*[other] OBSERVATIONS
|
||||
}
|
||||
# Onboarding text on MyObservations: Onboarding text on MyObservations: 11-50 observations
|
||||
Observations-you-upload-to-iNaturalist = Observations you upload to iNaturalist can be used by scientists and researchers worldwide.
|
||||
# Title of screen asking for permission to access the camera
|
||||
Observe-and-identify-organisms-in-real-time-with-your-camera = Observe and identify organisms in real time with your camera
|
||||
# Text for a button prompting the user to grant access to the camera
|
||||
@@ -1126,8 +1114,6 @@ Search-suggestions-with-location = Search suggestions with location
|
||||
Search-suggestions-without-location = Search suggestions without location
|
||||
SEARCH-TAXA = SEARCH TAXA
|
||||
SEARCH-USERS = SEARCH USERS
|
||||
# Accessibility label for Explore button on MyObservations toolbar
|
||||
See-all-your-observations-in-explore = See all your observations in explore
|
||||
# Accessibility label for Observations button on UserProfile screen
|
||||
See-observations-by-this-user-in-Explore = See observations by this user in Explore
|
||||
# Accessibility label for Explore button on TaxonDetails screen
|
||||
@@ -1355,8 +1341,6 @@ View-suggestions = View suggestions
|
||||
Watch-your-notifications-for-identifications = Watch your notifications for identifications!
|
||||
We-are-not-confident-enough-to-make-a-top-ID-suggestion = We’re not confident enough to make a top ID suggestion, but here are some other suggestions:
|
||||
Welcome-back = Welcome back!
|
||||
# Welcome user back to app
|
||||
Welcome-user = <0>Welcome back,</0><1>{ $userHandle }</1>
|
||||
Weve-made-some-updates = We've made some updates, so we recommend taking a look at your settings. You can always update these later.
|
||||
WHAT-IS-INATURALIST = WHAT IS INATURALIST?
|
||||
Whats-more-by-recording = What's more, by recording and sharing your observations, you'll create research-quality data for scientists working to better understand and protect nature. So if you like recording your findings from the outdoors, or if you just like learning about life, join us!
|
||||
@@ -1414,12 +1398,6 @@ X-Observations =
|
||||
[one] 1 Observation
|
||||
*[other] { $count } Observations
|
||||
}
|
||||
# Shows number of observations in a variety of contexts
|
||||
X-observations =
|
||||
{ $count ->
|
||||
[one] 1 observation
|
||||
*[other] { $count } observations
|
||||
}
|
||||
# Label for a count of observations that appears above this text
|
||||
X-OBSERVATIONS--below-number =
|
||||
{ $count ->
|
||||
@@ -1527,8 +1505,6 @@ You-can-also-check-out-merchandise = You can also check out merchandise for iNat
|
||||
You-can-click-join-on-the-project-page = You can click “join” on the project page.
|
||||
You-can-find-answers-on-our-help-page = You can find answers on our help page.
|
||||
You-can-only-add-20-photos-per-observation = You can only add 20 photos per observation
|
||||
# Onboarding text on MyObservations: Onboarding text on MyObservations: 51-100 observations
|
||||
You-can-search-observations-of-any-plant-or-animal = You can search observations of any plant or animal anywhere in the world with Explore!
|
||||
You-can-still-share-the-file = You can still share the file with another app. If you can email it, please send it to { $email }
|
||||
You-can-upload-this-observation-to-our-community = You can upload this observation to our community to get an identification from a real person, and help our AI improve its identifications in the future
|
||||
You-changed-filters-will-be-discarded = You changed filters, but they were not applied to your explore search results.
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
"April": "April",
|
||||
"Are-you-an-educator": "Are you an educator wanting to use iNaturalist with your students?",
|
||||
"Are-you-sure-you-want-to-log-out": "Are you sure you want to log out of your iNaturalist account? All observations that haven’t been uploaded to iNaturalist will be deleted.",
|
||||
"As-you-upload-more-observations": "As you upload more observations, others in our community may be able to help you identify them!",
|
||||
"attribution-cc-by": "some rights reserved (CC BY)",
|
||||
"attribution-cc-by-nc": "some rights reserved (CC BY-NC)",
|
||||
"attribution-cc-by-nc-nd": "some rights reserved (CC BY-NC-ND)",
|
||||
@@ -310,7 +309,6 @@
|
||||
"Google-Play-Services-Not-Installed": "Google Play Services Not Installed",
|
||||
"GRANT-PERMISSION": "GRANT PERMISSION",
|
||||
"Grant-Permission-title": "Grant Permission",
|
||||
"Grid-layout": "Grid layout",
|
||||
"Group-Photos": "Group Photos",
|
||||
"Group-photos-onboarding": "Group photos into observations– make sure there is only one species per observation",
|
||||
"Grow-your-collection": "Grow your collection",
|
||||
@@ -403,7 +401,6 @@
|
||||
"LEAVE-PROJECT--question": "LEAVE PROJECT?",
|
||||
"LEAVE-US-A-REVIEW": "LEAVE US A REVIEW!",
|
||||
"Lets-reset-your-password": "Let’s reset your password.",
|
||||
"List-layout": "List layout",
|
||||
"Loading-iNaturalists-AI-Camera": "Loading iNaturalist's AI Camera",
|
||||
"Loads-content-that-requires-an-Internet-connection": "Loads content that requires an Internet connection",
|
||||
"LOCATION": "LOCATION",
|
||||
@@ -411,9 +408,6 @@
|
||||
"Location-accuracy-is-too-imprecise": "Location accuracy is too imprecise to help identifiers. Please zoom in.",
|
||||
"LOCATION-TOO-IMPRECISE": "LOCATION TOO IMPRECISE",
|
||||
"LOG-IN": "LOG IN",
|
||||
"Log-in": "Log in",
|
||||
"Log-in-to-contribute-and-sync": "Log in to contribute & sync",
|
||||
"Log-in-to-contribute-your-observations": "Log in to contribute your observations to science!",
|
||||
"LOG-IN-TO-INATURALIST": "LOG IN TO INATURALIST",
|
||||
"Log-in-to-iNaturalist": "Log in to iNaturalist",
|
||||
"LOG-OUT": "LOG OUT",
|
||||
@@ -511,7 +505,6 @@
|
||||
"Observations-on-iNat-are-cited": "Observations on iNaturalist are cited in scientific papers, have led to rediscoveries, and help scientists understand life on our planet",
|
||||
"Observations-View": "Observations View",
|
||||
"OBSERVATIONS-WITHOUT-NUMBER": "{ $count ->\n [one] OBSERVATION\n *[other] OBSERVATIONS\n}",
|
||||
"Observations-you-upload-to-iNaturalist": "Observations you upload to iNaturalist can be used by scientists and researchers worldwide.",
|
||||
"Observe-and-identify-organisms-in-real-time-with-your-camera": "Observe and identify organisms in real time with your camera",
|
||||
"OBSERVE-ORGANISMS": "OBSERVE ORGANISMS",
|
||||
"OBSERVED-AT--label": "OBSERVED AT",
|
||||
@@ -712,7 +705,6 @@
|
||||
"Search-suggestions-without-location": "Search suggestions without location",
|
||||
"SEARCH-TAXA": "SEARCH TAXA",
|
||||
"SEARCH-USERS": "SEARCH USERS",
|
||||
"See-all-your-observations-in-explore": "See all your observations in explore",
|
||||
"See-observations-by-this-user-in-Explore": "See observations by this user in Explore",
|
||||
"See-observations-of-this-taxon-in-explore": "See observations of this taxon in explore",
|
||||
"See-project-members": "See project members",
|
||||
@@ -875,7 +867,6 @@
|
||||
"Watch-your-notifications-for-identifications": "Watch your notifications for identifications!",
|
||||
"We-are-not-confident-enough-to-make-a-top-ID-suggestion": "We’re not confident enough to make a top ID suggestion, but here are some other suggestions:",
|
||||
"Welcome-back": "Welcome back!",
|
||||
"Welcome-user": "<0>Welcome back,</0><1>{ $userHandle }</1>",
|
||||
"Weve-made-some-updates": "We've made some updates, so we recommend taking a look at your settings. You can always update these later.",
|
||||
"WHAT-IS-INATURALIST": "WHAT IS INATURALIST?",
|
||||
"Whats-more-by-recording": "What's more, by recording and sharing your observations, you'll create research-quality data for scientists working to better understand and protect nature. So if you like recording your findings from the outdoors, or if you just like learning about life, join us!",
|
||||
@@ -899,7 +890,6 @@
|
||||
"X-Identifiers": "{ $count ->\n [one] { $count } Identifier\n *[other] { $count } Identifiers\n}",
|
||||
"X-MEMBERS": "{ $count } MEMBERS",
|
||||
"X-Observations": "{ $count ->\n [one] 1 Observation\n *[other] { $count } Observations\n}",
|
||||
"X-observations": "{ $count ->\n [one] 1 observation\n *[other] { $count } observations\n}",
|
||||
"X-OBSERVATIONS--below-number": "{ $count ->\n [one] OBSERVATION\n *[other] OBSERVATIONS\n}",
|
||||
"X-observations-deleted": "{ $count ->\n [one] 1 observation deleted\n *[other] { $count } observations deleted\n}",
|
||||
"X-observations-uploaded": "{ $count ->\n [one] 1 observation uploaded\n *[other] { $count } observations uploaded\n}",
|
||||
@@ -926,7 +916,6 @@
|
||||
"You-can-click-join-on-the-project-page": "You can click “join” on the project page.",
|
||||
"You-can-find-answers-on-our-help-page": "You can find answers on our help page.",
|
||||
"You-can-only-add-20-photos-per-observation": "You can only add 20 photos per observation",
|
||||
"You-can-search-observations-of-any-plant-or-animal": "You can search observations of any plant or animal anywhere in the world with Explore!",
|
||||
"You-can-still-share-the-file": "You can still share the file with another app. If you can email it, please send it to { $email }",
|
||||
"You-can-upload-this-observation-to-our-community": "You can upload this observation to our community to get an identification from a real person, and help our AI improve its identifications in the future",
|
||||
"You-changed-filters-will-be-discarded": "You changed filters, but they were not applied to your explore search results.",
|
||||
|
||||
@@ -129,8 +129,6 @@ Apply-filters = Apply filters
|
||||
April = April
|
||||
Are-you-an-educator = Are you an educator wanting to use iNaturalist with your students?
|
||||
Are-you-sure-you-want-to-log-out = Are you sure you want to log out of your iNaturalist account? All observations that haven’t been uploaded to iNaturalist will be deleted.
|
||||
# Onboarding text on MyObservations: 0-10 observations
|
||||
As-you-upload-more-observations = As you upload more observations, others in our community may be able to help you identify them!
|
||||
attribution-cc-by = some rights reserved (CC BY)
|
||||
attribution-cc-by-nc = some rights reserved (CC BY-NC)
|
||||
attribution-cc-by-nc-nd = some rights reserved (CC BY-NC-ND)
|
||||
@@ -551,8 +549,6 @@ Google-Play-Services-Not-Installed = Google Play Services Not Installed
|
||||
GRANT-PERMISSION = GRANT PERMISSION
|
||||
# Title of a screen asking for permission
|
||||
Grant-Permission-title = Grant Permission
|
||||
# Label for button to switch to a grid layout of observations
|
||||
Grid-layout = Grid layout
|
||||
Group-Photos = Group Photos
|
||||
# Onboarding for users learning to group photos in the camera roll
|
||||
Group-photos-onboarding = Group photos into observations– make sure there is only one species per observation
|
||||
@@ -692,8 +688,6 @@ LEAVE-PROJECT = LEAVE PROJECT
|
||||
LEAVE-PROJECT--question = LEAVE PROJECT?
|
||||
LEAVE-US-A-REVIEW = LEAVE US A REVIEW!
|
||||
Lets-reset-your-password = Let’s reset your password.
|
||||
# Label for button to switch to a list layout of observations
|
||||
List-layout = List layout
|
||||
Loading-iNaturalists-AI-Camera = Loading iNaturalist's AI Camera
|
||||
Loads-content-that-requires-an-Internet-connection = Loads content that requires an Internet connection
|
||||
LOCATION = LOCATION
|
||||
@@ -701,10 +695,6 @@ Location = Location
|
||||
Location-accuracy-is-too-imprecise = Location accuracy is too imprecise to help identifiers. Please zoom in.
|
||||
LOCATION-TOO-IMPRECISE = LOCATION TOO IMPRECISE
|
||||
LOG-IN = LOG IN
|
||||
# Second person imperative label to go to log in screen
|
||||
Log-in = Log in
|
||||
Log-in-to-contribute-and-sync = Log in to contribute & sync
|
||||
Log-in-to-contribute-your-observations = Log in to contribute your observations to science!
|
||||
LOG-IN-TO-INATURALIST = LOG IN TO INATURALIST
|
||||
Log-in-to-iNaturalist = Log in to iNaturalist
|
||||
LOG-OUT = LOG OUT
|
||||
@@ -863,8 +853,6 @@ OBSERVATIONS-WITHOUT-NUMBER =
|
||||
[one] OBSERVATION
|
||||
*[other] OBSERVATIONS
|
||||
}
|
||||
# Onboarding text on MyObservations: Onboarding text on MyObservations: 11-50 observations
|
||||
Observations-you-upload-to-iNaturalist = Observations you upload to iNaturalist can be used by scientists and researchers worldwide.
|
||||
# Title of screen asking for permission to access the camera
|
||||
Observe-and-identify-organisms-in-real-time-with-your-camera = Observe and identify organisms in real time with your camera
|
||||
# Text for a button prompting the user to grant access to the camera
|
||||
@@ -1126,8 +1114,6 @@ Search-suggestions-with-location = Search suggestions with location
|
||||
Search-suggestions-without-location = Search suggestions without location
|
||||
SEARCH-TAXA = SEARCH TAXA
|
||||
SEARCH-USERS = SEARCH USERS
|
||||
# Accessibility label for Explore button on MyObservations toolbar
|
||||
See-all-your-observations-in-explore = See all your observations in explore
|
||||
# Accessibility label for Observations button on UserProfile screen
|
||||
See-observations-by-this-user-in-Explore = See observations by this user in Explore
|
||||
# Accessibility label for Explore button on TaxonDetails screen
|
||||
@@ -1355,8 +1341,6 @@ View-suggestions = View suggestions
|
||||
Watch-your-notifications-for-identifications = Watch your notifications for identifications!
|
||||
We-are-not-confident-enough-to-make-a-top-ID-suggestion = We’re not confident enough to make a top ID suggestion, but here are some other suggestions:
|
||||
Welcome-back = Welcome back!
|
||||
# Welcome user back to app
|
||||
Welcome-user = <0>Welcome back,</0><1>{ $userHandle }</1>
|
||||
Weve-made-some-updates = We've made some updates, so we recommend taking a look at your settings. You can always update these later.
|
||||
WHAT-IS-INATURALIST = WHAT IS INATURALIST?
|
||||
Whats-more-by-recording = What's more, by recording and sharing your observations, you'll create research-quality data for scientists working to better understand and protect nature. So if you like recording your findings from the outdoors, or if you just like learning about life, join us!
|
||||
@@ -1414,12 +1398,6 @@ X-Observations =
|
||||
[one] 1 Observation
|
||||
*[other] { $count } Observations
|
||||
}
|
||||
# Shows number of observations in a variety of contexts
|
||||
X-observations =
|
||||
{ $count ->
|
||||
[one] 1 observation
|
||||
*[other] { $count } observations
|
||||
}
|
||||
# Label for a count of observations that appears above this text
|
||||
X-OBSERVATIONS--below-number =
|
||||
{ $count ->
|
||||
@@ -1527,8 +1505,6 @@ You-can-also-check-out-merchandise = You can also check out merchandise for iNat
|
||||
You-can-click-join-on-the-project-page = You can click “join” on the project page.
|
||||
You-can-find-answers-on-our-help-page = You can find answers on our help page.
|
||||
You-can-only-add-20-photos-per-observation = You can only add 20 photos per observation
|
||||
# Onboarding text on MyObservations: Onboarding text on MyObservations: 51-100 observations
|
||||
You-can-search-observations-of-any-plant-or-animal = You can search observations of any plant or animal anywhere in the world with Explore!
|
||||
You-can-still-share-the-file = You can still share the file with another app. If you can email it, please send it to { $email }
|
||||
You-can-upload-this-observation-to-our-community = You can upload this observation to our community to get an identification from a real person, and help our AI improve its identifications in the future
|
||||
You-changed-filters-will-be-discarded = You changed filters, but they were not applied to your explore search results.
|
||||
|
||||
@@ -16,7 +16,7 @@ import ExploreProjectSearch from "components/Explore/SearchScreens/ExploreProjec
|
||||
import ExploreTaxonSearch from "components/Explore/SearchScreens/ExploreTaxonSearch";
|
||||
import ExploreUserSearch from "components/Explore/SearchScreens/ExploreUserSearch";
|
||||
import Help from "components/Help/Help.tsx";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer.tsx";
|
||||
import Notifications from "components/Notifications/Notifications.tsx";
|
||||
import DQAContainer from "components/ObsDetails/DQAContainer";
|
||||
import ObsDetailsContainer from "components/ObsDetails/ObsDetailsContainer";
|
||||
|
||||
@@ -460,10 +460,29 @@ class Observation extends Realm.Object {
|
||||
}
|
||||
};
|
||||
|
||||
static markPendingDeletion( realm, uuidToDelete ) {
|
||||
const observation = realm.objectForPrimaryKey( "Observation", uuidToDelete );
|
||||
if ( observation ) {
|
||||
safeRealmWrite( realm, ( ) => {
|
||||
observation._pending_deletion = true;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
static clearPendingDeletion( realm, uuidToDelete ) {
|
||||
const observation = realm.objectForPrimaryKey( "Observation", uuidToDelete );
|
||||
if ( observation ) {
|
||||
safeRealmWrite( realm, ( ) => {
|
||||
observation._pending_deletion = false;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "Observation",
|
||||
primaryKey: "uuid",
|
||||
properties: {
|
||||
_pending_deletion: "bool?",
|
||||
// datetime the observation was created on the device
|
||||
_created_at: "date?",
|
||||
// datetime the observation was requested to be deleted
|
||||
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
User,
|
||||
Vote
|
||||
],
|
||||
schemaVersion: 64,
|
||||
schemaVersion: 66,
|
||||
path: `${RNFS.DocumentDirectoryPath}/db.realm`,
|
||||
// https://github.com/realm/realm-js/pull/6076 embedded constraints
|
||||
migrationOptions: {
|
||||
|
||||
@@ -31,7 +31,9 @@ const useLocalObservations = ( ): Object => {
|
||||
[["needs_sync", true], ["_created_at", true]]
|
||||
);
|
||||
|
||||
const obsNotFlaggedForDeletion = sortedCollection.filtered( "_deleted_at == nil" );
|
||||
// eslint-disable-next-line max-len
|
||||
const deletionFilters = "_deleted_at == nil OR _pending_deletion == false OR _pending_deletion == nil";
|
||||
const obsNotFlaggedForDeletion = sortedCollection.filtered( deletionFilters );
|
||||
stagedObservationList.current = [...obsNotFlaggedForDeletion];
|
||||
|
||||
const unsynced = Observation.filterUnsyncedObservations( realm );
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react-native";
|
||||
import { MS_BEFORE_TOOLBAR_RESET } from "components/MyObservations/hooks/useUploadObservations.ts";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer.tsx";
|
||||
import i18next from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { flatten } from "lodash";
|
||||
@@ -324,7 +324,7 @@ describe( "MyObservations", ( ) => {
|
||||
// expect( inatjs.observations.updates ).not.toHaveBeenCalled();
|
||||
inatjs.observations.updates.mockClear( );
|
||||
renderAppWithComponent( <MyObservationsContainer /> );
|
||||
expect( await screen.findByText( /Welcome back/ ) ).toBeTruthy();
|
||||
expect( await screen.findByText( /OBSERVATIONS/ ) ).toBeTruthy();
|
||||
await waitFor( ( ) => {
|
||||
expect( inatjs.observations.updates ).toHaveBeenCalled( );
|
||||
} );
|
||||
@@ -345,7 +345,7 @@ describe( "MyObservations", ( ) => {
|
||||
const realm = global.mockRealms[__filename];
|
||||
expect( realm.objects( "Observation" ).length ).toBeGreaterThan( 0 );
|
||||
renderAppWithComponent( <MyObservationsContainer /> );
|
||||
const button = await screen.findByTestId( "MyObservationsToolbar.toggleListView" );
|
||||
const button = await screen.findByTestId( "SegmentedButton.list" );
|
||||
fireEvent.press( button );
|
||||
// Awaiting the first observation because using await in the forEach errors out
|
||||
const firstObs = mockSyncedObservations[0];
|
||||
@@ -355,7 +355,13 @@ describe( "MyObservations", ( ) => {
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "displays observation status in list view", async () => {
|
||||
it( "displays observation status in list view in advanced mode", async () => {
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true
|
||||
} );
|
||||
const realm = global.mockRealms[__filename];
|
||||
expect( realm.objects( "Observation" ).length ).toBeGreaterThan( 0 );
|
||||
renderAppWithComponent( <MyObservationsContainer /> );
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { screen, waitFor } from "@testing-library/react-native";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer.tsx";
|
||||
// import inatjs from "inaturalistjs";
|
||||
import React from "react";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// remote data retrieval and local data persistence
|
||||
|
||||
import { screen, waitFor } from "@testing-library/react-native";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer.tsx";
|
||||
import React from "react";
|
||||
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
@@ -100,7 +100,7 @@ const actor = userEvent.setup( );
|
||||
async function navigateToObsDetails( ) {
|
||||
await waitFor( ( ) => {
|
||||
global.timeTravel( );
|
||||
expect( screen.getByText( /Welcome back/ ) ).toBeVisible( );
|
||||
expect( screen.getByText( /OBSERVATION/ ) ).toBeVisible( );
|
||||
} );
|
||||
const firstObservation = await screen.findByTestId(
|
||||
`ObsPressable.${mockObservations[0].uuid}`
|
||||
@@ -133,31 +133,6 @@ describe( "logged in", ( ) => {
|
||||
|
||||
global.withAnimatedTimeTravelEnabled( { skipFakeTimers: true } );
|
||||
|
||||
describe( "from MyObs toolbar", ( ) => {
|
||||
it( "should show observations view and navigate back to MyObs", async ( ) => {
|
||||
renderApp( );
|
||||
await waitFor( ( ) => {
|
||||
global.timeTravel( );
|
||||
expect( screen.getByText( /Welcome back/ ) ).toBeVisible( );
|
||||
} );
|
||||
const exploreButton = await screen.findByLabelText( /See all your observations in explore/ );
|
||||
await actor.press( exploreButton );
|
||||
expect( inatjs.observations.search ).toHaveBeenCalledWith( expect.objectContaining( {
|
||||
user_id: mockUser.id,
|
||||
verifiable: true
|
||||
} ), {
|
||||
api_token: TEST_JWT
|
||||
} );
|
||||
const defaultGlobalLocation = await screen.findByText( /Worldwide/ );
|
||||
expect( defaultGlobalLocation ).toBeVisible( );
|
||||
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
|
||||
expect( observationsViewIcon ).toBeVisible( );
|
||||
const backButton = screen.queryByTestId( "Explore.BackButton" );
|
||||
await actor.press( backButton );
|
||||
expect( await screen.findByText( /Welcome back/ ) ).toBeVisible( );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "from TaxonDetails", ( ) => {
|
||||
it( "should show observations view and navigate back to TaxonDetails", async ( ) => {
|
||||
renderApp( );
|
||||
|
||||
@@ -64,7 +64,7 @@ const navigateToObsEditViaGroupPhotos = async ( ) => {
|
||||
);
|
||||
await waitFor( ( ) => {
|
||||
global.timeTravel( );
|
||||
expect( screen.getByText( /Welcome back/ ) ).toBeVisible( );
|
||||
expect( screen.getByText( /OBSERVATIONS/ ) ).toBeVisible( );
|
||||
} );
|
||||
const tabBar = await screen.findByTestId( "CustomTabBar" );
|
||||
const addObsButton = await within( tabBar ).findByLabelText( "Add observations" );
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { fireEvent, screen, waitForElementToBeRemoved } from "@testing-library/react-native";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
|
||||
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer.tsx";
|
||||
import React from "react";
|
||||
import { measureRenders } from "reassure";
|
||||
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import MyObservations from "components/MyObservations/MyObservations";
|
||||
import MyObservationsSimple, { OBSERVATIONS_TAB }
|
||||
from "components/MyObservations/MyObservationsSimple.tsx";
|
||||
import React from "react";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
// import DeviceInfo from "react-native-device-info";
|
||||
import useDeviceOrientation from "sharedHooks/useDeviceOrientation.ts";
|
||||
import factory from "tests/factory";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
@@ -36,21 +37,21 @@ const DEVICE_ORIENTATION_PHONE_LANDSCAPE = {
|
||||
screenHeight: 393
|
||||
};
|
||||
|
||||
const DEVICE_ORIENTATION_TABLET_PORTRAIT = {
|
||||
deviceOrientation: "portrait",
|
||||
isTablet: true,
|
||||
isLandscapeMode: false,
|
||||
screenWidth: 820,
|
||||
screenHeight: 1180
|
||||
};
|
||||
// const DEVICE_ORIENTATION_TABLET_PORTRAIT = {
|
||||
// deviceOrientation: "portrait",
|
||||
// isTablet: true,
|
||||
// isLandscapeMode: false,
|
||||
// screenWidth: 820,
|
||||
// screenHeight: 1180
|
||||
// };
|
||||
|
||||
const DEVICE_ORIENTATION_TABLET_LANDSCAPE = {
|
||||
deviceOrientation: "landscapeLeft",
|
||||
isTablet: true,
|
||||
isLandscapeMode: true,
|
||||
screenWidth: 1180,
|
||||
screenHeight: 820
|
||||
};
|
||||
// const DEVICE_ORIENTATION_TABLET_LANDSCAPE = {
|
||||
// deviceOrientation: "landscapeLeft",
|
||||
// isTablet: true,
|
||||
// isLandscapeMode: true,
|
||||
// screenWidth: 1180,
|
||||
// screenHeight: 820
|
||||
// };
|
||||
|
||||
jest.mock( "sharedHooks/useDeviceOrientation.ts", ( ) => ( {
|
||||
__esModule: true,
|
||||
@@ -58,16 +59,17 @@ jest.mock( "sharedHooks/useDeviceOrientation.ts", ( ) => ( {
|
||||
} ) );
|
||||
|
||||
const renderMyObservations = layout => renderComponent(
|
||||
<MyObservations
|
||||
<MyObservationsSimple
|
||||
layout={layout}
|
||||
observations={mockObservations}
|
||||
onEndReached={jest.fn( )}
|
||||
toggleLayout={jest.fn( )}
|
||||
setShowLoginSheet={jest.fn( )}
|
||||
activeTab={OBSERVATIONS_TAB}
|
||||
/>
|
||||
);
|
||||
|
||||
describe( "MyObservations", () => {
|
||||
describe( "MyObservationsSimple", () => {
|
||||
beforeAll( async () => {
|
||||
jest.useFakeTimers( );
|
||||
} );
|
||||
@@ -115,17 +117,17 @@ describe( "MyObservations", () => {
|
||||
expect( list.props.numColumns ).toEqual( 2 );
|
||||
} );
|
||||
} );
|
||||
describe( "on a tablet", ( ) => {
|
||||
beforeEach( ( ) => {
|
||||
DeviceInfo.isTablet.mockReturnValue( true );
|
||||
} );
|
||||
it( "should have 4 columns", async ( ) => {
|
||||
useDeviceOrientation.mockImplementation( ( ) => DEVICE_ORIENTATION_TABLET_PORTRAIT );
|
||||
renderMyObservations( "grid" );
|
||||
const list = screen.getByTestId( "MyObservationsAnimatedList" );
|
||||
expect( list.props.numColumns ).toEqual( 4 );
|
||||
} );
|
||||
} );
|
||||
// describe( "on a tablet", ( ) => {
|
||||
// beforeEach( ( ) => {
|
||||
// DeviceInfo.isTablet.mockReturnValue( true );
|
||||
// } );
|
||||
// it( "should have 4 columns", async ( ) => {
|
||||
// useDeviceOrientation.mockImplementation( ( ) => DEVICE_ORIENTATION_TABLET_PORTRAIT );
|
||||
// renderMyObservations( "grid" );
|
||||
// const list = screen.getByTestId( "MyObservationsAnimatedList" );
|
||||
// expect( list.props.numColumns ).toEqual( 4 );
|
||||
// } );
|
||||
// } );
|
||||
} );
|
||||
describe( "landscape orientation", ( ) => {
|
||||
describe( "on a phone", ( ) => {
|
||||
@@ -136,17 +138,17 @@ describe( "MyObservations", () => {
|
||||
expect( list.props.numColumns ).toEqual( 2 );
|
||||
} );
|
||||
} );
|
||||
describe( "on a tablet", ( ) => {
|
||||
beforeEach( ( ) => {
|
||||
DeviceInfo.isTablet.mockReturnValue( true );
|
||||
} );
|
||||
it( "should have 6 columns", async ( ) => {
|
||||
useDeviceOrientation.mockImplementation( ( ) => DEVICE_ORIENTATION_TABLET_LANDSCAPE );
|
||||
renderMyObservations( "grid" );
|
||||
const list = screen.getByTestId( "MyObservationsAnimatedList" );
|
||||
expect( list.props.numColumns ).toEqual( 6 );
|
||||
} );
|
||||
} );
|
||||
// describe( "on a tablet", ( ) => {
|
||||
// beforeEach( ( ) => {
|
||||
// DeviceInfo.isTablet.mockReturnValue( true );
|
||||
// } );
|
||||
// it( "should have 6 columns", async ( ) => {
|
||||
// useDeviceOrientation.mockImplementation( ( ) => DEVICE_ORIENTATION_TABLET_LANDSCAPE );
|
||||
// renderMyObservations( "grid" );
|
||||
// const list = screen.getByTestId( "MyObservationsAnimatedList" );
|
||||
// expect( list.props.numColumns ).toEqual( 6 );
|
||||
// } );
|
||||
// } );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -1,5 +1,5 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import ToolbarContainer from "components/MyObservations/ToolbarContainer";
|
||||
import SimpleUploadBannerContainer from "components/MyObservations/SimpleUploadBannerContainer";
|
||||
import React from "react";
|
||||
import {
|
||||
MANUAL_SYNC_IN_PROGRESS,
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
UPLOAD_IN_PROGRESS,
|
||||
UPLOAD_PENDING
|
||||
} from "stores/createUploadObservationsSlice.ts";
|
||||
import useStore from "stores/useStore";
|
||||
import useStore, { zustandStorage } from "stores/useStore";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
const initialStoreState = useStore.getState( );
|
||||
const mockUser = {};
|
||||
|
||||
const deletionStore = {
|
||||
currentDeleteCount: 1,
|
||||
@@ -26,18 +26,18 @@ beforeAll( ( ) => {
|
||||
jest.useFakeTimers( );
|
||||
} );
|
||||
|
||||
describe( "Toolbar Container", () => {
|
||||
beforeEach( ( ) => {
|
||||
useStore.setState( initialStoreState, true );
|
||||
} );
|
||||
|
||||
describe( "SimpleUploadBannerContainer", () => {
|
||||
it( "displays syncing text before beginning uploads when sync button tapped", ( ) => {
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
numUnuploadedObservations: 1,
|
||||
uploadStatus: UPLOAD_PENDING,
|
||||
syncingStatus: MANUAL_SYNC_IN_PROGRESS
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent( <SimpleUploadBannerContainer currentUser={mockUser} /> );
|
||||
|
||||
const statusText = screen.getByText( /Syncing.../ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
@@ -45,11 +45,19 @@ describe( "Toolbar Container", () => {
|
||||
|
||||
it( "displays a pending upload", ( ) => {
|
||||
useStore.setState( {
|
||||
numUnuploadedObservations: 1,
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
uploadStatus: UPLOAD_PENDING,
|
||||
syncingStatus: SYNC_PENDING
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent(
|
||||
<SimpleUploadBannerContainer
|
||||
numUploadableObservations={1}
|
||||
currentUser={mockUser}
|
||||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText( /Upload 1 observation/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
@@ -57,12 +65,16 @@ describe( "Toolbar Container", () => {
|
||||
|
||||
it( "displays an upload in progress", ( ) => {
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
initialNumObservationsInQueue: 1,
|
||||
numUploadsAttempted: 1,
|
||||
uploadStatus: UPLOAD_IN_PROGRESS,
|
||||
syncingStatus: SYNC_PENDING
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent( <SimpleUploadBannerContainer currentUser={mockUser} /> );
|
||||
|
||||
const statusText = screen.getByText( /Uploading 1 observation/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
@@ -71,34 +83,36 @@ describe( "Toolbar Container", () => {
|
||||
it( "displays a completed upload", () => {
|
||||
const numUploadsAttempted = 1;
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
numUploadsAttempted,
|
||||
uploadStatus: UPLOAD_COMPLETE,
|
||||
syncingStatus: SYNC_PENDING,
|
||||
initialNumObservationsInQueue: numUploadsAttempted
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent( <SimpleUploadBannerContainer currentUser={mockUser} /> );
|
||||
|
||||
const statusText = screen.getByText( /1 observation uploaded/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "displays an upload error", () => {
|
||||
const multiError = "Couldn't complete upload";
|
||||
useStore.setState( {
|
||||
multiError,
|
||||
syncingStatus: SYNC_PENDING
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
expect( screen.getByText( multiError ) ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "displays multiple pending uploads", () => {
|
||||
useStore.setState( {
|
||||
numUnuploadedObservations: 4,
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
uploadStatus: UPLOAD_PENDING,
|
||||
syncingStatus: SYNC_PENDING
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent(
|
||||
<SimpleUploadBannerContainer
|
||||
numUploadableObservations={4}
|
||||
currentUser={mockUser}
|
||||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText( /Upload 4 observations/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
@@ -106,12 +120,16 @@ describe( "Toolbar Container", () => {
|
||||
|
||||
it( "displays multiple uploads in progress", () => {
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
uploadStatus: UPLOAD_IN_PROGRESS,
|
||||
numUploadsAttempted: 2,
|
||||
syncingStatus: SYNC_PENDING,
|
||||
initialNumObservationsInQueue: 5
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent( <SimpleUploadBannerContainer currentUser={mockUser} /> );
|
||||
|
||||
const statusText = screen.getByText( /Uploading 2 of 5 observations/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
@@ -120,12 +138,16 @@ describe( "Toolbar Container", () => {
|
||||
it( "displays multiple completed uploads", () => {
|
||||
const numUploadsAttempted = 7;
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
numUploadsAttempted,
|
||||
uploadStatus: UPLOAD_COMPLETE,
|
||||
syncingStatus: SYNC_PENDING,
|
||||
initialNumObservationsInQueue: numUploadsAttempted
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent( <SimpleUploadBannerContainer currentUser={mockUser} /> );
|
||||
|
||||
const statusText = screen.getByText( /7 observations uploaded/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
@@ -141,7 +163,7 @@ describe( "Toolbar Container", () => {
|
||||
// ...deletionStore,
|
||||
// syncingStatus: HANDLING_LOCAL_DELETIONS
|
||||
// } );
|
||||
// renderComponent( <ToolbarContainer /> );
|
||||
// renderComponent( <SimpleUploadBannerContainer /> );
|
||||
|
||||
// const statusText = screen.getByText( /Deleting 1 of 1 observation/ );
|
||||
// expect( statusText ).toBeVisible( );
|
||||
@@ -149,12 +171,16 @@ describe( "Toolbar Container", () => {
|
||||
|
||||
it( "displays deletions completed", () => {
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
...deletionStore,
|
||||
currentDeleteCount: 1,
|
||||
deleteQueue: [{}],
|
||||
initialNumDeletionsInQueue: 1
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent( <SimpleUploadBannerContainer currentUser={mockUser} /> );
|
||||
|
||||
const statusText = screen.getByText( /1 observation deleted/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
@@ -163,11 +189,15 @@ describe( "Toolbar Container", () => {
|
||||
it( "displays deletion error", ( ) => {
|
||||
const deleteError = "Unknown problem deleting observations";
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
...deletionStore,
|
||||
deleteError,
|
||||
initialNumDeletionsInQueue: 2
|
||||
} );
|
||||
renderComponent( <ToolbarContainer /> );
|
||||
renderComponent( <SimpleUploadBannerContainer currentUser={mockUser} /> );
|
||||
|
||||
const deletingText = screen.getByText( /Deleting/ );
|
||||
expect( deletingText ).toBeVisible( );
|
||||
@@ -175,4 +205,47 @@ describe( "Toolbar Container", () => {
|
||||
const statusText = screen.getByText( deleteError );
|
||||
expect( statusText ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should hide banner if logged out and only one observation", ( ) => {
|
||||
zustandStorage.setItem( "numOfUserObservations", 1 );
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
uploadStatus: UPLOAD_PENDING,
|
||||
syncingStatus: SYNC_PENDING,
|
||||
numOfUserObservations: 1
|
||||
} );
|
||||
renderComponent(
|
||||
<SimpleUploadBannerContainer
|
||||
numUploadableObservations={1}
|
||||
currentUser={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.queryByText( /Upload 1 observation/ );
|
||||
expect( statusText ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it( "should show banner if logged out with more than one observation", ( ) => {
|
||||
zustandStorage.setItem( "numOfUserObservations", 2 );
|
||||
useStore.setState( {
|
||||
layout: {
|
||||
isDefaultMode: false
|
||||
},
|
||||
isAdvancedUser: true,
|
||||
uploadStatus: UPLOAD_PENDING,
|
||||
syncingStatus: SYNC_PENDING
|
||||
} );
|
||||
renderComponent(
|
||||
<SimpleUploadBannerContainer
|
||||
numUploadableObservations={1}
|
||||
currentUser={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText( /Upload 1 observation/ );
|
||||
expect( statusText ).toBeVisible( );
|
||||
} );
|
||||
} );
|
||||
@@ -50,11 +50,11 @@ const mockRemoteObservation = factory( "RemoteObservation", {
|
||||
taxon: factory.states( "genus" )( "RemoteTaxon" )
|
||||
} );
|
||||
|
||||
const mockMutate = jest.fn();
|
||||
const mockMutateAsync = jest.fn();
|
||||
jest.mock( "sharedHooks/useAuthenticatedMutation", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
mutate: mockMutate
|
||||
mutateAsync: mockMutateAsync
|
||||
} )
|
||||
} ) );
|
||||
|
||||
@@ -140,7 +140,7 @@ describe( "automatic sync while user is logged out", ( ) => {
|
||||
await waitFor( ( ) => {
|
||||
expect( deletedObs ).toBeFalsy( );
|
||||
} );
|
||||
expect( mockMutate ).not.toHaveBeenCalled( );
|
||||
expect( mockMutateAsync ).not.toHaveBeenCalled( );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -176,7 +176,7 @@ describe( "automatic sync while user is logged in", ( ) => {
|
||||
renderHook( ( ) => useSyncObservations( currentUserId ) );
|
||||
|
||||
await waitFor( ( ) => {
|
||||
expect( mockMutate ).not.toHaveBeenCalled( );
|
||||
expect( mockMutateAsync ).not.toHaveBeenCalled( );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -195,7 +195,7 @@ describe( "automatic sync while user is logged in", ( ) => {
|
||||
renderHook( ( ) => useSyncObservations( currentUserId ) );
|
||||
|
||||
await waitFor( ( ) => {
|
||||
expect( mockMutate )
|
||||
expect( mockMutateAsync )
|
||||
.toHaveBeenCalledWith( { uuid: syncedObservations[0].uuid } );
|
||||
} );
|
||||
} );
|
||||
|
||||
Reference in New Issue
Block a user