| 1 | // g1t's service worker: browser notifications, nothing else. It caches |
| 2 | // nothing and answers no requests, so pages load exactly as without it. |
| 3 | // Pushes come from the notify service (services/notify), encrypted for |
| 4 | // this browser: { title, body, href, tag, kind, urgent, workspace, actions, |
| 5 | // card }. A notification's buttons are links: each opens its page. Actions |
| 6 | // without a link (from cards that were pressed in place) are left off. |
| 7 | |
| 8 | self.addEventListener("install", () => self.skipWaiting()); |
| 9 | self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim())); |
| 10 | |
| 11 | self.addEventListener("push", (event) => { |
| 12 | let data = {}; |
| 13 | try { |
| 14 | data = event.data ? event.data.json() : {}; |
| 15 | } catch { |
| 16 | data = { title: "g1t", body: event.data ? event.data.text() : "" }; |
| 17 | } |
| 18 | const title = data.title || "g1t"; |
| 19 | const safe = (href) => (typeof href === "string" && href.startsWith("/") && !href.startsWith("//") ? href : null); |
| 20 | const actions = Array.isArray(data.actions) |
| 21 | ? data.actions |
| 22 | .filter((a) => a && typeof a.id === "string" && typeof a.label === "string" && safe(a.href)) |
| 23 | .slice(0, 2) |
| 24 | .map((a) => ({ id: a.id, label: a.label, href: safe(a.href) })) |
| 25 | : []; |
| 26 | event.waitUntil( |
| 27 | self.registration.showNotification(title, { |
| 28 | body: data.body || "", |
| 29 | icon: "/icon-192.png", |
| 30 | badge: "/badge-96.png", |
| 31 | // One notification per conversation: a newer message replaces the last. |
| 32 | tag: data.tag || undefined, |
| 33 | renotify: Boolean(data.tag), |
| 34 | actions: actions.map((a) => ({ action: a.id, title: a.label })), |
| 35 | data: { href: safe(data.href) || "/", actions }, |
| 36 | }), |
| 37 | ); |
| 38 | }); |
| 39 | |
| 40 | self.addEventListener("notificationclick", (event) => { |
| 41 | event.notification.close(); |
| 42 | const data = event.notification.data || {}; |
| 43 | const pressed = event.action && Array.isArray(data.actions) ? data.actions.find((a) => a.id === event.action) : null; |
| 44 | const href = (pressed && pressed.href) || data.href || "/"; |
| 45 | const target = new URL(href, self.location.origin).href; |
| 46 | event.waitUntil( |
| 47 | (async () => { |
| 48 | const open = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); |
| 49 | // A tab already on g1t: brought forward and moved there. |
| 50 | const tab = open.find((client) => new URL(client.url).origin === self.location.origin); |
| 51 | if (tab) { |
| 52 | await tab.focus(); |
| 53 | if ("navigate" in tab) { |
| 54 | try { |
| 55 | await tab.navigate(target); |
| 56 | return; |
| 57 | } catch { |
| 58 | // Not ours to navigate (uncontrolled): told instead. |
| 59 | } |
| 60 | } |
| 61 | tab.postMessage({ type: "g1t:navigate", href }); |
| 62 | return; |
| 63 | } |
| 64 | await self.clients.openWindow(target); |
| 65 | })(), |
| 66 | ); |
| 67 | }); |