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/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx b/__tests__/components/profile-activity/ProfileActivityLogs.test.tsx
index 4d87e5f773..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/CommonFilterTargetSelect";
-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/app/[user]/identity/page.tsx b/app/[user]/identity/page.tsx
index 3bbe64256c..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/CommonFilterTargetSelect";
-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 1c1187e4bf..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/CommonFilterTargetSelect";
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/brain/notifications/Notifications.tsx b/components/brain/notifications/Notifications.tsx
index 38a7dc938c..552de90273 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";
@@ -28,33 +29,202 @@ 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 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;
}
+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, 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 lastErrorMessageRef = useRef(null);
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 +249,9 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio
});
useEffect(() => {
+ if (!isAuthenticated) {
+ return;
+ }
if (reload === "true" || hasMarkedAllAsReadRef.current) {
return;
}
@@ -87,7 +260,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,8 +276,10 @@ 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,
@@ -106,25 +287,49 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio
});
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 +342,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 +375,85 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio
isPinnedToBottomRef.current = true;
}, [activeFilter?.cause]);
+ useEffect(() => {
+ if (!queryError) {
+ 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);
+
+ 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 = globalThis.setTimeout(() => {
+ setHasTimedOut(true);
+ }, LOAD_TIMEOUT_MS);
+
+ return () => {
+ globalThis.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 +473,62 @@ 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;
+ lastErrorMessageRef.current = null;
+ 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;
useLayoutEffect(() => {
const scrollElement = scrollContainerRef.current;
@@ -264,7 +601,7 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio
cancelAnimationFrame(rafId);
}
};
- }, [items, showLoader, showNoItems]);
+ }, [items, showLoader, showNoItems, showErrorState]);
const handleScroll: UIEventHandler = useCallback(
(event) => {
@@ -298,39 +635,35 @@ export default function Notifications({ activeDrop, setActiveDrop }: Notificatio
]
);
- let notificationsContent = null;
- 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 (
-
+ {isAuthenticated ? (
+
+ ) : null}
(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 da442bb9bc..1fb6954502 100644
--- a/components/utils/CommonFilterTargetSelect.tsx
+++ b/components/utils/CommonFilterTargetSelect.tsx
@@ -2,24 +2,25 @@
import { useId } from "react";
-export enum FilterTargetType {
- ALL = "ALL",
- INCOMING = "INCOMING",
- OUTGOING = "OUTGOING",
-}
+import { ProfileActivityFilterTargetType } from "@/enums";
+
+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/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 9d0992896e..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/CommonFilterTargetSelect";
-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;
}
diff --git a/hooks/useNotificationsQuery.tsx b/hooks/useNotificationsQuery.tsx
index 4aae11d6a7..41a47afc12 100644
--- a/hooks/useNotificationsQuery.tsx
+++ b/hooks/useNotificationsQuery.tsx
@@ -13,35 +13,36 @@ 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 = {
limit: string;
cause: ApiNotificationCause[] | null;
pageParam?: number | null;
+ signal?: AbortSignal;
};
const getIdentityNotificationsQueryKey = (
@@ -54,10 +55,11 @@ const fetchNotifications = async ({
limit,
cause,
pageParam,
+ signal,
}: NotificationsQueryParams) => {
const params: Record = { limit };
- if (pageParam) {
+ if (pageParam != null) {
params.id_less_than = String(pageParam);
}
@@ -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) => {
+ 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;
+ },
});
const items = useMemo(() => {
@@ -145,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,