social overhaul

This commit is contained in:
2026-07-22 15:47:34 -07:00
parent 9db3fa697d
commit 86cda294f9
45 changed files with 3026 additions and 430 deletions
+144
View File
@@ -0,0 +1,144 @@
import { useCallback, useEffect, useState } from "react";
const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY as
| string
| undefined;
function urlBase64ToBuffer(base64String: string): ArrayBuffer {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const raw = atob(base64);
const buffer = new ArrayBuffer(raw.length);
const output = new Uint8Array(buffer);
for (let i = 0; i < raw.length; i++) output[i] = raw.charCodeAt(i);
return buffer;
}
export type PushPermission = "default" | "granted" | "denied";
export interface UseWebPush {
supported: boolean;
configured: boolean;
permission: PushPermission;
subscribed: boolean;
busy: boolean;
enable: () => Promise<boolean>;
disable: () => Promise<boolean>;
sendTest: () => Promise<boolean>;
}
async function getRegistration(): Promise<ServiceWorkerRegistration | null> {
if (!("serviceWorker" in navigator)) return null;
return (
(await navigator.serviceWorker.getRegistration()) ??
(await navigator.serviceWorker.ready.catch(() => null))
);
}
export function useWebPush(): UseWebPush {
const [supported, setSupported] = useState(false);
const [permission, setPermission] = useState<PushPermission>("default");
const [subscribed, setSubscribed] = useState(false);
const [busy, setBusy] = useState(false);
const configured = Boolean(VAPID_PUBLIC_KEY);
useEffect(() => {
const isSupported =
typeof window !== "undefined" &&
"Notification" in window &&
"PushManager" in window &&
"serviceWorker" in navigator;
setSupported(isSupported);
if (!isSupported) return;
setPermission(Notification.permission as PushPermission);
(async () => {
const reg = await getRegistration();
const sub = await reg?.pushManager.getSubscription();
setSubscribed(Boolean(sub));
})();
}, []);
const enable = useCallback(async (): Promise<boolean> => {
if (!supported || !configured || !VAPID_PUBLIC_KEY) return false;
setBusy(true);
try {
const perm = await Notification.requestPermission();
setPermission(perm as PushPermission);
if (perm !== "granted") return false;
const reg = await getRegistration();
if (!reg) return false;
let sub = await reg.pushManager.getSubscription();
if (!sub) {
sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToBuffer(VAPID_PUBLIC_KEY),
});
}
const res = await fetch("/api/push/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ subscription: sub.toJSON() }),
});
if (!res.ok) return false;
setSubscribed(true);
return true;
} catch {
return false;
} finally {
setBusy(false);
}
}, [supported, configured]);
const disable = useCallback(async (): Promise<boolean> => {
if (!supported) return false;
setBusy(true);
try {
const reg = await getRegistration();
const sub = await reg?.pushManager.getSubscription();
if (sub) {
await fetch("/api/push/unsubscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ endpoint: sub.endpoint }),
}).catch(() => {});
await sub.unsubscribe().catch(() => {});
}
setSubscribed(false);
return true;
} catch {
return false;
} finally {
setBusy(false);
}
}, [supported]);
const sendTest = useCallback(async (): Promise<boolean> => {
setBusy(true);
try {
const res = await fetch("/api/push/test", { method: "POST" });
return res.ok;
} catch {
return false;
} finally {
setBusy(false);
}
}, []);
return {
supported,
configured,
permission,
subscribed,
busy,
enable,
disable,
sendTest,
};
}