mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-06-18 12:41:54 -04:00
Merge pull request #3723 from inaturalist/mob-1460-new-myobs-screen-state-foundation
Mob 1460 new myobs screen state foundation
This commit is contained in:
@@ -5,6 +5,11 @@ import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import type { FlashListRef } from "@shopify/flash-list";
|
||||
import { fetchSpeciesCounts } from "api/observations";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import {
|
||||
MY_OBSERVATIONS_ACTION,
|
||||
MyObservationsProvider,
|
||||
useMyObservations,
|
||||
} from "providers/MyObservationsContext";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -16,9 +21,9 @@ import { Alert } from "react-native";
|
||||
import Observation from "realmModels/Observation";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
import type { RealmObservation } from "realmModels/types";
|
||||
import type { SPECIES_SORT } from "sharedHelpers/speciesSort";
|
||||
import {
|
||||
sortSpeciesCounts,
|
||||
SPECIES_SORT,
|
||||
speciesSortToApiParams,
|
||||
} from "sharedHelpers/speciesSort";
|
||||
import startupPerformanceTracker from "sharedHelpers/startupPerformanceTracker";
|
||||
@@ -62,7 +67,7 @@ interface SyncOptions {
|
||||
skipSomeUploads?: string[];
|
||||
}
|
||||
|
||||
const MyObservationsContainer = ( ) => {
|
||||
const MyObservationsWithProvider = ( ) => {
|
||||
const { isDefaultMode, loggedInWhileInDefaultMode } = useLayoutPrefs();
|
||||
const { t } = useTranslation( );
|
||||
const realm = useRealm( );
|
||||
@@ -71,6 +76,8 @@ const MyObservationsContainer = ( ) => {
|
||||
const taxaListRef = useRef<FlashListRef<SpeciesCount>>( null );
|
||||
const navigateToObsEdit = useNavigateToObsEdit( );
|
||||
|
||||
const { state: myObsState, dispatch: myObsDispatch } = useMyObservations( );
|
||||
|
||||
const setStartUploadObservations = useStore( state => state.setStartUploadObservations );
|
||||
const uploadQueue = useStore( state => state.uploadQueue );
|
||||
const addToUploadQueue = useStore( state => state.addToUploadQueue );
|
||||
@@ -126,8 +133,12 @@ const MyObservationsContainer = ( ) => {
|
||||
|
||||
const [openSheet, setOpenSheet] = useState<ACTIVE_SHEET>( ACTIVE_SHEET.NONE );
|
||||
|
||||
const [speciesSortOptionId, setSpeciesSortOptionId]
|
||||
= useState<SPECIES_SORT>( SPECIES_SORT.COUNT_DESC );
|
||||
const setSpeciesSortOptionId = ( value: SPECIES_SORT ) => {
|
||||
myObsDispatch( {
|
||||
type: MY_OBSERVATIONS_ACTION.SET_SPECIES_SORT,
|
||||
speciesSort: value,
|
||||
} );
|
||||
};
|
||||
|
||||
const toggleLayout = ( ) => {
|
||||
writeLayoutToStorage( layout === "grid"
|
||||
@@ -322,8 +333,8 @@ const MyObservationsContainer = ( ) => {
|
||||
|
||||
// Map the selected sort option to API params
|
||||
const sortAPIParams = useMemo(
|
||||
() => speciesSortToApiParams( speciesSortOptionId ),
|
||||
[speciesSortOptionId],
|
||||
() => speciesSortToApiParams( myObsState.speciesSort ),
|
||||
[myObsState.speciesSort],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -333,7 +344,7 @@ const MyObservationsContainer = ( ) => {
|
||||
totalResults: numTotalTaxaRemote,
|
||||
refetch: refetchTaxa,
|
||||
} = useInfiniteScroll(
|
||||
`MyObsSimple-fetchSpeciesCounts-${currentUser?.id}-${speciesSortOptionId}`,
|
||||
`MyObsSimple-fetchSpeciesCounts-${currentUser?.id}-${myObsState.speciesSort}`,
|
||||
fetchSpeciesCounts,
|
||||
{
|
||||
user_id: currentUser?.id,
|
||||
@@ -394,13 +405,13 @@ const MyObservationsContainer = ( ) => {
|
||||
}
|
||||
|
||||
// For logged-out users: apply client-side sorting to local data
|
||||
return sortSpeciesCounts( unsortedTaxa || [], speciesSortOptionId );
|
||||
return sortSpeciesCounts( unsortedTaxa || [], myObsState.speciesSort );
|
||||
}, [
|
||||
currentUser,
|
||||
isConnected,
|
||||
remoteObservedTaxaCounts,
|
||||
localObservedSpeciesCount,
|
||||
speciesSortOptionId,
|
||||
myObsState.speciesSort,
|
||||
] );
|
||||
|
||||
if ( !layout ) { return null; }
|
||||
@@ -449,11 +460,17 @@ const MyObservationsContainer = ( ) => {
|
||||
setOpenSheet={setOpenSheet}
|
||||
setSpeciesSortOptionId={setSpeciesSortOptionId}
|
||||
showNoResults={showNoResults}
|
||||
speciesSortOptionId={speciesSortOptionId}
|
||||
speciesSortOptionId={myObsState.speciesSort}
|
||||
taxa={taxa}
|
||||
toggleLayout={toggleLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MyObservationsContainer = ( ) => (
|
||||
<MyObservationsProvider>
|
||||
<MyObservationsWithProvider />
|
||||
</MyObservationsProvider>
|
||||
);
|
||||
|
||||
export default MyObservationsContainer;
|
||||
|
||||
@@ -67,7 +67,7 @@ interface Props {
|
||||
openSheet: ACTIVE_SHEET;
|
||||
setActiveTab: ( newTab: string ) => void;
|
||||
setOpenSheet: ( value: ACTIVE_SHEET ) => void;
|
||||
setSpeciesSortOptionId: React.Dispatch<React.SetStateAction<SPECIES_SORT>>;
|
||||
setSpeciesSortOptionId: ( value: SPECIES_SORT ) => void;
|
||||
showNoResults: boolean;
|
||||
speciesSortOptionId: SPECIES_SORT;
|
||||
taxa?: SpeciesCount[];
|
||||
|
||||
102
src/providers/MyObservationsContext.tsx
Normal file
102
src/providers/MyObservationsContext.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import * as React from "react";
|
||||
import { OBSERVATIONS_SORT } from "sharedHelpers/observationsSort";
|
||||
import { SPECIES_SORT } from "sharedHelpers/speciesSort";
|
||||
|
||||
export enum MY_OBSERVATIONS_ACTION {
|
||||
SET_OBSERVATIONS_SORT = "SET_OBSERVATIONS_SORT",
|
||||
SET_SPECIES_SORT = "SET_SPECIES_SORT",
|
||||
SET_TAXON_SEARCH = "SET_TAXON_SEARCH",
|
||||
CLEAR_TAXON_SEARCH = "CLEAR_TAXON_SEARCH",
|
||||
}
|
||||
|
||||
export interface MyObservationsTaxon {
|
||||
id: number;
|
||||
name: string;
|
||||
preferred_common_name?: string;
|
||||
}
|
||||
|
||||
export interface MyObservationsState {
|
||||
observationsSort: OBSERVATIONS_SORT;
|
||||
speciesSort: SPECIES_SORT;
|
||||
searchedTaxon: MyObservationsTaxon | null;
|
||||
}
|
||||
|
||||
export type MyObservationsAction =
|
||||
| {
|
||||
type: MY_OBSERVATIONS_ACTION.SET_OBSERVATIONS_SORT;
|
||||
observationsSort: OBSERVATIONS_SORT;
|
||||
}
|
||||
| {
|
||||
type: MY_OBSERVATIONS_ACTION.SET_SPECIES_SORT;
|
||||
speciesSort: SPECIES_SORT;
|
||||
}
|
||||
| {
|
||||
type: MY_OBSERVATIONS_ACTION.SET_TAXON_SEARCH;
|
||||
searchTaxon: MyObservationsTaxon;
|
||||
}
|
||||
| { type: MY_OBSERVATIONS_ACTION.CLEAR_TAXON_SEARCH };
|
||||
|
||||
export const initialMyObservationsState: MyObservationsState = {
|
||||
observationsSort: OBSERVATIONS_SORT.DATE_UPLOADED_NEWEST,
|
||||
speciesSort: SPECIES_SORT.COUNT_DESC,
|
||||
searchedTaxon: null,
|
||||
};
|
||||
|
||||
export function myObservationsReducer(
|
||||
state: MyObservationsState,
|
||||
action: MyObservationsAction,
|
||||
): MyObservationsState {
|
||||
switch ( action.type ) {
|
||||
case MY_OBSERVATIONS_ACTION.SET_OBSERVATIONS_SORT:
|
||||
return { ...state, observationsSort: action.observationsSort };
|
||||
case MY_OBSERVATIONS_ACTION.SET_SPECIES_SORT:
|
||||
return { ...state, speciesSort: action.speciesSort };
|
||||
case MY_OBSERVATIONS_ACTION.SET_TAXON_SEARCH:
|
||||
return { ...state, searchedTaxon: action.searchTaxon };
|
||||
case MY_OBSERVATIONS_ACTION.CLEAR_TAXON_SEARCH:
|
||||
return { ...state, searchedTaxon: null };
|
||||
default: {
|
||||
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
|
||||
const _exhaustive: never = action;
|
||||
return _exhaustive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface MyObservationsContextValue {
|
||||
state: MyObservationsState;
|
||||
dispatch: ( action: MyObservationsAction ) => void;
|
||||
}
|
||||
|
||||
const MyObservationsContext = React.createContext<
|
||||
MyObservationsContextValue | undefined
|
||||
>( undefined );
|
||||
|
||||
export const MyObservationsProvider = ( {
|
||||
children,
|
||||
}: React.PropsWithChildren ) => {
|
||||
const [state, dispatch] = React.useReducer(
|
||||
myObservationsReducer,
|
||||
initialMyObservationsState,
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ( { state, dispatch } ),
|
||||
[state],
|
||||
);
|
||||
|
||||
return (
|
||||
<MyObservationsContext value={value}>
|
||||
{children}
|
||||
</MyObservationsContext>
|
||||
);
|
||||
};
|
||||
|
||||
export function useMyObservations( ): MyObservationsContextValue {
|
||||
const context = React.useContext( MyObservationsContext );
|
||||
// Pattern from https://kentcdodds.com/blog/how-to-use-react-context-effectively
|
||||
if ( context === undefined ) {
|
||||
throw new Error( "useMyObservations must be used within a MyObservationsProvider" );
|
||||
}
|
||||
return context;
|
||||
}
|
||||
12
tests/unit/providers/MyObservationsContext.test.js
Normal file
12
tests/unit/providers/MyObservationsContext.test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { initialMyObservationsState } from "providers/MyObservationsContext";
|
||||
import { OBSERVATIONS_SORT } from "sharedHelpers/observationsSort";
|
||||
import { SPECIES_SORT } from "sharedHelpers/speciesSort";
|
||||
|
||||
describe( "initialMyObservationsState", ( ) => {
|
||||
it( "starts with obs sorted by date uploaded (newest), species sort desc, and no taxon", ( ) => {
|
||||
expect( initialMyObservationsState.observationsSort )
|
||||
.toBe( OBSERVATIONS_SORT.DATE_UPLOADED_NEWEST );
|
||||
expect( initialMyObservationsState.speciesSort ).toBe( SPECIES_SORT.COUNT_DESC );
|
||||
expect( initialMyObservationsState.searchedTaxon ).toBeNull( );
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user