// The service worker of one persona's Web Push subscription. wwwroot/js/push.js registers this script once per persona // the reader turns notifications on for, at the scope push/{slot}/ (a random name this device keeps in localStorage's // PushDevice, Services/PushService.cs), so each persona has a push subscription of its own: its own endpoint and keys, // which PrivaPub stores with that persona's token alone. The same file serves development and published builds; the // app's own workers (service-worker.js, service-worker.published.js) receive no pushes. It controls no page, so it has no // fetch handler. // // A push shows the payload's title, body and icon, tagged with its notification id so a repeat replaces it. The payload // is Mastodon's, so it also carries the subscription's access token: that is never read, kept, shown or passed on here. // A click opens decePub on the notifications of the persona this registration is for: an open decePub tab is focused and // told which (main.js, CascadingState.PushOpened), else a new one opens at ?notifications=1&push={slot}. const appRoot = new URL("./", self.location).href const slot = new URL(self.registration.scope).pathname.split("/").filter(part => part.length > 0).pop() self.addEventListener("install", () => self.skipWaiting()) self.addEventListener("push", event => event.waitUntil(show(event.data))) self.addEventListener("notificationclick", event => { event.notification.close() event.waitUntil(open()) }) // the push service changed the subscription: the same key subscribes again here, and the app tells PrivaPub the new // endpoint the next time it starts (PushService.Reconcile), since this worker holds no token to tell it with self.addEventListener("pushsubscriptionchange", event => event.waitUntil(renew(event))) async function show(data) { let title = "decePub" const options = { icon: appRoot + "imgs/icon-192.png", timestamp: Date.now(), data: {} } try { const payload = data ? data.json() : null if (payload && typeof payload == "object") { if (typeof payload.title == "string" && payload.title.length > 0) title = payload.title if (typeof payload.body == "string") options.body = payload.body if (isWebAddress(payload.icon)) options.icon = payload.icon if (payload.notification_id != null) options.tag = String(payload.notification_id) if (typeof payload.preferred_locale == "string") options.lang = payload.preferred_locale options.data = { notificationId: options.tag ?? null, type: typeof payload.notification_type == "string" ? payload.notification_type : null } } } catch (e) { console.error(e) } // a push always shows something (userVisibleOnly): one that can't be read shows decePub's name alone await self.registration.showNotification(title, options) try { // the count is the app's to say (Index.razor); until it opens, a dot says something waits await self.navigator.setAppBadge?.() } catch (e) { console.debug(e) } } function isWebAddress(value) { if (typeof value != "string") return false try { const url = new URL(value) return url.protocol === "https:" || url.protocol === "http:" } catch { return false } } async function open() { const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true }) const app = windows.filter(client => client.url.startsWith(appRoot)) const target = app.find(client => client.focused) ?? app.find(client => client.visibilityState === "visible") ?? app[0] if (target) { try { await target.focus() } catch (e) { console.error(e) } // (navigate() is only for the worker that controls the tab: the app's own one does) target.postMessage({ type: "decepub-push-open", slot }) return } const url = new URL(appRoot) url.searchParams.set("notifications", "1") url.searchParams.set("push", slot) await self.clients.openWindow(url.href) } async function renew(event) { try { const key = event.oldSubscription?.options?.applicationServerKey if (!key || event.newSubscription) return await self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key }) } catch (e) { console.error(e) } }