diff --git a/src/components/Notifications/Notifications.tsx b/src/components/Notifications/Notifications.tsx index 6e742bafa..b6c2cd830 100644 --- a/src/components/Notifications/Notifications.tsx +++ b/src/components/Notifications/Notifications.tsx @@ -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( OWNER_TAB ); const { t } = useTranslation(); @@ -44,14 +47,14 @@ const Notifications = ( ) => { {activeTab === OWNER_TAB && ( EventRegister.emit( NOTIFICATIONS_REFRESHED, OWNER_TAB )} /> )} {activeTab === OTHER_TAB && ( EventRegister.emit( NOTIFICATIONS_REFRESHED, OTHER_TAB )} /> )} diff --git a/src/components/Notifications/NotificationsContainer.tsx b/src/components/Notifications/NotificationsContainer.tsx index 54fd4ff38..d438776c5 100644 --- a/src/components/Notifications/NotificationsContainer.tsx +++ b/src/components/Notifications/NotificationsContainer.tsx @@ -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} diff --git a/src/components/Notifications/NotificationsList.tsx b/src/components/Notifications/NotificationsList.tsx index 636ac1123..ebcc16dcc 100644 --- a/src/components/Notifications/NotificationsList.tsx +++ b/src/components/Notifications/NotificationsList.tsx @@ -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 ; + } + + // Loading if ( isInitialLoading ) { return ( @@ -67,10 +75,7 @@ const NotificationsList = ( { ); } - if ( isConnected === false ) { - return ; - } - + // 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, ] ); diff --git a/src/sharedHooks/useInfiniteNotificationsScroll.ts b/src/sharedHooks/useInfiniteNotificationsScroll.ts index 8f05bb3ca..dd5f1cd04 100644 --- a/src/sharedHooks/useInfiniteNotificationsScroll.ts +++ b/src/sharedHooks/useInfiniteNotificationsScroll.ts @@ -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 ), }; };