Merge pull request #3356 from inaturalist/mob-187-notifications-should-time-out-when-theres-low-connectivity

show offline/retry state for notifications when request takes too long
This commit is contained in:
Abbey Campbell
2026-02-13 14:10:29 -08:00
committed by GitHub
4 changed files with 75 additions and 14 deletions

View File

@@ -14,6 +14,9 @@ import NotificationsTab, {
OWNER_TAB,
} from "./NotificationsTab";
const OWNER_TAB_PARAMS = { observations_by: "owner" } as const;
const FOLLOWING_TAB_PARAMS = { observations_by: "following" } as const;
const Notifications = ( ) => {
const [activeTab, setActiveTab] = useState<typeof OWNER_TAB | typeof OTHER_TAB>( OWNER_TAB );
const { t } = useTranslation();
@@ -44,14 +47,14 @@ const Notifications = ( ) => {
{activeTab === OWNER_TAB && (
<NotificationsContainer
currentUser={currentUser}
notificationParams={{ observations_by: "owner" }}
notificationParams={OWNER_TAB_PARAMS}
onRefresh={( ) => EventRegister.emit( NOTIFICATIONS_REFRESHED, OWNER_TAB )}
/>
)}
{activeTab === OTHER_TAB && (
<NotificationsContainer
currentUser={currentUser}
notificationParams={{ observations_by: "following" }}
notificationParams={FOLLOWING_TAB_PARAMS}
onRefresh={( ) => EventRegister.emit( NOTIFICATIONS_REFRESHED, OTHER_TAB )}
/>
)}

View File

@@ -32,6 +32,7 @@ const NotificationsContainer = ( {
isError,
isFetching,
isInitialLoading,
loadingTimedOut,
notifications,
refetch,
} = useInfiniteNotificationsScroll( notificationParams );
@@ -69,6 +70,7 @@ const NotificationsContainer = ( {
isFetching={isFetching}
isInitialLoading={isInitialLoading}
isConnected={isConnected}
loadingTimedOut={loadingTimedOut}
onEndReached={fetchNextPage}
reload={refetch}
refreshing={refreshing}

View File

@@ -20,6 +20,7 @@ interface Props {
isFetching?: boolean;
isInitialLoading?: boolean;
isConnected: boolean | null;
loadingTimedOut: boolean;
onEndReached: ( ) => void;
onRefresh: ( ) => void;
refreshing: boolean;
@@ -39,6 +40,7 @@ const NotificationsList = ( {
isFetching,
isInitialLoading,
isConnected,
loadingTimedOut,
onEndReached,
onRefresh,
reload,
@@ -59,6 +61,12 @@ const NotificationsList = ( {
), [isFetching, isConnected, data.length] );
const renderEmptyComponent = useCallback( ( ) => {
// show an offline/retry state if the user isn't connected or this request just takes too long
if ( isConnected === false || loadingTimedOut ) {
return <OfflineNotice onPress={reload} />;
}
// Loading
if ( isInitialLoading ) {
return (
<View className="h-full justify-center">
@@ -67,10 +75,7 @@ const NotificationsList = ( {
);
}
if ( isConnected === false ) {
return <OfflineNotice onPress={reload} />;
}
// Empty/error state
let msg = t( "No-Notifications-Found" );
let msg2 = null;
if ( !currentUser ) {
@@ -92,6 +97,7 @@ const NotificationsList = ( {
isError,
isInitialLoading,
isConnected,
loadingTimedOut,
reload,
t,
] );

View File

@@ -1,3 +1,4 @@
import { useQueryClient } from "@tanstack/react-query";
import { fetchObservationUpdates, fetchRemoteObservations } from "api/observations";
import type {
ApiNotification,
@@ -7,12 +8,15 @@ import type {
} from "api/types";
import flatten from "lodash/flatten";
import { RealmContext } from "providers/contexts";
import { useEffect, useMemo, useState } from "react";
import type Realm from "realm";
import Observation from "realmModels/Observation";
import { useAuthenticatedInfiniteQuery, useCurrentUser } from "sharedHooks";
const { useRealm } = RealmContext;
const LOADING_TIMEOUT = 5000;
// Extends API response with data we need in this app
export interface Notification extends ApiNotification {
resource?: ApiObservation;
@@ -24,6 +28,7 @@ interface InfiniteNotificationsScrollResponse {
isError?: boolean;
isFetching?: boolean;
isInitialLoading?: boolean;
loadingTimedOut: boolean;
notifications: Notification[];
refetch: ( ) => void;
}
@@ -89,10 +94,22 @@ const useInfiniteNotificationsScroll = (
): InfiniteNotificationsScrollResponse => {
const currentUser = useCurrentUser( );
const realm = useRealm( );
const queryClient = useQueryClient();
const [loadingTimedOut, setLoadingTimedOut] = useState( false );
const queryKey = ["useInfiniteNotificationsScroll", JSON.stringify( notificationParams )];
const queryKey = useMemo(
() => ["useInfiniteNotificationsScroll", JSON.stringify( notificationParams )],
[notificationParams],
);
const infQueryResult = useAuthenticatedInfiniteQuery(
const {
data,
isFetching,
isInitialLoading,
isError,
refetch,
fetchNextPage,
} = useAuthenticatedInfiniteQuery(
queryKey,
async ( { pageParam }: { pageParam: number }, optsWithAuth: ApiOpts ) => {
const params = { ...BASE_PARAMS, ...notificationParams };
@@ -142,16 +159,49 @@ const useInfiniteNotificationsScroll = (
},
);
// We want to timeout and show an offline/retry state if this request takes too long
useEffect( () => {
// Reset if we get data
if ( data !== undefined && !isFetching ) {
setLoadingTimedOut( false );
return undefined;
}
// Don't set timer if not loading
if ( !isFetching ) {
return undefined;
}
// Set a timeout and cancel the query if we hit the limit
const timer = setTimeout( () => {
if ( data === undefined && isFetching ) {
queryClient.cancelQueries( { queryKey } );
setLoadingTimedOut( true );
}
}, LOADING_TIMEOUT );
// eslint-disable-next-line consistent-return
return () => clearTimeout( timer );
}, [data, isFetching, queryKey, queryClient] );
// Reset when user manually retries
useEffect( () => {
if ( isFetching ) {
setLoadingTimedOut( false );
}
}, [isFetching] );
return {
refetch: infQueryResult.refetch,
isError: infQueryResult.isError,
isFetching: infQueryResult.isFetching,
isInitialLoading: infQueryResult.isInitialLoading,
refetch,
isError,
isFetching,
isInitialLoading,
loadingTimedOut,
// Disable fetchNextPage if signed out
fetchNextPage: currentUser
? infQueryResult.fetchNextPage
? fetchNextPage
: ( ) => undefined,
notifications: flatten( infQueryResult?.data?.pages ),
notifications: flatten( data?.pages ),
};
};