a demonstration replicated social networking web app built with anproto wiredove.net/
social ed25519 protocols
at master 159 lines 4.9 kB view raw
1function urlBase64ToUint8Array(base64String) { 2 const padding = "=".repeat((4 - (base64String.length % 4)) % 4); 3 const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); 4 const raw = atob(base64); 5 const output = new Uint8Array(raw.length); 6 for (let i = 0; i < raw.length; i += 1) { 7 output[i] = raw.charCodeAt(i); 8 } 9 return output; 10} 11 12async function ensureServiceWorker(serviceWorkerUrl) { 13 if (!("serviceWorker" in navigator)) { 14 throw new Error("Service Worker not supported in this browser"); 15 } 16 const registration = await navigator.serviceWorker.register(serviceWorkerUrl); 17 return registration; 18} 19 20async function getPublicKey(vapidKeyUrl) { 21 const res = await fetch(vapidKeyUrl); 22 if (!res.ok) throw new Error("Failed to load VAPID public key"); 23 const data = await res.json(); 24 return data.key; 25} 26 27async function showLocalNotification(title, body, iconUrl) { 28 if (!("Notification" in window) || Notification.permission !== "granted") { 29 return; 30 } 31 const registration = await navigator.serviceWorker.getRegistration(); 32 if (!registration) return; 33 await registration.showNotification(title, { body, icon: iconUrl }); 34} 35 36export function notificationsButton(options = {}) { 37 const { 38 className = "notifications-link", 39 iconOn = "notifications_active", 40 iconOff = "notifications", 41 titleOn = "Turn off notifications", 42 titleOff = "Turn on notifications", 43 serviceWorkerUrl = "/sw.js", 44 vapidKeyUrl = "/vapid-public-key", 45 subscribeUrl = "/subscribe", 46 unsubscribeUrl = "/unsubscribe", 47 iconUrl = "/favicon.ico", 48 welcomeTitle = "Welcome to Wiredove", 49 welcomeBody = "Your notifications are on.", 50 goodbyeTitle = "Goodbye from Wiredove!", 51 goodbyeBody = "Your notifications are off.", 52 onStatus, 53 onToggle, 54 } = options; 55 56 const button = document.createElement("a"); 57 button.href = "#"; 58 button.className = className; 59 button.title = titleOff; 60 button.setAttribute("aria-label", titleOff); 61 62 const icon = document.createElement("span"); 63 icon.className = "material-symbols-outlined"; 64 icon.setAttribute("aria-hidden", "true"); 65 icon.textContent = iconOff; 66 button.appendChild(icon); 67 68 function setStatus(text) { 69 if (onStatus) onStatus(text); 70 } 71 72 function setState(enabled) { 73 button.dataset.enabled = enabled ? "true" : "false"; 74 icon.textContent = enabled ? iconOn : iconOff; 75 const title = enabled ? titleOn : titleOff; 76 button.title = title; 77 button.setAttribute("aria-label", title); 78 if (onToggle) onToggle(enabled); 79 } 80 81 async function subscribe() { 82 setStatus("requesting permission"); 83 const permission = await Notification.requestPermission(); 84 if (permission !== "granted") { 85 setStatus("permission denied"); 86 return; 87 } 88 89 const registration = await ensureServiceWorker(serviceWorkerUrl); 90 const key = await getPublicKey(vapidKeyUrl); 91 const subscription = await registration.pushManager.subscribe({ 92 userVisibleOnly: true, 93 applicationServerKey: urlBase64ToUint8Array(key), 94 }); 95 96 const res = await fetch(subscribeUrl, { 97 method: "POST", 98 headers: { "content-type": "application/json" }, 99 body: JSON.stringify(subscription), 100 }); 101 102 if (!res.ok) throw new Error("Subscribe failed"); 103 setStatus("subscribed"); 104 setState(true); 105 await showLocalNotification(welcomeTitle, welcomeBody, iconUrl); 106 } 107 108 async function unsubscribe() { 109 const registration = await navigator.serviceWorker.getRegistration(); 110 if (!registration) { 111 setStatus("no service worker"); 112 return; 113 } 114 115 const subscription = await registration.pushManager.getSubscription(); 116 if (!subscription) { 117 setStatus("not subscribed"); 118 return; 119 } 120 121 await subscription.unsubscribe(); 122 await fetch(unsubscribeUrl, { 123 method: "POST", 124 headers: { "content-type": "application/json" }, 125 body: JSON.stringify({ endpoint: subscription.endpoint }), 126 }); 127 128 setStatus("unsubscribed"); 129 setState(false); 130 await showLocalNotification(goodbyeTitle, goodbyeBody, iconUrl); 131 } 132 133 async function refresh() { 134 if (!("serviceWorker" in navigator)) { 135 setState(false); 136 return; 137 } 138 139 const registration = await navigator.serviceWorker.getRegistration(); 140 const subscription = registration 141 ? await registration.pushManager.getSubscription() 142 : null; 143 setState(!!subscription); 144 } 145 146 button.addEventListener("click", (event) => { 147 event.preventDefault(); 148 const enabled = button.dataset.enabled === "true"; 149 const action = enabled ? unsubscribe : subscribe; 150 action().catch((err) => { 151 console.error(err); 152 setStatus(enabled ? "unsubscribe failed" : "subscribe failed"); 153 }); 154 }); 155 156 button.refresh = refresh; 157 refresh().catch(() => setState(false)); 158 return button; 159}