added on local at 2026-07-01 12:34:43
| 1 | // PTMatrix service worker — minimal push handler. | |
| 2 | // Receives empty (no-payload) pushes from PTMatrix's WebPush module, then | |
| 3 | // fetches the latest unread notification from /notifications.cgi to decide | |
| 4 | // what to show. Empty-payload push avoids RFC 8291 payload encryption | |
| 5 | // entirely so the server-side stays a thin curl+openssl shim. | |
| 6 | // | |
| 7 | // Versioned in the URL (?v=20260618) so wrapper template can bust this. | |
| 8 | ||
| 9 | self.addEventListener('install', (e) => { | |
| 10 | self.skipWaiting(); | |
| 11 | }); | |
| 12 | self.addEventListener('activate', (e) => { | |
| 13 | e.waitUntil(self.clients.claim()); | |
| 14 | }); | |
| 15 | ||
| 16 | async function buildNotification(event) { | |
| 17 | let data = {}; | |
| 18 | if (event.data) { | |
| 19 | try { data = event.data.json(); } catch(_) { | |
| 20 | try { data = { body: event.data.text() }; } catch(_) {} | |
| 21 | } | |
| 22 | } | |
| 23 | // Empty-payload push: ask the server what to show. | |
| 24 | if (!data || (!data.title && !data.body)) { | |
| 25 | try { | |
| 26 | const r = await fetch('/notifications.cgi?act=latest_unread', { credentials: 'include' }); | |
| 27 | if (r.ok) { | |
| 28 | const j = await r.json(); | |
| 29 | if (j && j.ok && j.item) { | |
| 30 | data.title = j.item.title || 'PTMatrix'; | |
| 31 | data.body = j.item.body || ''; | |
| 32 | data.url = j.item.url || '/notifications.cgi'; | |
| 33 | data.tag = 'ptm-' + (j.item.id || Date.now()); | |
| 34 | data.unread_count = j.unread_count; | |
| 35 | } | |
| 36 | } | |
| 37 | } catch(_) {} | |
| 38 | } | |
| 39 | const title = data.title || 'PTMatrix'; | |
| 40 | const opts = { | |
| 41 | body: data.body || 'You have a new notification.', | |
| 42 | data: { url: data.url || '/notifications.cgi' }, | |
| 43 | icon: '/assets/images/icon-192.png', | |
| 44 | badge: '/assets/images/badge-72.png', | |
| 45 | tag: data.tag || 'ptmatrix', | |
| 46 | renotify: true, | |
| 47 | }; | |
| 48 | // Badge API: keep the unread count visible on the home-screen icon (iOS PWA). | |
| 49 | if (typeof data.unread_count === 'number' && 'setAppBadge' in self) { | |
| 50 | try { self.setAppBadge(data.unread_count); } catch(_) {} | |
| 51 | } | |
| 52 | return self.registration.showNotification(title, opts); | |
| 53 | } | |
| 54 | ||
| 55 | self.addEventListener('push', (event) => { | |
| 56 | event.waitUntil(buildNotification(event)); | |
| 57 | }); | |
| 58 | ||
| 59 | self.addEventListener('notificationclick', (event) => { | |
| 60 | event.notification.close(); | |
| 61 | const target = (event.notification.data && event.notification.data.url) || '/notifications.cgi'; | |
| 62 | event.waitUntil((async () => { | |
| 63 | const wins = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); | |
| 64 | for (const w of wins) { | |
| 65 | try { | |
| 66 | if (w.url && w.url.indexOf(self.location.origin) === 0) { | |
| 67 | await w.focus(); | |
| 68 | if (w.navigate) { try { await w.navigate(target); } catch(_) {} } | |
| 69 | return; | |
| 70 | } | |
| 71 | } catch(_) {} | |
| 72 | } | |
| 73 | await self.clients.openWindow(target); | |
| 74 | })()); | |
| 75 | }); |