From acee09ec4410521bd0e461e58c6033e49396ac40 Mon Sep 17 00:00:00 2001 From: feruzm Date: Thu, 17 Sep 2026 12:51:37 +0000 Subject: [PATCH 1/3] fix(sdk): unread notification count uses a placeholder, not initialData initialData is stamped as fetched at creation, so under a 60s staleTime the seeded 0 counted as fresh: fetchQuery returned it without a request, an observer skipped the fetch on mount until the next refetchInterval, and a count restored from a persisted cache lost to it. placeholderData keeps a number on screen while loading without any of that. The navbar bell ignores placeholder data, so the count loaded with the page does not ring it, and the deck toolbar defaults the count to 0. --- .../deck-toolbar-base-actions.tsx | 2 +- .../navbar/navbar-notifications-button.tsx | 13 ++-- .../navbar-notifications-button.spec.tsx | 68 ++++++++++++++++++ ...cations-unread-count-query-options.spec.ts | 71 +++++++++++++++++++ ...otifications-unread-count-query-options.ts | 6 +- 5 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/specs/features/shared/navbar-notifications-button.spec.tsx create mode 100644 packages/sdk/src/modules/notifications/queries/get-notifications-unread-count-query-options.spec.ts diff --git a/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar-base-actions.tsx b/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar-base-actions.tsx index 1254526f14..dc9b468973 100644 --- a/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar-base-actions.tsx +++ b/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar-base-actions.tsx @@ -21,7 +21,7 @@ export const DeckToolbarBaseActions = ({ setShowPurchaseDialog }: Props) => { const { activeUser } = useActiveAccount(); const toggleUIProp = useGlobalStore((s) => s.toggleUiProp); - const { data: unread } = useQuery( + const { data: unread = 0 } = useQuery( getNotificationsUnreadCountQueryOptions( activeUser?.username, getAccessToken(activeUser?.username ?? "") diff --git a/apps/web/src/features/shared/navbar/navbar-notifications-button.tsx b/apps/web/src/features/shared/navbar/navbar-notifications-button.tsx index 0067603e86..13c36049f8 100644 --- a/apps/web/src/features/shared/navbar/navbar-notifications-button.tsx +++ b/apps/web/src/features/shared/navbar/navbar-notifications-button.tsx @@ -18,12 +18,13 @@ export function NavbarNotificationsButton({ onClick }: { onClick?: () => void }) const toggleUiProp = useGlobalStore((state) => state.toggleUiProp); const globalNotifications = useGlobalStore((state) => state.globalNotifications); - const { data: unread } = useQuery( + const { data, isPlaceholderData } = useQuery( getNotificationsUnreadCountQueryOptions( activeUser?.username, getAccessToken(activeUser?.username ?? "") ) ); + const unread = data ?? 0; const [ringing, setRinging] = useState(false); // Ref guard: remembers the first unread count seen after mount so the bell @@ -31,15 +32,17 @@ export function NavbarNotificationsButton({ onClick }: { onClick?: () => void }) const prevUnreadRef = useRef(undefined); useEffect(() => { - if (typeof unread !== "number") { + // Only counts from the server: the 0 shown while the first request runs is not a + // reading, and recording it would ring the bell as soon as the real count arrives. + if (isPlaceholderData || typeof data !== "number") { return; } const prev = prevUnreadRef.current; - prevUnreadRef.current = unread; - if (prev !== undefined && unread > prev) { + prevUnreadRef.current = data; + if (prev !== undefined && data > prev) { setRinging(true); } - }, [unread]); + }, [data, isPlaceholderData]); return ( ({ useQuery: () => unreadResult })); +vi.mock("@ecency/sdk", () => ({ getNotificationsUnreadCountQueryOptions: vi.fn(() => ({})) })); +vi.mock("@/core/global-store", () => ({ + useGlobalStore: (s: any) => s({ toggleUiProp: vi.fn(), globalNotifications: true }) +})); +vi.mock("@/core/hooks", () => ({ + useActiveAccount: () => ({ activeUser: { username: "tester" } }) +})); +vi.mock("@/utils", () => ({ getAccessToken: vi.fn(() => "mock-token") })); +vi.mock("@/config", () => ({ + EcencyConfigManager: { Conditional: ({ children }: any) => <>{children} } +})); +vi.mock("@ui/tooltip", () => ({ Tooltip: ({ children }: any) => <>{children} })); +vi.mock("@ui/button", () => ({ + Button: ({ iconClassName, icon, appearance, onAnimationEnd, ...rest }: any) => ( +