/// import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'; export type {}; declare const self: ServiceWorkerGlobalScope; const DEFAULT_NOTIFICATION_ICON = '/public/res/apple/apple-touch-icon-180x180.png'; const DEFAULT_NOTIFICATION_BADGE = '/public/res/apple-touch-icon-72x72.png'; const pendingReplies = new Map(); let messageIdCounter = 0; function sendAndWaitForReply(client: WindowClient, type: string, payload: object) { messageIdCounter += 1; const id = messageIdCounter; const promise = new Promise((resolve) => { pendingReplies.set(id, resolve); }); client.postMessage({ type, id, payload }); return promise; } function fetchConfig(token?: string): RequestInit | undefined { if (!token) return undefined; return { headers: { Authorization: `Bearer ${token}`, }, cache: 'default', }; } self.addEventListener('message', (event: ExtendableMessageEvent) => { if (event.data.type === 'togglePush') { const token = event.data?.token; const fetchOptions = fetchConfig(token); event.waitUntil( fetch(`${event.data.url}/_matrix/client/v3/pushers/set`, { method: 'POST', ...fetchOptions, body: JSON.stringify(event.data.pusherData), }) ); return; } const { replyTo } = event.data; if (replyTo) { const resolve = pendingReplies.get(replyTo); if (resolve) { pendingReplies.delete(replyTo); resolve(event.data.payload); } } }); self.addEventListener('activate', (event: ExtendableEvent) => { event.waitUntil( (async () => { await self.clients.claim(); })() ); }); self.addEventListener('install', (event: ExtendableEvent) => { event.waitUntil(self.skipWaiting()); }); self.addEventListener('fetch', (event: FetchEvent) => { const { url, method } = event.request; if (method !== 'GET') return; if ( !url.includes('/_matrix/client/v1/media/download') && !url.includes('/_matrix/client/v1/media/thumbnail') ) { return; } event.respondWith( (async (): Promise => { if (!event.clientId) throw new Error('Missing clientId'); const client = await self.clients.get(event.clientId); if (!client) throw new Error('Client not found'); const token = await sendAndWaitForReply(client, 'token', {}); if (!token) throw new Error('Failed to retrieve token'); const response = await fetch(url, fetchConfig(token)); if (!response.ok) throw new Error(`Fetch failed: ${response.statusText}`); return response; })() ); event.waitUntil( (async function () { console.log('Ensuring fetch processing completes before worker termination.'); })() ); }); const onPushNotification = async (event: PushEvent) => { let title = 'New Notification'; const options: NotificationOptions = { body: 'You have a new message!', icon: DEFAULT_NOTIFICATION_ICON, badge: DEFAULT_NOTIFICATION_BADGE, data: { url: self.registration.scope, timestamp: Date.now(), }, // tag: 'cinny-notification-tag', // Optional: Replaces existing notification with same tag // renotify: true, // Optional: If using tag, renotify will alert user even if tag matches // silent: false, // Optional: Set to true for no sound/vibration. User can also set this. }; if (event.data) { try { const pushData = event.data.json(); title = pushData.title || title; options.body = options.body ?? pushData.data.toString(); options.icon = pushData.icon || options.icon; options.badge = pushData.badge || options.badge; if (pushData.image) options.image = pushData.image; if (pushData.vibrate) options.vibrate = pushData.vibrate; if (pushData.actions) options.actions = pushData.actions; options.tag = 'Cinny'; if (typeof pushData.renotify === 'boolean') options.renotify = pushData.renotify; if (typeof pushData.silent === 'boolean') options.silent = pushData.silent; if (pushData.data) { options.data = { ...options.data, ...pushData.data }; } if (typeof pushData.unread === 'number') { await self.navigator.setAppBadge(pushData.unread); } else { await navigator.clearAppBadge(); } } catch (e) { const pushText = event.data.text(); options.body = pushText || options.body; } } return self.registration.showNotification(title, options); }; self.addEventListener('push', (event: PushEvent) => event.waitUntil(onPushNotification(event))); self.addEventListener('notificationclick', (event: NotificationEvent) => { event.notification.close(); /** * We should likely add a postMessage back to navigate to the room the event is from */ const targetUrl = event.notification.data?.url || self.registration.scope; event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { for (const client of clientList) { if (client.url === targetUrl && 'focus' in client) { return (client as WindowClient).focus(); } } if (self.clients.openWindow) { return self.clients.openWindow(targetUrl); } return Promise.resolve(); }) ); }); if (self.__WB_MANIFEST) { precacheAndRoute(self.__WB_MANIFEST); } cleanupOutdatedCaches();