129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* PushToggle — the settings control for browser push notifications. Reads the
|
|
* current state on mount (supported? permission? server-configured?
|
|
* subscribed?) and lets the user enable/disable. Renders as a settings row
|
|
* matching the rest of /settings.
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { toast } from "sonner";
|
|
import {
|
|
disablePush,
|
|
enablePush,
|
|
isPushEnabled,
|
|
pushConfigured,
|
|
pushPermission,
|
|
pushSupported,
|
|
} from "@/lib/push";
|
|
|
|
type State =
|
|
| "loading"
|
|
| "unsupported"
|
|
| "unavailable" // server has no VAPID keys
|
|
| "denied" // browser permission blocked
|
|
| "on"
|
|
| "off";
|
|
|
|
export function PushToggle() {
|
|
const [state, setState] = useState<State>("loading");
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
if (!pushSupported()) return finish("unsupported");
|
|
if (pushPermission() === "denied") return finish("denied");
|
|
if (!(await pushConfigured())) return finish("unavailable");
|
|
finish((await isPushEnabled()) ? "on" : "off");
|
|
function finish(s: State) {
|
|
if (!cancelled) setState(s);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
async function toggle() {
|
|
setBusy(true);
|
|
try {
|
|
if (state === "on") {
|
|
await disablePush();
|
|
setState("off");
|
|
toast.success("Push notifications turned off");
|
|
} else {
|
|
await enablePush();
|
|
setState("on");
|
|
toast.success("Push notifications turned on");
|
|
}
|
|
} catch (err) {
|
|
const msg =
|
|
err instanceof Error ? err.message : "Something went wrong.";
|
|
// A fresh block surfaces as a permission error — reflect it.
|
|
if (pushPermission() === "denied") setState("denied");
|
|
toast.error(msg);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const description = describe(state);
|
|
|
|
return (
|
|
<ul className="divide-y divide-border">
|
|
<li className="flex items-center justify-between gap-4 py-3">
|
|
<div className="min-w-0">
|
|
<span className="text-sm text-foreground">
|
|
Push notifications
|
|
</span>
|
|
<p className="text-xs text-muted-foreground mt-0.5">{description}</p>
|
|
</div>
|
|
<div className="shrink-0">
|
|
{state === "on" || state === "off" ? (
|
|
<button
|
|
type="button"
|
|
onClick={toggle}
|
|
disabled={busy}
|
|
aria-pressed={state === "on"}
|
|
className={[
|
|
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-60",
|
|
state === "on" ? "bg-primary" : "bg-muted-foreground/30",
|
|
].join(" ")}
|
|
>
|
|
<span
|
|
className={[
|
|
"inline-block size-5 transform rounded-full bg-white shadow transition-transform",
|
|
state === "on" ? "translate-x-5" : "translate-x-0.5",
|
|
].join(" ")}
|
|
/>
|
|
</button>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">
|
|
{state === "loading" ? "…" : "Unavailable"}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
function describe(state: State): string {
|
|
switch (state) {
|
|
case "on":
|
|
return "On — you'll be notified about follow requests and more, even when the app is closed.";
|
|
case "off":
|
|
return "Get notified about follow requests and more, even when the app is closed.";
|
|
case "denied":
|
|
return "Blocked in your browser. Enable notifications for this site in your browser settings, then reload.";
|
|
case "unsupported":
|
|
return "This browser doesn't support push notifications.";
|
|
case "unavailable":
|
|
return "Push notifications aren't available right now.";
|
|
default:
|
|
return "Checking…";
|
|
}
|
|
}
|