mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-11 16:36:32 +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>
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useWorkspaceId } from "@multica/core/hooks";
|
|
import { notificationPreferenceOptions } from "@multica/core/notification-preferences/queries";
|
|
import { useUpdateNotificationPreferences } from "@multica/core/notification-preferences/mutations";
|
|
import type { NotificationGroupKey, NotificationPreferences } from "@multica/core/types";
|
|
import { Card, CardContent } from "@multica/ui/components/ui/card";
|
|
import { Switch } from "@multica/ui/components/ui/switch";
|
|
import { toast } from "sonner";
|
|
import { useT } from "../../i18n";
|
|
import { BrowserNotificationSetting } from "./browser-notification-setting";
|
|
|
|
// Inbox event groups rendered in the per-event toggle list. `system_notifications`
|
|
// is a sibling preference key but lives in its own section below.
|
|
const INBOX_GROUP_KEYS = [
|
|
"assignments",
|
|
"status_changes",
|
|
"comments",
|
|
"updates",
|
|
"agent_activity",
|
|
] as const;
|
|
type InboxGroupKey = (typeof INBOX_GROUP_KEYS)[number];
|
|
|
|
export function NotificationsTab() {
|
|
const { t } = useT("settings");
|
|
const wsId = useWorkspaceId();
|
|
const { data } = useQuery(notificationPreferenceOptions(wsId));
|
|
const mutation = useUpdateNotificationPreferences();
|
|
|
|
const preferences = data?.preferences ?? {};
|
|
|
|
const handleToggle = (key: NotificationGroupKey, enabled: boolean) => {
|
|
const updated: NotificationPreferences = {
|
|
...preferences,
|
|
[key]: enabled ? "all" : "muted",
|
|
};
|
|
// Remove keys set to "all" (default) to keep the object clean
|
|
if (enabled) {
|
|
delete updated[key];
|
|
}
|
|
mutation.mutate(updated, {
|
|
onError: (err) =>
|
|
toast.error(
|
|
err instanceof Error && err.message
|
|
? err.message
|
|
: t(($) => $.notifications.toast_failed),
|
|
),
|
|
});
|
|
};
|
|
|
|
const systemEnabled = preferences.system_notifications !== "muted";
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<section className="space-y-4">
|
|
<div>
|
|
<h2 className="text-sm font-semibold">{t(($) => $.notifications.title)}</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{t(($) => $.notifications.description)}
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent className="divide-y">
|
|
{INBOX_GROUP_KEYS.map((key: InboxGroupKey) => {
|
|
const enabled = preferences[key] !== "muted";
|
|
return (
|
|
<div
|
|
key={key}
|
|
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
|
|
>
|
|
<div className="space-y-0.5 pr-4">
|
|
<p className="text-sm font-medium">{t(($) => $.notifications.groups[key].label)}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t(($) => $.notifications.groups[key].description)}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={enabled}
|
|
onCheckedChange={(checked) => handleToggle(key, checked)}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
|
|
<section className="space-y-4">
|
|
<div>
|
|
<h2 className="text-sm font-semibold">{t(($) => $.notifications.system.title)}</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{t(($) => $.notifications.system.description)}
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5 pr-4">
|
|
<p className="text-sm font-medium">{t(($) => $.notifications.system.label)}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t(($) => $.notifications.system.hint)}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={systemEnabled}
|
|
onCheckedChange={(checked) => handleToggle("system_notifications", checked)}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Web-only: the browser permission banners require. Renders nothing on
|
|
desktop (OS-native delivery) or where the Notification API is absent. */}
|
|
<BrowserNotificationSetting />
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|