From a6c80d65d3ce9e02ebdd27d863fa1a5d49446ac5 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 6 May 2026 16:59:01 +0800 Subject: [PATCH] fix(notifications): fetch system_notifications pref lazily Settings is the only mounted reader of notificationPreferenceOptions, so a fresh app start (or any session that never visits Settings) left the cache empty and the muted preference silently fell back to default "all". Switch the inbox:new handler to ensureQueryData so the value is fetched on first use and cached for subsequent events. Co-authored-by: multica-agent --- packages/core/realtime/use-realtime-sync.ts | 26 +++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/core/realtime/use-realtime-sync.ts b/packages/core/realtime/use-realtime-sync.ts index a1572c64d3..b380642c40 100644 --- a/packages/core/realtime/use-realtime-sync.ts +++ b/packages/core/realtime/use-realtime-sync.ts @@ -28,8 +28,7 @@ import { } from "../issues/ws-updaters"; import { onInboxNew, onInboxInvalidate, onInboxIssueStatusChanged, onInboxIssueDeleted } from "../inbox/ws-updaters"; import { inboxKeys } from "../inbox/queries"; -import { notificationPreferenceKeys } from "../notification-preferences/queries"; -import type { NotificationPreferenceResponse } from "../types"; +import { notificationPreferenceOptions } from "../notification-preferences/queries"; import { workspaceKeys, workspaceListOptions } from "../workspace/queries"; import { chatKeys } from "../chat/queries"; import { useChatStore } from "../chat"; @@ -270,7 +269,7 @@ export function useRealtimeSync( if (wsId) onIssueLabelsChanged(qc, wsId, issue_id, labels ?? []); }); - const unsubInboxNew = ws.on("inbox:new", (p) => { + const unsubInboxNew = ws.on("inbox:new", async (p) => { const { item } = p as InboxNewPayload; if (!item) return; const wsId = getCurrentWsId(); @@ -280,14 +279,21 @@ export function useRealtimeSync( // styling is enough — no need to interrupt with a banner. `desktopAPI` // is injected by the preload script; its absence (web app) skips silently. if (typeof document !== "undefined" && document.hasFocus()) return; - // Respect the user's system-notification preference. Read from the - // Query cache so we don't have to plumb it through hook params; if the - // pref hasn't loaded yet the default ("all") fires the banner. + // Respect the user's system-notification preference. The Settings page + // owns the only `useQuery` for this resource, so on a fresh app start + // (or any session that hasn't visited Settings) the React Query cache + // is empty — using `getQueryData` would silently default to "all" and + // ignore the user's saved choice. `ensureQueryData` resolves to the + // cached value if present and otherwise fetches once, populating the + // cache for subsequent events. On network failure we fall through to + // the default ("all") rather than swallow the banner entirely. if (wsId) { - const prefData = qc.getQueryData( - notificationPreferenceKeys.all(wsId), - ); - if (prefData?.preferences?.system_notifications === "muted") return; + try { + const prefData = await qc.ensureQueryData(notificationPreferenceOptions(wsId)); + if (prefData?.preferences?.system_notifications === "muted") return; + } catch { + // Fall through with default behavior. + } } // Capture the source workspace slug at emit time. The user may switch // workspaces before clicking the banner (macOS Notification Center