diff --git a/src/components/MyObservations/MyObservationsContainer.tsx b/src/components/MyObservations/MyObservationsContainer.tsx index 37d7ad075..49d64b510 100644 --- a/src/components/MyObservations/MyObservationsContainer.tsx +++ b/src/components/MyObservations/MyObservationsContainer.tsx @@ -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>( 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.NONE ); - const [speciesSortOptionId, setSpeciesSortOptionId] - = useState( 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 = ( ) => ( + + + +); + export default MyObservationsContainer; diff --git a/src/components/MyObservations/MyObservationsSimple.tsx b/src/components/MyObservations/MyObservationsSimple.tsx index 970b83d5b..630c4653e 100644 --- a/src/components/MyObservations/MyObservationsSimple.tsx +++ b/src/components/MyObservations/MyObservationsSimple.tsx @@ -67,7 +67,7 @@ interface Props { openSheet: ACTIVE_SHEET; setActiveTab: ( newTab: string ) => void; setOpenSheet: ( value: ACTIVE_SHEET ) => void; - setSpeciesSortOptionId: React.Dispatch>; + setSpeciesSortOptionId: ( value: SPECIES_SORT ) => void; showNoResults: boolean; speciesSortOptionId: SPECIES_SORT; taxa?: SpeciesCount[]; diff --git a/src/providers/MyObservationsContext.tsx b/src/providers/MyObservationsContext.tsx new file mode 100644 index 000000000..8ccad9dce --- /dev/null +++ b/src/providers/MyObservationsContext.tsx @@ -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 ( + + {children} + + ); +}; + +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; +} diff --git a/tests/unit/providers/MyObservationsContext.test.js b/tests/unit/providers/MyObservationsContext.test.js new file mode 100644 index 000000000..778c6db09 --- /dev/null +++ b/tests/unit/providers/MyObservationsContext.test.js @@ -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( ); + } ); +} );