Skip to content
70 linesCodeBlameRaw
1import { env } from "cloudflare:workers";
2import { data } from "react-router";
3
4import { notifyClient, type NotifyPreferencesChange, type PresenceChange, type PushSubscriptionJson } from "@g1t/contracts";
5import type { SoundSettingsChange } from "@g1t/contracts/sounds";
6
7import type { Route } from "./+types/api";
8import { identity } from "../../lib/services.server";
9import { assertSameOrigin, requireUser } from "../../lib/session.server";
10
11function client() {
12 if (!env.NOTIFY) throw data({ error: "Notifications are not set up here." }, { status: 503 });
13 return notifyClient(env.NOTIFY);
14}
15
16/**
17 * The signed-in person's notification settings, as JSON: `GET` their
18 * status (with `?endpoint=` for whether this browser gets pushes), `POST`
19 * a change: `subscribe`, `unsubscribe`, `preferences`, `test`,
20 * `presence` (your status, being away, Do Not Disturb), or `sounds` (what
21 * you hear when chat moves, kept with the account by identity).
22 */
23export async function loader({ context, request }: Route.LoaderArgs) {
24 const user = requireUser(context, request);
25 const endpoint = new URL(request.url).searchParams.get("endpoint");
26 const status = await client().status(user, endpoint);
27 return Response.json(status, { headers: { "cache-control": "no-store" } });
28}
29
30type Sent = {
31 intent?: string;
32 subscription?: PushSubscriptionJson & { expirationTime?: number | null };
33 endpoint?: string;
34 preferences?: NotifyPreferencesChange;
35 change?: PresenceChange | SoundSettingsChange;
36};
37
38export async function action({ context, request }: Route.ActionArgs) {
39 assertSameOrigin(request);
40 const user = requireUser(context, request);
41 const sent = (await request.json().catch(() => ({}))) as Sent;
42 const notify = client();
43 switch (sent.intent) {
44 case "subscribe": {
45 const s = sent.subscription;
46 if (!s?.endpoint || !s.keys?.p256dh || !s.keys?.auth) return Response.json({ ok: false }, { status: 400 });
47 const subscription: PushSubscriptionJson = { endpoint: s.endpoint, keys: { p256dh: s.keys.p256dh, auth: s.keys.auth } };
48 return Response.json(await notify.subscribe(user, subscription, request.headers.get("user-agent")));
49 }
50 case "unsubscribe":
51 return Response.json(await notify.unsubscribe(user, String(sent.endpoint ?? "")));
52 case "preferences":
53 return Response.json(await notify.setPreferences(user, sent.preferences ?? {}));
54 case "test":
55 return Response.json(await notify.test(user));
56 case "sounds": {
57 const saved = await identity.setSoundSettings(user, (sent.change ?? {}) as SoundSettingsChange);
58 if (!saved.ok) return Response.json({ ok: false, error: saved.error.message }, { status: 400 });
59 return Response.json(saved.value);
60 }
61 case "presence": {
62 // Set by hand here: a calendar's or an integration's comes through their own door.
63 const change = (sent.change ?? {}) as PresenceChange;
64 const status = change.status ? { ...change.status, source: "manual" as const } : change.status;
65 return Response.json(await notify.setPresence(user, { ...change, ...("status" in change ? { status } : {}) }));
66 }
67 default:
68 return Response.json({ ok: false }, { status: 400 });
69 }
70}