Files
multica/apps/web/components/web-notification-bridge.tsx
Bohan Jiang f5db77340f feat(web): native notification banners for the web app (MUL-3116) (#3883)
The in-app inbox (sidebar badge, real-time WS updates, settings, inbox
page) was already shared and worked on web. The only Desktop-only piece
was the native OS banner: handleInboxNew called desktopAPI.showNotification,
which is undefined on web, so no banner fired for new inbox items while the
app was unfocused.

Add the browser equivalent, keeping handleInboxNew as the single decision
point (focus + source-workspace mute gating stays shared with desktop):

- packages/core/platform/system-notification.ts: browser Notification engine
  (showWebNotification) + permission helpers + a click-handler registry. Lives
  in core (the caller does) but injects the click-routing decision so core
  stays headless.
- handleInboxNew: branch desktopAPI (unchanged) → else showWebNotification.
- apps/web WebNotificationBridge: registers click routing to the source
  workspace's inbox (?issue=…), mirroring desktop's DesktopInboxBridge.
- Settings → Notifications: web-only opt-in to grant browser permission
  (hidden on desktop / where the API is unavailable); en/zh-Hans/ja/ko.

Permission is an explicit settings opt-in (no auto-prompt on load, per
browser best practice). Tests cover the engine and the web path in
handleInboxNew.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 14:12:20 +08:00

45 lines
1.6 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import {
registerSystemNotificationClickHandler,
type SystemNotificationPayload,
} from "@multica/core/platform";
import { paths } from "@multica/core/paths";
import { useNavigation } from "@multica/views/navigation";
/**
* Routes browser notification clicks to the source workspace's inbox, focused
* on the clicked item. The web counterpart of the desktop `DesktopInboxBridge`:
* desktop receives the click via Electron IPC, web wires it through the
* Notification API's `onclick` (registered here into the core singleton).
*
* The route uses the `slug` the notification was emitted with — the SOURCE
* workspace — not the active one, so a click always opens the right inbox even
* after the user switches workspaces (#3766). An empty slug (unresolved
* source) is ignored. Marking the row read is handled by InboxPage's
* selected-item effect, which covers the `?issue=` URL-param path.
*/
export function WebNotificationBridge() {
const { push } = useNavigation();
// The adapter identity changes with the current route; the ref keeps the
// registered click handler stable while always calling the latest push.
const pushRef = useRef(push);
useEffect(() => {
pushRef.current = push;
}, [push]);
useEffect(() => {
registerSystemNotificationClickHandler(
({ slug, issueKey }: SystemNotificationPayload) => {
if (!slug) return;
const inboxPath = `${paths.workspace(slug).inbox()}?issue=${encodeURIComponent(issueKey)}`;
pushRef.current(inboxPath);
},
);
return () => registerSystemNotificationClickHandler(null);
}, []);
return null;
}