From f5027dff27fc2a1ddfdcab45ea1e7eac38b5da3f Mon Sep 17 00:00:00 2001 From: Abbey Campbell Date: Fri, 17 Jul 2026 15:46:45 -0700 Subject: [PATCH] handle local sort for logged out users --- .../MyObservations/MyObservationsResults.tsx | 6 +- .../hooks/useMyObservationsQuery.ts | 61 ++++++++++++------- src/sharedHelpers/observationsSort.ts | 19 ++++++ src/sharedHooks/useLocalObservationIds.ts | 16 +++-- 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/components/MyObservations/MyObservationsResults.tsx b/src/components/MyObservations/MyObservationsResults.tsx index 0b96b19fb..acbf48519 100644 --- a/src/components/MyObservations/MyObservationsResults.tsx +++ b/src/components/MyObservations/MyObservationsResults.tsx @@ -98,7 +98,7 @@ const MyObservationsResults = ( ) => { const localObservationIds = useLocalObservationIds(); const sortMyObservationsEnabled = useFeatureFlag( FeatureFlag.SortMyObservationsEnabled ); const { - observationIds: serverOrderedObservationIds, + observationIds: queryObservationIds, isServerAuthoritative, isFetchingNextPage: isFetchingNextPageFromQuery, fetchNextPage: fetchNextPageFromQuery, @@ -106,8 +106,8 @@ const MyObservationsResults = ( ) => { } = useMyObservationsQuery( ); // Only use server-ordered list when the flag is on and the selected sort requires it const useServerOrder = sortMyObservationsEnabled && isServerAuthoritative; - const observationIds = useServerOrder - ? serverOrderedObservationIds + const observationIds = sortMyObservationsEnabled + ? queryObservationIds : localObservationIds; const { numUnuploadedObservations, diff --git a/src/components/MyObservations/hooks/useMyObservationsQuery.ts b/src/components/MyObservations/hooks/useMyObservationsQuery.ts index bbb8f9da4..0a1775564 100644 --- a/src/components/MyObservations/hooks/useMyObservationsQuery.ts +++ b/src/components/MyObservations/hooks/useMyObservationsQuery.ts @@ -2,6 +2,7 @@ import { RealmContext } from "providers/contexts"; import { useMyObservations } from "providers/MyObservationsContext"; import { useMemo } from "react"; import { OBSERVATIONS_SORT } from "sharedHelpers/observationsSort"; +import { useCurrentUser } from "sharedHooks"; import useLocalObservationIds from "sharedHooks/useLocalObservationIds"; import useServerOrderedObservations from "./useServerOrderedObservations"; @@ -25,12 +26,22 @@ interface UseMyObservationsQueryResult { // and interact with their obs offline. This hook uses selected sort to determine whether Realm or // the server should be the authoritative source of a user's observations (unsynced obs // are always merged in at the top regardless of source). +// +// Logged-out users can never have server-ordered observations, since they can't upload until +// they log in, so a non-default sort is applied to their local observations instead. const useMyObservationsQuery = ( ): UseMyObservationsQueryResult => { const { state } = useMyObservations( ); + const currentUser = useCurrentUser( ); const isDefaultSort = state.observationsSort === OBSERVATIONS_SORT.DATE_UPLOADED_NEWEST; + const sortLocally = !isDefaultSort && !currentUser; + const isServerAuthoritative = !isDefaultSort && !!currentUser; - const localObservationIds = useLocalObservationIds( ); + const localObservationIds = useLocalObservationIds( + sortLocally + ? state.observationsSort + : undefined, + ); const { observationIds: serverObservationIds, @@ -41,7 +52,7 @@ const useMyObservationsQuery = ( ): UseMyObservationsQueryResult => { refetch, } = useServerOrderedObservations( { sortBy: state.observationsSort, - enabled: !isDefaultSort, + enabled: isServerAuthoritative, } ); // if we want obs from the server, we'll want to prepend local, unsynced obs to the top @@ -66,34 +77,40 @@ const useMyObservationsQuery = ( ): UseMyObservationsQueryResult => { // dedupe in case any locally unsynced obs also exist in the server results const observationIds = useMemo( ( ) => { - if ( isDefaultSort ) return localObservationIds; + if ( isDefaultSort || sortLocally ) return localObservationIds; const unsyncedUuids = new Set( unsyncedObservationIds.map( o => o.uuid ) ); return [ ...unsyncedObservationIds, ...serverObservationIds.filter( o => !unsyncedUuids.has( o.uuid ) ), ]; - }, [isDefaultSort, localObservationIds, unsyncedObservationIds, serverObservationIds] ); + }, [ + isDefaultSort, + sortLocally, + localObservationIds, + unsyncedObservationIds, + serverObservationIds, + ] ); return { observationIds, - isServerAuthoritative: !isDefaultSort, - isLoading: isDefaultSort - ? false - : isLoading, - isFetchingNextPage: isDefaultSort - ? false - : isFetchingNextPage, - error: isDefaultSort - ? null - : error, - // since we never fetched for default sort, we don't need to refetch or paginate. - // pagination is still handled by useInfiniteObservationsScroll - refetch: isDefaultSort - ? NOOP_REFETCH - : refetch, - fetchNextPage: isDefaultSort - ? NOOP_FETCH_NEXT_PAGE - : fetchNextPage, + isServerAuthoritative, + isLoading: isServerAuthoritative + ? isLoading + : false, + isFetchingNextPage: isServerAuthoritative + ? isFetchingNextPage + : false, + error: isServerAuthoritative + ? error + : null, + // pagination only applies to the server-authoritative case; the default sort is handled by + // useInfiniteObservationsScroll, and locally-sorted data for logged-out users loads from Realm + refetch: isServerAuthoritative + ? refetch + : NOOP_REFETCH, + fetchNextPage: isServerAuthoritative + ? fetchNextPage + : NOOP_FETCH_NEXT_PAGE, }; }; diff --git a/src/sharedHelpers/observationsSort.ts b/src/sharedHelpers/observationsSort.ts index 9bf2eee76..c7d38644f 100644 --- a/src/sharedHelpers/observationsSort.ts +++ b/src/sharedHelpers/observationsSort.ts @@ -36,6 +36,25 @@ export function observationSortToApiParams( sort: OBSERVATIONS_SORT ): Observati return OBSERVATIONS_SORT_TO_API_PARAMS[sort]; } +// [property, reverse] tuple, matching Realm's .sorted() argument shape +type ObservationRealmSort = [string, boolean]; + +const OBSERVATIONS_SORT_TO_REALM_SORT: Record = { + [OBSERVATIONS_SORT.DATE_UPLOADED_NEWEST]: ["_created_at", true], + [OBSERVATIONS_SORT.DATE_UPLOADED_OLDEST]: ["_created_at", false], + // observed_on (a real date) is only populated once an observation has been uploaded and the + // server computes it -- local-only observations only ever have the + // observed_on_string set, so local sorting needs to use that field instead + [OBSERVATIONS_SORT.DATE_OBSERVED_NEWEST]: ["observed_on_string", true], + [OBSERVATIONS_SORT.DATE_OBSERVED_OLDEST]: ["observed_on_string", false], +}; + +// For sorting local, unsynced observations directly in a Realm query -- used for logged-out +// users, who can never have server-ordered observations +export function observationSortToRealmSort( sort: OBSERVATIONS_SORT ): ObservationRealmSort { + return OBSERVATIONS_SORT_TO_REALM_SORT[sort]; +} + export function useObservationsSortLabels( ): Record { const { t } = useTranslation( ); return { diff --git a/src/sharedHooks/useLocalObservationIds.ts b/src/sharedHooks/useLocalObservationIds.ts index a5e285d22..7aad3fa23 100644 --- a/src/sharedHooks/useLocalObservationIds.ts +++ b/src/sharedHooks/useLocalObservationIds.ts @@ -1,19 +1,27 @@ import { RealmContext } from "providers/contexts"; import { useMemo } from "react"; import Observation from "realmModels/Observation"; +import type { OBSERVATIONS_SORT } from "sharedHelpers/observationsSort"; +import { observationSortToRealmSort } from "sharedHelpers/observationsSort"; const { useQuery } = RealmContext; -const useLocalObservationIds = ( ) => { +const useLocalObservationIds = ( sortBy?: OBSERVATIONS_SORT ) => { const unsyncedObs = useQuery( { type: Observation, query: observations => observations .filtered( "_deleted_at == nil OR _pending_deletion == false OR _pending_deletion == nil" ) - .sorted( [["needs_sync", true], ["_created_at", true]] ), - keyPaths: ["uuid"], + .sorted( sortBy + ? [observationSortToRealmSort( sortBy )] + : [["needs_sync", true], ["_created_at", true]] ), + // widening this beyond uuid means more frequent re-renders for every consumer, + // so we only do it when we need a local sort applied to the results + keyPaths: sortBy + ? ["uuid", "_created_at", "observed_on_string"] + : ["uuid"], }, - [], + [sortBy], ); return useMemo(