From bf419c11c2e288a6db6150939de8fc8aedf60914 Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 10:12:19 +0200 Subject: [PATCH 01/11] notifications loader fix Signed-off-by: Simo --- .../notifications/Notifications.test.tsx | 42 ++- .../brain/notifications/Notifications.tsx | 291 ++++++++++++++++-- hooks/useNotificationsQuery.tsx | 24 +- 3 files changed, 321 insertions(+), 36 deletions(-) diff --git a/__tests__/components/brain/notifications/Notifications.test.tsx b/__tests__/components/brain/notifications/Notifications.test.tsx index 20c70d76be..2878842537 100644 --- a/__tests__/components/brain/notifications/Notifications.test.tsx +++ b/__tests__/components/brain/notifications/Notifications.test.tsx @@ -2,6 +2,9 @@ import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; const mutateAsyncMock = jest.fn(); +const requestAuthMock = jest.fn().mockResolvedValue({ success: true }); +const setActiveProfileProxyMock = jest.fn().mockResolvedValue(undefined); +const setToastMock = jest.fn(); jest.mock('@tanstack/react-query', () => ({ useMutation: () => ({ mutateAsync: mutateAsyncMock }), @@ -19,11 +22,19 @@ jest.mock('@/components/auth/Auth', () => { const React = require('react'); return { AuthContext: React.createContext({ - connectedProfile: { handle: 'bob' }, - activeProfileProxy: false, - setToast: jest.fn(), + connectedProfile: { handle: 'bob', id: '1' }, + activeProfileProxy: null, + fetchingProfile: false, + requestAuth: requestAuthMock, + setToast: setToastMock, + setActiveProfileProxy: setActiveProfileProxyMock, + }), + useAuth: () => ({ + setTitle: setTitleMock, + requestAuth: requestAuthMock, + setToast: setToastMock, + setActiveProfileProxy: setActiveProfileProxyMock, }), - useAuth: () => ({ setTitle: setTitleMock }), }; }); @@ -91,6 +102,11 @@ describe('Notifications component', () => { mutateAsyncMock.mockResolvedValue(undefined); useNotificationsQueryMock.mockReset(); setTitleMock.mockClear(); + requestAuthMock.mockClear(); + requestAuthMock.mockResolvedValue({ success: true }); + setActiveProfileProxyMock.mockClear(); + setActiveProfileProxyMock.mockResolvedValue(undefined); + setToastMock.mockClear(); }); it('shows loader when fetching and no items', async () => { @@ -99,9 +115,11 @@ describe('Notifications component', () => { isFetching: true, isFetchingNextPage: false, hasNextPage: false, - fetchNextPage: jest.fn(), - refetch: jest.fn(), + fetchNextPage: jest.fn().mockResolvedValue(undefined), + refetch: jest.fn().mockResolvedValue(undefined), isInitialQueryDone: false, + isSuccess: false, + error: null, }); render(); @@ -119,9 +137,11 @@ describe('Notifications component', () => { isFetching: false, isFetchingNextPage: false, hasNextPage: false, - fetchNextPage: jest.fn(), - refetch: jest.fn(), + fetchNextPage: jest.fn().mockResolvedValue(undefined), + refetch: jest.fn().mockResolvedValue(undefined), isInitialQueryDone: true, + isSuccess: true, + error: null, }); render(); @@ -138,9 +158,11 @@ describe('Notifications component', () => { isFetching: false, isFetchingNextPage: false, hasNextPage: false, - fetchNextPage: jest.fn(), - refetch: jest.fn(), + fetchNextPage: jest.fn().mockResolvedValue(undefined), + refetch: jest.fn().mockResolvedValue(undefined), isInitialQueryDone: true, + isSuccess: true, + error: null, }); render(); diff --git a/components/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx index 38a7dc938c..8a83b0b810 100644 --- a/components/brain/notifications/Notifications.tsx +++ b/components/brain/notifications/Notifications.tsx @@ -28,6 +28,10 @@ import SpinnerLoader from "@/components/common/SpinnerLoader"; import { NEAR_TOP_SCROLL_THRESHOLD_PX } from "../constants"; const STICK_TO_BOTTOM_SCROLL_THRESHOLD_PX = 32; +const LOAD_TIMEOUT_MS = 15000; +const DEFAULT_ERROR_MESSAGE = "Failed to load notifications. Please try again."; +const LOAD_TIMEOUT_MESSAGE = + "Loading notifications is taking longer than expected. Please try again."; interface NotificationsProps { readonly activeDrop: ActiveDropState | null; @@ -35,26 +39,41 @@ interface NotificationsProps { } export default function Notifications({ activeDrop, setActiveDrop }: NotificationsProps) { - const { connectedProfile, activeProfileProxy, setToast } = - useContext(AuthContext); + const { + connectedProfile, + activeProfileProxy, + fetchingProfile, + requestAuth, + setToast, + setActiveProfileProxy, + } = useContext(AuthContext); const scrollContainerRef = useRef(null); const hasInitializedScrollRef = useRef(false); const isPinnedToBottomRef = useRef(true); const hasMarkedAllAsReadRef = useRef(false); const isPrependingRef = useRef(false); const previousScrollHeightRef = useRef(0); + const errorToastShownRef = useRef(false); + const reauthTriggeredRef = useRef(false); + const timeoutToastShownRef = useRef(false); const { notificationsViewStyle } = useLayout(); const searchParams = useSearchParams(); const [activeFilter, setActiveFilter] = useState( null ); + const [hasTimedOut, setHasTimedOut] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); const { removeAllDeliveredNotifications } = useNotificationsContext(); const router = useRouter(); const pathname = usePathname(); const reload = searchParams?.get('reload') ?? undefined; + const isAuthenticated = !!connectedProfile?.handle && !activeProfileProxy; + const isLoadingProfile = fetchingProfile && !connectedProfile; + const hasConnectedProfile = !!connectedProfile; + const hasProfileHandle = !!connectedProfile?.handle; useSetTitle("Notifications | My Stream | Brain"); @@ -79,6 +98,9 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio }); useEffect(() => { + if (!isAuthenticated) { + return; + } if (reload === "true" || hasMarkedAllAsReadRef.current) { return; } @@ -87,7 +109,13 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio markAllAsRead().catch((error) => { console.error("Failed to mark notifications as read:", error); }); - }, [markAllAsRead, reload]); + }, [markAllAsRead, reload, isAuthenticated]); + + useEffect(() => { + if (!isAuthenticated) { + hasMarkedAllAsReadRef.current = false; + } + }, [isAuthenticated]); const { items, @@ -97,34 +125,81 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio fetchNextPage, refetch, isInitialQueryDone, + isSuccess, + error: queryError, } = useNotificationsQuery({ - identity: connectedProfile?.handle, + identity: isAuthenticated ? connectedProfile?.handle : undefined, activeProfileProxy: !!activeProfileProxy, limit: "30", reverse: true, cause: activeFilter?.cause, }); + const getErrorDetails = (error: unknown) => { + if (error instanceof Error) { + const message = error.message?.trim() || DEFAULT_ERROR_MESSAGE; + return { + message, + isUnauthorized: /unauthorized/i.test(message), + }; + } + if (typeof error === "string") { + const message = error.trim() || DEFAULT_ERROR_MESSAGE; + return { + message, + isUnauthorized: /unauthorized/i.test(message), + }; + } + return { + message: DEFAULT_ERROR_MESSAGE, + isUnauthorized: false, + }; + }; + useEffect(() => { - if (reload === "true") { - refetch() - .then(() => { - hasMarkedAllAsReadRef.current = true; - return markAllAsRead(); - }) - .catch((error) => { - console.error("Error during refetch:", error); - }); + if (reload !== "true") { + return; + } + + const clearReloadParam = () => { const params = new URLSearchParams(searchParams?.toString() || ""); params.delete("reload"); const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname || "/my-stream/notifications"; router.replace(newUrl, { scroll: false }); + }; + + if (!isAuthenticated) { + clearReloadParam(); + return; } - }, [reload, refetch, markAllAsRead, searchParams, pathname, router]); + + refetch() + .then(() => { + hasMarkedAllAsReadRef.current = true; + return markAllAsRead(); + }) + .catch((error) => { + console.error("Error during refetch:", error); + }) + .finally(() => { + clearReloadParam(); + }); + }, [ + reload, + refetch, + markAllAsRead, + searchParams, + pathname, + router, + isAuthenticated, + ]); const triggerFetchOlder = useCallback(() => { + if (!isAuthenticated) { + return; + } if (isFetchingNextPage) { return; } @@ -137,7 +212,7 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio } isPrependingRef.current = true; fetchNextPage(); - }, [isFetchingNextPage, hasNextPage, fetchNextPage]); + }, [isAuthenticated, isFetchingNextPage, hasNextPage, fetchNextPage]); useLayoutEffect(() => { const scrollElement = scrollContainerRef.current; @@ -170,6 +245,77 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio isPinnedToBottomRef.current = true; }, [activeFilter?.cause]); + useEffect(() => { + if (!queryError) { + setErrorMessage(null); + errorToastShownRef.current = false; + reauthTriggeredRef.current = false; + return; + } + + const { message, isUnauthorized } = getErrorDetails(queryError); + setErrorMessage(message); + setHasTimedOut(false); + + if (!errorToastShownRef.current) { + setToast({ message, type: "error" }); + errorToastShownRef.current = true; + } + + if (isUnauthorized && !reauthTriggeredRef.current) { + requestAuth().catch((error) => { + console.error("Failed to re-authenticate after notifications error:", error); + }); + reauthTriggeredRef.current = true; + } + }, [queryError, setToast, requestAuth]); + + useEffect(() => { + if (isSuccess) { + setHasTimedOut(false); + timeoutToastShownRef.current = false; + return; + } + + if (errorMessage || !isAuthenticated || isLoadingProfile) { + setHasTimedOut(false); + timeoutToastShownRef.current = false; + return; + } + + if (isInitialQueryDone) { + return; + } + + const timerId = window.setTimeout(() => { + setHasTimedOut(true); + }, LOAD_TIMEOUT_MS); + + return () => { + window.clearTimeout(timerId); + }; + }, [ + isSuccess, + errorMessage, + isAuthenticated, + isInitialQueryDone, + isLoadingProfile, + ]); + + useEffect(() => { + if (hasTimedOut) { + if (!timeoutToastShownRef.current) { + setToast({ + message: LOAD_TIMEOUT_MESSAGE, + type: "warning", + }); + timeoutToastShownRef.current = true; + } + } else { + timeoutToastShownRef.current = false; + } + }, [hasTimedOut, setToast]); + useLayoutEffect(() => { if (!isPrependingRef.current) { return; @@ -189,9 +335,78 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio isPrependingRef.current = false; }, [items]); - const showLoader = (!isInitialQueryDone || isFetching) && items.length === 0; - const showNoItems = isInitialQueryDone && !isFetching && items.length === 0; - const shouldEnableInfiniteScroll = !showLoader && !showNoItems; + const handleRetry = useCallback(() => { + setHasTimedOut(false); + setErrorMessage(null); + errorToastShownRef.current = false; + reauthTriggeredRef.current = false; + refetch({ cancelRefetch: true }).catch((error) => { + console.error("Failed to retry notifications fetch:", error); + }); + }, [refetch]); + + const handleAuthRetry = useCallback(() => { + requestAuth().catch((error) => { + console.error("Failed to re-authenticate:", error); + setToast({ + message: + error instanceof Error ? error.message : DEFAULT_ERROR_MESSAGE, + type: "error", + }); + }); + }, [requestAuth, setToast]); + + const handleProxyDisable = useCallback(() => { + setActiveProfileProxy(null).catch((error) => { + console.error("Failed to switch to primary profile:", error); + setToast({ + message: + error instanceof Error + ? error.message + : "Unable to switch to primary profile. Please try again.", + type: "error", + }); + }); + }, [setActiveProfileProxy, setToast]); + + const showLoader = + isAuthenticated && + !hasTimedOut && + !errorMessage && + (!isInitialQueryDone || isFetching) && + items.length === 0; + const showNoItems = + isAuthenticated && + !errorMessage && + !hasTimedOut && + isInitialQueryDone && + !isFetching && + items.length === 0; + const showErrorState = (errorMessage || hasTimedOut) && items.length === 0; + const shouldEnableInfiniteScroll = + isAuthenticated && !showLoader && !showNoItems && !showErrorState; + + const showProxyDisabledState = !!activeProfileProxy; + const resolvedErrorMessage = hasTimedOut + ? LOAD_TIMEOUT_MESSAGE + : errorMessage ?? DEFAULT_ERROR_MESSAGE; + + const renderStateMessage = ( + message: string, + action?: { label: string; handler: () => void } + ) => ( +
+

{message}

+ {action ? ( + + ) : null} +
+ ); useLayoutEffect(() => { const scrollElement = scrollContainerRef.current; @@ -264,7 +479,7 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio cancelAnimationFrame(rafId); } }; - }, [items, showLoader, showNoItems]); + }, [items, showLoader, showNoItems, showErrorState]); const handleScroll: UIEventHandler = useCallback( (event) => { @@ -299,7 +514,33 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio ); let notificationsContent = null; - if (showLoader) { + if (isLoadingProfile) { + notificationsContent = ( +
+ +
+ ); + } else if (!hasConnectedProfile) { + notificationsContent = renderStateMessage( + "Connect your wallet to view notifications.", + { label: "Reconnect wallet", handler: handleAuthRetry } + ); + } else if (!hasProfileHandle) { + notificationsContent = renderStateMessage( + "We couldn't determine your profile handle. Please reconnect to continue.", + { label: "Reconnect wallet", handler: handleAuthRetry } + ); + } else if (showProxyDisabledState) { + notificationsContent = renderStateMessage( + "Notifications are not available while you are using a profile proxy.", + { label: "Switch to primary profile", handler: handleProxyDisable } + ); + } else if (showErrorState) { + notificationsContent = renderStateMessage(resolvedErrorMessage, { + label: "Try again", + handler: handleRetry, + }); + } else if (showLoader) { notificationsContent = (
@@ -327,10 +568,12 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio className="tw-relative tw-flex tw-flex-col tw-rounded-t-xl tw-overflow-x-hidden scroll-shadow" style={notificationsViewStyle}>
- + {isAuthenticated ? ( + + ) : null}
{ const params: Record = { limit }; @@ -68,6 +70,7 @@ const fetchNotifications = async ({ return await commonApiFetch({ endpoint: "notifications", params, + signal, }); }; @@ -97,12 +100,29 @@ export function useNotificationsQuery({ */ const query = useInfiniteQuery({ queryKey: getIdentityNotificationsQueryKey(identity, limit, cause), - queryFn: ({ pageParam }: { pageParam: number | null }) => - fetchNotifications({ limit, cause, pageParam }), + queryFn: ({ + pageParam, + signal, + }: { + pageParam: number | null; + signal: AbortSignal | undefined; + }) => fetchNotifications({ limit, cause, pageParam, signal }), initialPageParam: null, getNextPageParam: (lastPage) => lastPage.notifications.at(-1)?.id ?? null, enabled: !!identity && !activeProfileProxy, staleTime: 60000, + retry: (failureCount, error: unknown) => { + if (typeof error === "string") { + if (error.toLowerCase().includes("unauthorized")) { + return false; + } + } else if (error instanceof Error) { + if (/unauthorized/i.test(error.message)) { + return false; + } + } + return failureCount < 3; + }, }); const items = useMemo(() => { From d643e921f9325d6d537cd65a0970a305bcf7b35f Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 10:29:43 +0200 Subject: [PATCH 02/11] wip Signed-off-by: Simo --- .../brain/notifications/Notifications.tsx | 13 +++++-- hooks/useNotificationsQuery.tsx | 37 +++++++++++-------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/components/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx index 8a83b0b810..9ee076434b 100644 --- a/components/brain/notifications/Notifications.tsx +++ b/components/brain/notifications/Notifications.tsx @@ -136,23 +136,30 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio }); const getErrorDetails = (error: unknown) => { + const status = + (error as any)?.status ?? + (error as any)?.response?.status ?? + (error as any)?.cause?.status; + if (error instanceof Error) { const message = error.message?.trim() || DEFAULT_ERROR_MESSAGE; return { message, - isUnauthorized: /unauthorized/i.test(message), + isUnauthorized: status === 401 || /unauthorized/i.test(message), }; } + if (typeof error === "string") { const message = error.trim() || DEFAULT_ERROR_MESSAGE; return { message, - isUnauthorized: /unauthorized/i.test(message), + isUnauthorized: status === 401 || /unauthorized/i.test(message), }; } + return { message: DEFAULT_ERROR_MESSAGE, - isUnauthorized: false, + isUnauthorized: status === 401, }; }; diff --git a/hooks/useNotificationsQuery.tsx b/hooks/useNotificationsQuery.tsx index f13347fee8..41a47afc12 100644 --- a/hooks/useNotificationsQuery.tsx +++ b/hooks/useNotificationsQuery.tsx @@ -13,29 +13,29 @@ interface UseNotificationsQueryProps { /** * If true, reverse the notifications order (e.g. for a "descending" / "newest first" display). */ - reverse?: boolean; + readonly reverse?: boolean; /** * Only fetch notifications if we have a valid identity. * Used in "enabled" to avoid sending queries prematurely. */ - identity?: string | null; + readonly identity?: string | null; /** * Example usage where you only fetch if no active profile proxy is set. * Adjust or remove according to your own logic. */ - activeProfileProxy?: boolean; + readonly activeProfileProxy?: boolean; /** * How many notifications to fetch per page. */ - limit?: string; + readonly limit?: string; /** * The cause of the notifications to fetch. */ - cause?: ApiNotificationCause[] | null; + readonly cause?: ApiNotificationCause[] | null; } type NotificationsQueryParams = { @@ -59,7 +59,7 @@ const fetchNotifications = async ({ }: NotificationsQueryParams) => { const params: Record = { limit }; - if (pageParam) { + if (pageParam != null) { params.id_less_than = String(pageParam); } @@ -112,14 +112,14 @@ export function useNotificationsQuery({ enabled: !!identity && !activeProfileProxy, staleTime: 60000, retry: (failureCount, error: unknown) => { - if (typeof error === "string") { - if (error.toLowerCase().includes("unauthorized")) { - return false; - } - } else if (error instanceof Error) { - if (/unauthorized/i.test(error.message)) { - return false; - } + const status = + (error as any)?.status ?? + (error as any)?.response?.status ?? + (error as any)?.cause?.status; + if (status === 401) return false; + if (typeof error === "string" && /unauthorized/i.test(error)) return false; + if (error instanceof Error && /unauthorized/i.test(error.message)) { + return false; } return failureCount < 3; }, @@ -165,8 +165,13 @@ export function usePrefetchNotifications() { } queryClient.prefetchInfiniteQuery({ queryKey: getIdentityNotificationsQueryKey(identity, limit, cause), - queryFn: ({ pageParam }: { pageParam?: number | null }) => - fetchNotifications({ limit, cause, pageParam }), + queryFn: ({ + pageParam, + signal, + }: { + pageParam?: number | null; + signal?: AbortSignal; + }) => fetchNotifications({ limit, cause, pageParam, signal }), initialPageParam: null, getNextPageParam: (lastPage) => lastPage.notifications.at(-1)?.id ?? null, From 63909e00c1ccebc28bdd4b8edada677657c6440d Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 11:10:24 +0200 Subject: [PATCH 03/11] wip Signed-off-by: Simo --- .../brain/notifications/Notifications.tsx | 211 ++++++++++++------ 1 file changed, 142 insertions(+), 69 deletions(-) diff --git a/components/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx index 9ee076434b..8def3811d6 100644 --- a/components/brain/notifications/Notifications.tsx +++ b/components/brain/notifications/Notifications.tsx @@ -8,12 +8,13 @@ import { useRef, useState, } from "react"; -import type { UIEventHandler } from "react"; +import type { ReactNode, UIEventHandler } from "react"; import { useSetTitle } from "@/contexts/TitleContext"; import { AuthContext } from "@/components/auth/Auth"; import { ReactQueryWrapperContext } from "@/components/react-query-wrapper/ReactQueryWrapper"; import { commonApiPostWithoutBodyAndResponse } from "@/services/api/common-api"; import NotificationsWrapper from "./NotificationsWrapper"; +import type { TypedNotification } from "@/types/feed.types"; import { useMutation } from "@tanstack/react-query"; import MyStreamNoItems from "../my-stream/layout/MyStreamNoItems"; import { useRouter, useSearchParams, usePathname } from "next/navigation"; @@ -33,6 +34,127 @@ const DEFAULT_ERROR_MESSAGE = "Failed to load notifications. Please try again."; const LOAD_TIMEOUT_MESSAGE = "Loading notifications is taking longer than expected. Please try again."; +interface StateAction { + readonly label: string; + readonly handler: () => void; +} + +function renderStateMessage( + message: string, + action?: StateAction +): ReactNode { + return ( +
+

{message}

+ {action ? ( + + ) : null} +
+ ); +} + +interface NotificationsContentParams { + readonly isLoadingProfile: boolean; + readonly hasConnectedProfile: boolean; + readonly hasProfileHandle: boolean; + readonly showProxyDisabledState: boolean; + readonly showErrorState: boolean; + readonly resolvedErrorMessage: string; + readonly handleRetry: () => void; + readonly handleAuthRetry: () => void; + readonly handleProxyDisable: () => void; + readonly showLoader: boolean; + readonly showNoItems: boolean; + readonly items: TypedNotification[]; + readonly loadingOlder: boolean; + readonly activeDrop: ActiveDropState | null; + readonly setActiveDrop: (activeDrop: ActiveDropState | null) => void; +} + +function resolveNotificationsContent({ + isLoadingProfile, + hasConnectedProfile, + hasProfileHandle, + showProxyDisabledState, + showErrorState, + resolvedErrorMessage, + handleRetry, + handleAuthRetry, + handleProxyDisable, + showLoader, + showNoItems, + items, + loadingOlder, + activeDrop, + setActiveDrop, +}: NotificationsContentParams): ReactNode { + if (isLoadingProfile) { + return ( +
+ +
+ ); + } + + if (!hasConnectedProfile) { + return renderStateMessage("Connect your wallet to view notifications.", { + label: "Reconnect wallet", + handler: handleAuthRetry, + }); + } + + if (!hasProfileHandle) { + return renderStateMessage( + "We couldn't determine your profile handle. Please reconnect to continue.", + { label: "Reconnect wallet", handler: handleAuthRetry } + ); + } + + if (showProxyDisabledState) { + return renderStateMessage( + "Notifications are not available while you are using a profile proxy.", + { label: "Switch to primary profile", handler: handleProxyDisable } + ); + } + + if (showErrorState) { + return renderStateMessage(resolvedErrorMessage, { + label: "Try again", + handler: handleRetry, + }); + } + + if (showLoader) { + return ( +
+ +
+ ); + } + + if (showNoItems) { + return ( +
+ +
+ ); + } + + return ( + + ); +} + interface NotificationsProps { readonly activeDrop: ActiveDropState | null; readonly setActiveDrop: (activeDrop: ActiveDropState | null) => void; @@ -294,12 +416,12 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio return; } - const timerId = window.setTimeout(() => { + const timerId = globalThis.setTimeout(() => { setHasTimedOut(true); }, LOAD_TIMEOUT_MS); return () => { - window.clearTimeout(timerId); + globalThis.clearTimeout(timerId); }; }, [ isSuccess, @@ -398,23 +520,6 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio ? LOAD_TIMEOUT_MESSAGE : errorMessage ?? DEFAULT_ERROR_MESSAGE; - const renderStateMessage = ( - message: string, - action?: { label: string; handler: () => void } - ) => ( -
-

{message}

- {action ? ( - - ) : null} -
- ); - useLayoutEffect(() => { const scrollElement = scrollContainerRef.current; if (!scrollElement) { @@ -520,55 +625,23 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio ] ); - let notificationsContent = null; - if (isLoadingProfile) { - notificationsContent = ( -
- -
- ); - } else if (!hasConnectedProfile) { - notificationsContent = renderStateMessage( - "Connect your wallet to view notifications.", - { label: "Reconnect wallet", handler: handleAuthRetry } - ); - } else if (!hasProfileHandle) { - notificationsContent = renderStateMessage( - "We couldn't determine your profile handle. Please reconnect to continue.", - { label: "Reconnect wallet", handler: handleAuthRetry } - ); - } else if (showProxyDisabledState) { - notificationsContent = renderStateMessage( - "Notifications are not available while you are using a profile proxy.", - { label: "Switch to primary profile", handler: handleProxyDisable } - ); - } else if (showErrorState) { - notificationsContent = renderStateMessage(resolvedErrorMessage, { - label: "Try again", - handler: handleRetry, - }); - } else if (showLoader) { - notificationsContent = ( -
- -
- ); - } else if (showNoItems) { - notificationsContent = ( -
- -
- ); - } else { - notificationsContent = ( - - ); - } + const notificationsContent = resolveNotificationsContent({ + isLoadingProfile, + hasConnectedProfile, + hasProfileHandle, + showProxyDisabledState, + showErrorState, + resolvedErrorMessage, + handleRetry, + handleAuthRetry, + handleProxyDisable, + showLoader, + showNoItems, + items, + loadingOlder: isFetchingNextPage, + activeDrop, + setActiveDrop, + }); return (
Date: Mon, 13 Oct 2025 11:36:55 +0200 Subject: [PATCH 04/11] wip Signed-off-by: Simo --- components/brain/notifications/Notifications.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx index 8def3811d6..f9915133c8 100644 --- a/components/brain/notifications/Notifications.tsx +++ b/components/brain/notifications/Notifications.tsx @@ -50,6 +50,7 @@ function renderStateMessage( From ac01987ee995687c438c12754d3c5e48e3a4c830 Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 12:25:28 +0200 Subject: [PATCH 05/11] wip Signed-off-by: Simo --- components/brain/notifications/Notifications.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/components/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx index f9915133c8..2a948cdbc3 100644 --- a/components/brain/notifications/Notifications.tsx +++ b/components/brain/notifications/Notifications.tsx @@ -50,7 +50,6 @@ function renderStateMessage( @@ -179,6 +178,7 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio const errorToastShownRef = useRef(false); const reauthTriggeredRef = useRef(false); const timeoutToastShownRef = useRef(false); + const lastErrorMessageRef = useRef(null); const { notificationsViewStyle } = useLayout(); const searchParams = useSearchParams(); @@ -380,10 +380,18 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio setErrorMessage(null); errorToastShownRef.current = false; reauthTriggeredRef.current = false; + lastErrorMessageRef.current = null; return; } const { message, isUnauthorized } = getErrorDetails(queryError); + + if (lastErrorMessageRef.current !== message) { + errorToastShownRef.current = false; + reauthTriggeredRef.current = false; + lastErrorMessageRef.current = message; + } + setErrorMessage(message); setHasTimedOut(false); @@ -470,6 +478,7 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio setErrorMessage(null); errorToastShownRef.current = false; reauthTriggeredRef.current = false; + lastErrorMessageRef.current = null; refetch({ cancelRefetch: true }).catch((error) => { console.error("Failed to retry notifications fetch:", error); }); From 25142c38c8c241a2d8f508c74fc2751beb6eceb0 Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 12:53:57 +0200 Subject: [PATCH 06/11] wip Signed-off-by: Simo --- .../profile-activity/ProfileActivityLogs.test.tsx | 2 +- app/[user]/identity/page.tsx | 2 +- app/[user]/rep/page.tsx | 2 +- components/profile-activity/ProfileActivityLogs.tsx | 13 +++++++++++++ components/utils/CommonFilterTargetSelect.tsx | 8 +++----- components/utils/filterTargetTypes.ts | 5 +++++ helpers/profile-logs.helpers.ts | 2 +- 7 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 components/utils/filterTargetTypes.ts diff --git a/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx b/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx index 4d87e5f773..196f89d07a 100644 --- a/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx +++ b/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx @@ -1,4 +1,4 @@ -import { FilterTargetType } from "@/components/utils/CommonFilterTargetSelect"; +import { FilterTargetType } from "@/components/utils/filterTargetTypes"; import { ProfileActivityLogType } from "@/enums"; import { convertActivityLogParams } from "@/helpers/profile-logs.helpers"; diff --git a/app/[user]/identity/page.tsx b/app/[user]/identity/page.tsx index 3bbe64256c..29a6980cf1 100644 --- a/app/[user]/identity/page.tsx +++ b/app/[user]/identity/page.tsx @@ -1,7 +1,7 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import type { ActivityLogParams } from "@/components/profile-activity/ProfileActivityLogs"; import UserPageIdentityWrapper from "@/components/user/identity/UserPageIdentityWrapper"; -import { FilterTargetType } from "@/components/utils/CommonFilterTargetSelect"; +import { FilterTargetType } from "@/components/utils/filterTargetTypes"; import { RateMatter } from "@/enums"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import { getProfileLogTypes } from "@/helpers/profile-logs.helpers"; diff --git a/app/[user]/rep/page.tsx b/app/[user]/rep/page.tsx index 1c1187e4bf..0119b86cdf 100644 --- a/app/[user]/rep/page.tsx +++ b/app/[user]/rep/page.tsx @@ -1,7 +1,7 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import type { ActivityLogParams } from "@/components/profile-activity/ProfileActivityLogs"; import UserPageRepWrapper from "@/components/user/rep/UserPageRepWrapper"; -import { FilterTargetType } from "@/components/utils/CommonFilterTargetSelect"; +import { FilterTargetType } from "@/components/utils/filterTargetTypes"; import { ApiProfileRepRatesState } from "@/entities/IProfile"; import { RateMatter } from "@/enums"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; diff --git a/components/profile-activity/ProfileActivityLogs.tsx b/components/profile-activity/ProfileActivityLogs.tsx index 562ebe4cc2..a9bacb5853 100644 --- a/components/profile-activity/ProfileActivityLogs.tsx +++ b/components/profile-activity/ProfileActivityLogs.tsx @@ -51,6 +51,11 @@ export default function ProfileActivityLogs({ readonly disableActiveGroup?: boolean; readonly children?: React.ReactNode; }) { + + console.log('==============================================='); + console.log('initialParams'); + console.log(initialParams); + console.log('==============================================='); const activeGroupId = useSelector(selectActiveGroupId); const [selectedFilters, setSelectedFilters] = useState< ProfileActivityLogType[] @@ -95,6 +100,14 @@ export default function ProfileActivityLogs({ }) ); +useEffect(() => { + + console.log('==============================================='); + console.log('params', params); + console.log('==============================================='); +}, [params]); + + useEffect(() => { setParams( convertActivityLogParams({ diff --git a/components/utils/CommonFilterTargetSelect.tsx b/components/utils/CommonFilterTargetSelect.tsx index da442bb9bc..052f6153eb 100644 --- a/components/utils/CommonFilterTargetSelect.tsx +++ b/components/utils/CommonFilterTargetSelect.tsx @@ -2,11 +2,9 @@ import { useId } from "react"; -export enum FilterTargetType { - ALL = "ALL", - INCOMING = "INCOMING", - OUTGOING = "OUTGOING", -} +import { FilterTargetType } from "./filterTargetTypes"; + +export { FilterTargetType }; const TARGETS = [ { id: FilterTargetType.ALL, name: "All" }, diff --git a/components/utils/filterTargetTypes.ts b/components/utils/filterTargetTypes.ts new file mode 100644 index 0000000000..30246f46b1 --- /dev/null +++ b/components/utils/filterTargetTypes.ts @@ -0,0 +1,5 @@ +export enum FilterTargetType { + ALL = "ALL", + INCOMING = "INCOMING", + OUTGOING = "OUTGOING", +} diff --git a/helpers/profile-logs.helpers.ts b/helpers/profile-logs.helpers.ts index 9d0992896e..e558b120d5 100644 --- a/helpers/profile-logs.helpers.ts +++ b/helpers/profile-logs.helpers.ts @@ -2,7 +2,7 @@ import { ActivityLogParams, ActivityLogParamsConverted, } from "@/components/profile-activity/ProfileActivityLogs"; -import { FilterTargetType } from "@/components/utils/CommonFilterTargetSelect"; +import { FilterTargetType } from "@/components/utils/filterTargetTypes"; import { ProfileActivityLogType } from "@/enums"; const DISABLED_LOG_TYPES = [ From 14f5d284c0c2a7957da58c750e413c49ce091945 Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 13:04:35 +0200 Subject: [PATCH 07/11] wip Signed-off-by: Simo --- components/profile-activity/ProfileActivityLogs.tsx | 13 ------------- components/utils/CommonFilterTargetSelect.tsx | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/components/profile-activity/ProfileActivityLogs.tsx b/components/profile-activity/ProfileActivityLogs.tsx index a9bacb5853..562ebe4cc2 100644 --- a/components/profile-activity/ProfileActivityLogs.tsx +++ b/components/profile-activity/ProfileActivityLogs.tsx @@ -51,11 +51,6 @@ export default function ProfileActivityLogs({ readonly disableActiveGroup?: boolean; readonly children?: React.ReactNode; }) { - - console.log('==============================================='); - console.log('initialParams'); - console.log(initialParams); - console.log('==============================================='); const activeGroupId = useSelector(selectActiveGroupId); const [selectedFilters, setSelectedFilters] = useState< ProfileActivityLogType[] @@ -100,14 +95,6 @@ export default function ProfileActivityLogs({ }) ); -useEffect(() => { - - console.log('==============================================='); - console.log('params', params); - console.log('==============================================='); -}, [params]); - - useEffect(() => { setParams( convertActivityLogParams({ diff --git a/components/utils/CommonFilterTargetSelect.tsx b/components/utils/CommonFilterTargetSelect.tsx index 052f6153eb..9e2a4f00b3 100644 --- a/components/utils/CommonFilterTargetSelect.tsx +++ b/components/utils/CommonFilterTargetSelect.tsx @@ -4,7 +4,7 @@ import { useId } from "react"; import { FilterTargetType } from "./filterTargetTypes"; -export { FilterTargetType }; +export { FilterTargetType } from "./filterTargetTypes"; const TARGETS = [ { id: FilterTargetType.ALL, name: "All" }, From 60074f3296fe44f92a4242f5d203eb37e3da2a0a Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 15:35:34 +0200 Subject: [PATCH 08/11] wip Signed-off-by: Simo --- app/[user]/identity/page.tsx | 5 ++--- app/[user]/rep/page.tsx | 5 ++--- .../profile-activity/ProfileActivityLogs.tsx | 18 +++++++++++------- components/utils/CommonFilterTargetSelect.tsx | 17 ++++++++++------- components/utils/filterTargetTypes.ts | 8 +++----- enums.ts | 6 ++++++ helpers/profile-logs.helpers.ts | 14 ++++++++------ 7 files changed, 42 insertions(+), 31 deletions(-) diff --git a/app/[user]/identity/page.tsx b/app/[user]/identity/page.tsx index 29a6980cf1..a1c31e2501 100644 --- a/app/[user]/identity/page.tsx +++ b/app/[user]/identity/page.tsx @@ -1,8 +1,7 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import type { ActivityLogParams } from "@/components/profile-activity/ProfileActivityLogs"; import UserPageIdentityWrapper from "@/components/user/identity/UserPageIdentityWrapper"; -import { FilterTargetType } from "@/components/utils/filterTargetTypes"; -import { RateMatter } from "@/enums"; +import { ProfileActivityFilterTargetType, RateMatter } from "@/enums"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import { getProfileLogTypes } from "@/helpers/profile-logs.helpers"; import { getInitialRatersParams } from "@/helpers/server.helpers"; @@ -18,7 +17,7 @@ const getInitialActivityLogParams = ( logTypes: [], }), matter: null, - targetType: FilterTargetType.ALL, + targetType: ProfileActivityFilterTargetType.ALL, handleOrWallet, groupId: null, }); diff --git a/app/[user]/rep/page.tsx b/app/[user]/rep/page.tsx index 0119b86cdf..fa08ff72e6 100644 --- a/app/[user]/rep/page.tsx +++ b/app/[user]/rep/page.tsx @@ -1,9 +1,8 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import type { ActivityLogParams } from "@/components/profile-activity/ProfileActivityLogs"; import UserPageRepWrapper from "@/components/user/rep/UserPageRepWrapper"; -import { FilterTargetType } from "@/components/utils/filterTargetTypes"; import { ApiProfileRepRatesState } from "@/entities/IProfile"; -import { RateMatter } from "@/enums"; +import { ProfileActivityFilterTargetType, RateMatter } from "@/enums"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import { getProfileLogTypes } from "@/helpers/profile-logs.helpers"; import { getInitialRatersParams } from "@/helpers/server.helpers"; @@ -24,7 +23,7 @@ const getInitialActivityLogParams = ( logTypes: [], }), matter: RateMatter.REP, - targetType: FilterTargetType.ALL, + targetType: ProfileActivityFilterTargetType.ALL, handleOrWallet, groupId: null, }); diff --git a/components/profile-activity/ProfileActivityLogs.tsx b/components/profile-activity/ProfileActivityLogs.tsx index 562ebe4cc2..06a421d75c 100644 --- a/components/profile-activity/ProfileActivityLogs.tsx +++ b/components/profile-activity/ProfileActivityLogs.tsx @@ -5,13 +5,15 @@ import { CountlessPage } from "@/helpers/Types"; import { commonApiFetch } from "@/services/api/common-api"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; -import CommonFilterTargetSelect, { - FilterTargetType, -} from "../utils/CommonFilterTargetSelect"; +import CommonFilterTargetSelect from "../utils/CommonFilterTargetSelect"; import ProfileActivityLogsFilter from "./filter/ProfileActivityLogsFilter"; import ProfileActivityLogsList from "./list/ProfileActivityLogsList"; -import { ProfileActivityLogType, RateMatter } from "@/enums"; +import { + ProfileActivityFilterTargetType, + ProfileActivityLogType, + RateMatter, +} from "@/enums"; import { convertActivityLogParams } from "@/helpers/profile-logs.helpers"; import { selectActiveGroupId } from "@/store/groupSlice"; import { useSelector } from "react-redux"; @@ -24,7 +26,7 @@ export interface ActivityLogParams { readonly pageSize: number; readonly logTypes: ProfileActivityLogType[]; readonly matter: RateMatter | null; - readonly targetType: FilterTargetType; + readonly targetType: ProfileActivityFilterTargetType; readonly handleOrWallet: string | null; readonly groupId: string | null; } @@ -55,7 +57,9 @@ export default function ProfileActivityLogs({ const [selectedFilters, setSelectedFilters] = useState< ProfileActivityLogType[] >(initialParams.logTypes); - const [targetType, setTargetType] = useState( + const [targetType, setTargetType] = useState< + ProfileActivityFilterTargetType + >( initialParams.targetType ); const [currentPage, setCurrentPage] = useState(initialParams.page); @@ -75,7 +79,7 @@ export default function ProfileActivityLogs({ setCurrentPage(1); }; - const onTargetType = (target: FilterTargetType) => { + const onTargetType = (target: ProfileActivityFilterTargetType) => { setTargetType(target); setCurrentPage(1); }; diff --git a/components/utils/CommonFilterTargetSelect.tsx b/components/utils/CommonFilterTargetSelect.tsx index 9e2a4f00b3..1fb6954502 100644 --- a/components/utils/CommonFilterTargetSelect.tsx +++ b/components/utils/CommonFilterTargetSelect.tsx @@ -2,22 +2,25 @@ import { useId } from "react"; -import { FilterTargetType } from "./filterTargetTypes"; +import { ProfileActivityFilterTargetType } from "@/enums"; -export { FilterTargetType } from "./filterTargetTypes"; +export { ProfileActivityFilterTargetType } from "@/enums"; +export { + ProfileActivityFilterTargetType as FilterTargetType, +} from "@/enums"; const TARGETS = [ - { id: FilterTargetType.ALL, name: "All" }, - { id: FilterTargetType.OUTGOING, name: "Outgoing" }, - { id: FilterTargetType.INCOMING, name: "Incoming" }, + { id: ProfileActivityFilterTargetType.ALL, name: "All" }, + { id: ProfileActivityFilterTargetType.OUTGOING, name: "Outgoing" }, + { id: ProfileActivityFilterTargetType.INCOMING, name: "Incoming" }, ]; export default function CommonFilterTargetSelect({ selected, onChange, }: { - readonly selected: FilterTargetType; - readonly onChange: (filter: FilterTargetType) => void; + readonly selected: ProfileActivityFilterTargetType; + readonly onChange: (filter: ProfileActivityFilterTargetType) => void; }) { const baseId = useId().replaceAll(":", ""); const groupName = `filter-target-${baseId}`; diff --git a/components/utils/filterTargetTypes.ts b/components/utils/filterTargetTypes.ts index 30246f46b1..20d0798fea 100644 --- a/components/utils/filterTargetTypes.ts +++ b/components/utils/filterTargetTypes.ts @@ -1,5 +1,3 @@ -export enum FilterTargetType { - ALL = "ALL", - INCOMING = "INCOMING", - OUTGOING = "OUTGOING", -} +export { + ProfileActivityFilterTargetType as FilterTargetType, +} from "@/enums"; diff --git a/enums.ts b/enums.ts index 7bacce86b5..8a7a0b3659 100644 --- a/enums.ts +++ b/enums.ts @@ -145,6 +145,12 @@ export enum DelegationCenterSection { HTML = "html", } +export enum ProfileActivityFilterTargetType { + ALL = "ALL", + INCOMING = "INCOMING", + OUTGOING = "OUTGOING", +} + export enum ProfileActivityLogType { RATING_EDIT = "RATING_EDIT", HANDLE_EDIT = "HANDLE_EDIT", diff --git a/helpers/profile-logs.helpers.ts b/helpers/profile-logs.helpers.ts index e558b120d5..8ae6abbcd8 100644 --- a/helpers/profile-logs.helpers.ts +++ b/helpers/profile-logs.helpers.ts @@ -2,8 +2,10 @@ import { ActivityLogParams, ActivityLogParamsConverted, } from "@/components/profile-activity/ProfileActivityLogs"; -import { FilterTargetType } from "@/components/utils/filterTargetTypes"; -import { ProfileActivityLogType } from "@/enums"; +import { + ProfileActivityFilterTargetType, + ProfileActivityLogType, +} from "@/enums"; const DISABLED_LOG_TYPES = [ ProfileActivityLogType.DROP_COMMENT, @@ -34,7 +36,7 @@ export const INITIAL_ACTIVITY_LOGS_PARAMS: ActivityLogParams = { logTypes: [], }), matter: null, - targetType: FilterTargetType.ALL, + targetType: ProfileActivityFilterTargetType.ALL, handleOrWallet: null, groupId: null, }; @@ -65,18 +67,18 @@ export const convertActivityLogParams = ({ return converted; } - if (params.targetType === FilterTargetType.ALL) { + if (params.targetType === ProfileActivityFilterTargetType.ALL) { converted.include_incoming = "true"; converted.profile = params.handleOrWallet; return converted; } - if (params.targetType === FilterTargetType.INCOMING) { + if (params.targetType === ProfileActivityFilterTargetType.INCOMING) { converted.target = params.handleOrWallet; return converted; } - if (params.targetType === FilterTargetType.OUTGOING) { + if (params.targetType === ProfileActivityFilterTargetType.OUTGOING) { converted.profile = params.handleOrWallet; return converted; } From b12071a78e55a987bc65c81dd0785a0df1bb9c0d Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 15:46:27 +0200 Subject: [PATCH 09/11] wip Signed-off-by: Simo --- .../profile-activity/ProfileActivityLogs.test.tsx | 12 +++++++----- components/utils/filterTargetTypes.ts | 3 --- 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 components/utils/filterTargetTypes.ts diff --git a/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx b/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx index 196f89d07a..c1b01ba3d2 100644 --- a/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx +++ b/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx @@ -1,5 +1,7 @@ -import { FilterTargetType } from "@/components/utils/filterTargetTypes"; -import { ProfileActivityLogType } from "@/enums"; +import { + ProfileActivityFilterTargetType, + ProfileActivityLogType, +} from "@/enums"; import { convertActivityLogParams } from "@/helpers/profile-logs.helpers"; describe("convertActivityLogParams", () => { @@ -8,7 +10,7 @@ describe("convertActivityLogParams", () => { pageSize: 10, logTypes: [ProfileActivityLogType.DROP_CREATED], matter: null, - targetType: FilterTargetType.ALL, + targetType: ProfileActivityFilterTargetType.ALL, handleOrWallet: null, groupId: "g1", }; @@ -43,7 +45,7 @@ describe("convertActivityLogParams", () => { params: { ...base, handleOrWallet: "u", - targetType: FilterTargetType.INCOMING, + targetType: ProfileActivityFilterTargetType.INCOMING, }, disableActiveGroup: false, }); @@ -52,7 +54,7 @@ describe("convertActivityLogParams", () => { params: { ...base, handleOrWallet: "u", - targetType: FilterTargetType.OUTGOING, + targetType: ProfileActivityFilterTargetType.OUTGOING, }, disableActiveGroup: false, }); diff --git a/components/utils/filterTargetTypes.ts b/components/utils/filterTargetTypes.ts deleted file mode 100644 index 20d0798fea..0000000000 --- a/components/utils/filterTargetTypes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { - ProfileActivityFilterTargetType as FilterTargetType, -} from "@/enums"; From 448e53bd79649b8e3a5b701da2f7119f6bec98cd Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 15:50:19 +0200 Subject: [PATCH 10/11] wip Signed-off-by: Simo --- components/brain/notifications/Notifications.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx index 2a948cdbc3..b8836d15c5 100644 --- a/components/brain/notifications/Notifications.tsx +++ b/components/brain/notifications/Notifications.tsx @@ -521,7 +521,7 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio isInitialQueryDone && !isFetching && items.length === 0; - const showErrorState = (errorMessage || hasTimedOut) && items.length === 0; + const showErrorState = (!!errorMessage || hasTimedOut) && items.length === 0; const shouldEnableInfiniteScroll = isAuthenticated && !showLoader && !showNoItems && !showErrorState; From a5fee9477e39282719acc0a978010d863aa027c0 Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 13 Oct 2025 16:00:38 +0200 Subject: [PATCH 11/11] wip Signed-off-by: Simo --- .../brain/notifications/Notifications.tsx | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/components/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx index b8836d15c5..552de90273 100644 --- a/components/brain/notifications/Notifications.tsx +++ b/components/brain/notifications/Notifications.tsx @@ -160,6 +160,34 @@ interface NotificationsProps { readonly setActiveDrop: (activeDrop: ActiveDropState | null) => void; } +const getErrorDetails = (error: unknown) => { + const status = + (error as any)?.status ?? + (error as any)?.response?.status ?? + (error as any)?.cause?.status; + + if (error instanceof Error) { + const message = error.message?.trim() || DEFAULT_ERROR_MESSAGE; + return { + message, + isUnauthorized: status === 401 || /unauthorized/i.test(message), + }; + } + + if (typeof error === "string") { + const message = error.trim() || DEFAULT_ERROR_MESSAGE; + return { + message, + isUnauthorized: status === 401 || /unauthorized/i.test(message), + }; + } + + return { + message: DEFAULT_ERROR_MESSAGE, + isUnauthorized: status === 401, + }; +}; + export default function Notifications({ activeDrop, setActiveDrop }: NotificationsProps) { const { connectedProfile, @@ -258,34 +286,6 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio cause: activeFilter?.cause, }); - const getErrorDetails = (error: unknown) => { - const status = - (error as any)?.status ?? - (error as any)?.response?.status ?? - (error as any)?.cause?.status; - - if (error instanceof Error) { - const message = error.message?.trim() || DEFAULT_ERROR_MESSAGE; - return { - message, - isUnauthorized: status === 401 || /unauthorized/i.test(message), - }; - } - - if (typeof error === "string") { - const message = error.trim() || DEFAULT_ERROR_MESSAGE; - return { - message, - isUnauthorized: status === 401 || /unauthorized/i.test(message), - }; - } - - return { - message: DEFAULT_ERROR_MESSAGE, - isUnauthorized: status === 401, - }; - }; - useEffect(() => { if (reload !== "true") { return;