mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 16:20:35 +02:00
The Settings → System Notifications toggle persisted but never fired on the web app — `inbox:new` only invoked `desktopAPI.showNotification`, which is undefined outside Electron. Web users saw the toggle, enabled it, and got nothing (GH #2339). - Extract a shared `showSystemNotification` helper in `core/notifications` that bridges the Electron preload IPC AND the browser Notifications API. On web, the click handler focuses the tab and navigates to the inbox with the issue selector. - Settings tab now requests browser permission when the toggle flips on, shows actionable hints when permission is denied / not yet granted / unsupported, and offers a "Send test notification" button so users can verify their setup without waiting for an inbox event. - Refresh permission state on visibilitychange so the UI reflects changes the user makes in browser site settings. - Tests cover desktop bridge, granted/denied/missing permission, and click-routing. Co-authored-by: multica-agent <github@multica.ai>
133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
import { createLogger } from "../logger";
|
|
|
|
const logger = createLogger("system-notification");
|
|
|
|
interface DesktopNotificationPayload {
|
|
slug: string;
|
|
itemId: string;
|
|
issueKey: string;
|
|
title: string;
|
|
body: string;
|
|
}
|
|
|
|
interface DesktopAPI {
|
|
showNotification?: (payload: DesktopNotificationPayload) => void;
|
|
}
|
|
|
|
function getDesktopAPI(): DesktopAPI | undefined {
|
|
if (typeof window === "undefined") return undefined;
|
|
return (window as unknown as { desktopAPI?: DesktopAPI }).desktopAPI;
|
|
}
|
|
|
|
export type WebNotificationSupport =
|
|
| "supported"
|
|
| "permission_default"
|
|
| "permission_denied"
|
|
| "api_unavailable"
|
|
| "no_window";
|
|
|
|
export function detectWebNotificationSupport(): WebNotificationSupport {
|
|
if (typeof window === "undefined") return "no_window";
|
|
if (typeof Notification === "undefined") return "api_unavailable";
|
|
switch (Notification.permission) {
|
|
case "granted":
|
|
return "supported";
|
|
case "denied":
|
|
return "permission_denied";
|
|
default:
|
|
return "permission_default";
|
|
}
|
|
}
|
|
|
|
export interface SystemNotificationPayload extends DesktopNotificationPayload {
|
|
/** Path to navigate to when the user clicks the banner (web fallback only). */
|
|
inboxPath: string;
|
|
}
|
|
|
|
/**
|
|
* Fire a native OS notification for an inbox item, abstracting over the
|
|
* Electron preload bridge (`window.desktopAPI`) and the browser
|
|
* Notifications API. Returns a status string useful for diagnostics; the
|
|
* caller is expected to have already gated on focus + the user's
|
|
* `system_notifications` preference.
|
|
*
|
|
* On the desktop app the click handler routing is wired in the main process
|
|
* (see apps/desktop/src/main/index.ts). On web we wire it here: the Notification
|
|
* click event focuses the tab and navigates to the inbox path with the issue
|
|
* selector pre-populated, mirroring the desktop UX as closely as the browser
|
|
* sandbox allows.
|
|
*/
|
|
export function showSystemNotification(payload: SystemNotificationPayload): WebNotificationSupport | "delivered_desktop" {
|
|
const desktopAPI = getDesktopAPI();
|
|
if (desktopAPI?.showNotification) {
|
|
desktopAPI.showNotification({
|
|
slug: payload.slug,
|
|
itemId: payload.itemId,
|
|
issueKey: payload.issueKey,
|
|
title: payload.title,
|
|
body: payload.body,
|
|
});
|
|
return "delivered_desktop";
|
|
}
|
|
|
|
const support = detectWebNotificationSupport();
|
|
if (support !== "supported") {
|
|
logger.debug("skip web notification", { support, title: payload.title });
|
|
return support;
|
|
}
|
|
|
|
try {
|
|
const notification = new Notification(payload.title, {
|
|
body: payload.body,
|
|
tag: payload.itemId,
|
|
});
|
|
notification.addEventListener("click", () => {
|
|
try {
|
|
window.focus();
|
|
} catch {
|
|
// Some browsers reject window.focus() outside a user gesture; ignore.
|
|
}
|
|
window.location.assign(payload.inboxPath);
|
|
notification.close();
|
|
});
|
|
return "supported";
|
|
} catch (err) {
|
|
logger.warn("web notification failed", err);
|
|
return "api_unavailable";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prompt the browser for notification permission. Must be invoked from a
|
|
* user gesture (click, keypress) or the request is silently denied in many
|
|
* browsers. Returns the resulting permission state, or "unsupported" if the
|
|
* Notifications API is missing entirely.
|
|
*/
|
|
export async function requestWebNotificationPermission(): Promise<
|
|
"granted" | "denied" | "default" | "unsupported"
|
|
> {
|
|
if (typeof window === "undefined" || typeof Notification === "undefined") {
|
|
return "unsupported";
|
|
}
|
|
if (Notification.permission === "granted" || Notification.permission === "denied") {
|
|
return Notification.permission;
|
|
}
|
|
try {
|
|
const result = await Notification.requestPermission();
|
|
return result;
|
|
} catch (err) {
|
|
logger.warn("requestPermission failed", err);
|
|
return "denied";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* True when this build is the Electron desktop app — the preload script
|
|
* injects `window.desktopAPI`. Used by the settings UI to hide the
|
|
* browser-permission affordance, since the main process owns notifications
|
|
* on desktop.
|
|
*/
|
|
export function isDesktopApp(): boolean {
|
|
return Boolean(getDesktopAPI());
|
|
}
|