"use client"; /** * NotificationMatrix — the Settings grid controlling which events reach the * user over which channels (in-app, email, push). Account-wide preferences; * the device-level PushToggle above it governs whether *this* device can * receive push at all (two layers, like an OS permission vs. app settings). * * Cells write through to PUT /notifications/prefs one at a time and re-render * from the matrix the server returns, so the UI always shows resolved state. */ import { useEffect, useState } from "react"; import { toast } from "sonner"; import { notifications, type NotificationPrefs, type PrefChannel, type PrefEvent, } from "@/lib/social"; import { ToggleSwitch } from "@/components/settings/setting-row"; import { enablePush, isPushEnabled, pushPermission, pushSupported, } from "@/lib/push"; const EVENTS: { key: PrefEvent; label: string; description: string; /** Channels this event can actually be delivered on. Omit for all three. * A cell outside this set renders as "—" rather than a live toggle, so we * never show a switch that silently does nothing. */ channels?: PrefChannel[]; }[] = [ { key: "follows", label: "Follows", description: "Follow requests and accepted requests.", }, { key: "post_interactions", label: "Post interactions", description: "Likes and mentions on your posts.", }, { key: "sadhna_reminders", label: "Sādhana reminders", description: "Daily nudges to chant your rounds and share a verse.", }, { key: "community", label: "Community", description: "Rare announcements — such as new feature, downtime etc.", // Announcements are in-app/push only; we deliberately don't email them. channels: ["in_app", "push"], }, ]; const CHANNELS: { key: PrefChannel; label: string }[] = [ { key: "in_app", label: "In-app" }, { key: "email", label: "Email" }, { key: "push", label: "Push" }, ]; export function NotificationMatrix() { const [prefs, setPrefs] = useState(null); const [failed, setFailed] = useState(false); // The "event/channel" cell currently being written, so only it disables. const [busyCell, setBusyCell] = useState(null); useEffect(() => { let cancelled = false; notifications .prefs() .then(({ prefs }) => { if (!cancelled) setPrefs(prefs); }) .catch(() => { if (!cancelled) setFailed(true); }); return () => { cancelled = true; }; }, []); async function toggle(event: PrefEvent, channel: PrefChannel) { if (!prefs) return; // Default-on when the key is absent: prefs are stored as opt-outs, and a // BFF that predates this category simply omits it. const next = !(prefs[event]?.[channel] ?? true); setBusyCell(`${event}/${channel}`); try { // Turning a push cell ON only helps if this device can receive push — // offer the permission/subscription right at the gesture, best-effort // (the account-wide pref still saves if the user declines). if ( channel === "push" && next && pushSupported() && pushPermission() !== "denied" && !(await isPushEnabled()) ) { await enablePush().catch(() => { /* declined / unavailable — other subscribed devices still get it */ }); } const { prefs: updated } = await notifications.setPref( event, channel, next, ); setPrefs(updated); } catch { toast.error("Could not update preference"); } finally { setBusyCell(null); } } if (failed) { return (

Couldn't load your notification preferences. Reload to try again.

); } return (
{/* Header row */} {CHANNELS.map((c) => ( {c.label} ))} {EVENTS.map((event) => (
{event.label}

{event.description}

{CHANNELS.map((channel) => { const deliverable = !event.channels || event.channels.includes(channel.key); return (
{!deliverable ? ( — ) : prefs ? ( toggle(event.key, channel.key)} label={`${event.label} — ${channel.label}`} /> ) : ( … )}
); })}
))}
); }