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:
Abbey Campbell
2026-06-16 13:03:05 -07:00
committed by GitHub
4 changed files with 142 additions and 11 deletions

View File

@@ -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;

View File

@@ -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[];

View 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;
}

View 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( );
} );
} );