diff --git a/public/sw.js b/public/sw.js index 5d6180a3..8fb13834 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,8 +1,9 @@ /* Prabhupāda Says — Web Push service worker. * * Handles incoming push messages (shows a notification) and clicks (focuses - * an existing tab or opens the target URL). The push payload is the JSON the - * BFF sends: { title, body, url, icon, tag }. + * an existing same-origin tab and navigates it, or opens a new one). The push + * payload is the JSON the BFF sends: + * { title, body, url, icon, tag, require_interaction } */ self.addEventListener("install", () => self.skipWaiting()); @@ -11,38 +12,68 @@ self.addEventListener("activate", (event) => ); self.addEventListener("push", (event) => { - let data = {}; - try { - data = event.data ? event.data.json() : {}; - } catch { - data = { body: event.data && event.data.text ? event.data.text() : "" }; - } - const title = data.title || "Prabhupāda Says"; - const options = { - body: data.body || "", - icon: data.icon || "/logo_dark.png", - badge: "/logo_dark.png", - tag: data.tag || undefined, - data: { url: data.url || "/" }, - }; - event.waitUntil(self.registration.showNotification(title, options)); + event.waitUntil( + (async () => { + let data = {}; + if (event.data) { + try { + // event.data.json() is synchronous; text() is a Promise — only + // awaited in the fallback. + data = event.data.json(); + } catch { + data = { body: await event.data.text() }; + } + } + + const title = data.title || "Prabhupāda Says"; + const options = { + body: data.body || "", + icon: data.icon || "/icon-192.png", + badge: "/icon-192.png", + // A tag dedupes a series (e.g. daily reminders replace the previous + // one); renotify re-alerts when a tagged notification is replaced. + tag: data.tag || undefined, + renotify: !!data.tag, + // Reminders set this so they stay until acted on (desktop otherwise + // auto-dismisses). Defaults off for everything else. + requireInteraction: data.require_interaction === true, + vibrate: [100, 50, 100], + data: { url: data.url || "/" }, + actions: [{ action: "open", title: "Open" }], + }; + await self.registration.showNotification(title, options); + })(), + ); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); - const url = - (event.notification.data && event.notification.data.url) || "/"; + // The only action is "open"; the default body click behaves the same. + const url = (event.notification.data && event.notification.data.url) || "/"; + const target = new URL(url, self.location.origin).href; + event.waitUntil( - self.clients - .matchAll({ type: "window", includeUncontrolled: true }) - .then((list) => { - for (const client of list) { - if ("focus" in client) { - if ("navigate" in client) client.navigate(url); - return client.focus(); + (async () => { + const list = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + // Reuse a single existing same-origin tab — focus it and navigate only + // that one (not every matched window). + for (const client of list) { + if (client.url.startsWith(self.location.origin)) { + await client.focus(); + if ("navigate" in client) { + try { + await client.navigate(target); + } catch { + /* cross-document navigation can reject; focus already happened */ + } } + return; } - if (self.clients.openWindow) return self.clients.openWindow(url); - }), + } + if (self.clients.openWindow) await self.clients.openWindow(target); + })(), ); });