mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-24 11:10:25 +02:00
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>
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
getWebNotificationPermission,
|
|
isWebNotificationSupported,
|
|
requestWebNotificationPermission,
|
|
type WebNotificationPermission,
|
|
} from "@multica/core/platform";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import { Card, CardContent } from "@multica/ui/components/ui/card";
|
|
import { isDesktopShell } from "../../platform";
|
|
import { useT } from "../../i18n";
|
|
|
|
/**
|
|
* Web-only control for the browser permission that native notification banners
|
|
* require. Desktop delivers banners through the OS via Electron (no browser
|
|
* permission involved), so this renders nothing there. It also renders nothing
|
|
* when the Notification API is unavailable (SSR, older browsers).
|
|
*
|
|
* Capability and permission are read from `window`, so the first paint defers
|
|
* to a post-mount effect to keep SSR and client markup identical (no hydration
|
|
* mismatch).
|
|
*/
|
|
export function BrowserNotificationSetting() {
|
|
const { t } = useT("settings");
|
|
const [mounted, setMounted] = useState(false);
|
|
const [permission, setPermission] =
|
|
useState<WebNotificationPermission>("default");
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
setPermission(getWebNotificationPermission());
|
|
}, []);
|
|
|
|
// Pre-mount, on desktop, or where the API is missing → nothing to manage.
|
|
if (!mounted || isDesktopShell() || !isWebNotificationSupported()) return null;
|
|
|
|
const handleEnable = async () => {
|
|
setPermission(await requestWebNotificationPermission());
|
|
};
|
|
|
|
const statusHint =
|
|
permission === "granted"
|
|
? t(($) => $.notifications.browser.granted)
|
|
: permission === "denied"
|
|
? t(($) => $.notifications.browser.denied)
|
|
: t(($) => $.notifications.browser.hint);
|
|
|
|
return (
|
|
<Card>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="space-y-0.5 pr-4">
|
|
<p className="text-sm font-medium">
|
|
{t(($) => $.notifications.browser.label)}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">{statusHint}</p>
|
|
</div>
|
|
{permission === "default" && (
|
|
<Button size="sm" variant="outline" onClick={handleEnable}>
|
|
{t(($) => $.notifications.browser.enable)}
|
|
</Button>
|
|
)}
|
|
{permission === "granted" && (
|
|
<span className="shrink-0 text-xs font-medium text-muted-foreground">
|
|
{t(($) => $.notifications.browser.enabled_badge)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|