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