Add web push notifications for new inbox items
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { del, get, post, put, type NotificationPrefs } from "./api.js";
|
||||
import { errorText, h, replace } from "./dom.js";
|
||||
import { on, state } from "./state.js";
|
||||
|
||||
const kinds: { key: keyof NotificationPrefs; label: string }[] = [
|
||||
{ key: "blocking_questions", label: "Blocking questions" },
|
||||
{ key: "questions", label: "Non-blocking questions" },
|
||||
{ key: "reports", label: "Turn summaries" },
|
||||
{ key: "errors", label: "Session errors" },
|
||||
{ key: "rate_limits", label: "Rate limits" },
|
||||
];
|
||||
|
||||
export function mountSettings(root: HTMLElement): () => void {
|
||||
const device = h("div", { class: "push-device" });
|
||||
const prefs = h("fieldset", { class: "push-prefs" });
|
||||
|
||||
const renderPrefs = () => {
|
||||
const me = state.me;
|
||||
if (!me) {
|
||||
replace(prefs);
|
||||
return;
|
||||
}
|
||||
replace(
|
||||
prefs,
|
||||
h("legend", {}, "Notify me about"),
|
||||
...kinds.map(({ key, label }) => {
|
||||
const box = h("input", { type: "checkbox", checked: me.notification_prefs[key] });
|
||||
box.onchange = async () => {
|
||||
try {
|
||||
state.me = await put("/api/me/notifications", { body: { ...me.notification_prefs, [key]: box.checked } });
|
||||
} catch (err) {
|
||||
alert(errorText(err));
|
||||
}
|
||||
renderPrefs();
|
||||
};
|
||||
return h("label", {}, box, " ", label);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
let disposed = false;
|
||||
const renderDevice = async () => {
|
||||
const status = await deviceStatus();
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
if (status.kind === "unsupported") {
|
||||
replace(device, h("p", { class: "hint" }, "This browser doesn't support web push."));
|
||||
return;
|
||||
}
|
||||
if (status.kind === "denied") {
|
||||
replace(device, h("p", { class: "hint" }, "Notifications are blocked for this site in the browser's settings."));
|
||||
return;
|
||||
}
|
||||
|
||||
const button = h("button", {}, status.sub ? "Turn off on this browser" : "Turn on for this browser");
|
||||
button.onclick = async () => {
|
||||
button.disabled = true;
|
||||
try {
|
||||
if (status.sub) {
|
||||
await disable(status.sub);
|
||||
} else {
|
||||
await enable(status.reg);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(errorText(err));
|
||||
}
|
||||
await renderDevice();
|
||||
};
|
||||
replace(device, h("p", {}, status.sub ? "This browser receives notifications." : "This browser doesn't receive notifications."), button);
|
||||
};
|
||||
|
||||
replace(root, h("div", { class: "settings-view" }, h("h2", {}, "Notifications"), device, prefs));
|
||||
const off = on("me", renderPrefs);
|
||||
renderPrefs();
|
||||
void renderDevice().catch((err: unknown) => replace(device, h("p", { class: "error-text" }, errorText(err))));
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
off();
|
||||
};
|
||||
}
|
||||
|
||||
type DeviceStatus =
|
||||
| { kind: "unsupported" }
|
||||
| { kind: "denied" }
|
||||
| { kind: "ready"; reg: ServiceWorkerRegistration; sub: PushSubscription | null };
|
||||
|
||||
async function deviceStatus(): Promise<DeviceStatus> {
|
||||
if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) {
|
||||
return { kind: "unsupported" };
|
||||
}
|
||||
if (Notification.permission === "denied") {
|
||||
return { kind: "denied" };
|
||||
}
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const sub = await reg.pushManager.getSubscription();
|
||||
if (sub) {
|
||||
await register(sub);
|
||||
}
|
||||
return { kind: "ready", reg, sub };
|
||||
}
|
||||
|
||||
async function enable(reg: ServiceWorkerRegistration): Promise<void> {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
return;
|
||||
}
|
||||
const key = await get("/api/push/key");
|
||||
const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64url(key.public_key) });
|
||||
await register(sub);
|
||||
}
|
||||
|
||||
async function disable(sub: PushSubscription): Promise<void> {
|
||||
await del("/api/me/push-subscriptions", { query: { endpoint: sub.endpoint } });
|
||||
await sub.unsubscribe();
|
||||
}
|
||||
|
||||
async function register(sub: PushSubscription): Promise<void> {
|
||||
const keys = sub.toJSON().keys ?? {};
|
||||
await post("/api/me/push-subscriptions", { body: { endpoint: sub.endpoint, p256dh: keys["p256dh"] ?? "", auth: keys["auth"] ?? "" } });
|
||||
}
|
||||
|
||||
function base64url(s: string): Uint8Array<ArrayBuffer> {
|
||||
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/"));
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) {
|
||||
out[i] = bin.charCodeAt(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user