Files
multica/packages/views/settings/components/notifications-tab.tsx
Jiayuan Zhang e3dd31cbe5 feat(notifications): add system notifications toggle in settings (#2132)
* feat(notifications): add system notifications toggle in settings

Add a per-user, per-workspace toggle to enable/disable native OS
notification banners. Reuses the existing notification-preferences
endpoint by introducing a `system_notifications` key alongside the
inbox event groups; the realtime handler reads the cached preference
and skips desktopAPI.showNotification when muted.

Co-authored-by: multica-agent <github@multica.ai>

* fix(notifications): fetch system_notifications pref lazily

Settings is the only mounted reader of notificationPreferenceOptions,
so a fresh app start (or any session that never visits Settings) left
the cache empty and the muted preference silently fell back to default
"all". Switch the inbox:new handler to ensureQueryData so the value is
fetched on first use and cached for subsequent events.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-06 11:01:22 +02:00

112 lines
3.9 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";
// 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: () => toast.error(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>
</section>
</div>
);
}