handle local sort for logged out users

This commit is contained in:
Abbey Campbell
2026-07-17 15:46:45 -07:00
parent a38414a0e5
commit f5027dff27
4 changed files with 73 additions and 29 deletions

View File

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

View File

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

View File

@@ -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, ObservationRealmSort> = {
[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<OBSERVATIONS_SORT, ObservationSortLabels> {
const { t } = useTranslation( );
return {

View File

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