/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ /// declare const self: ServiceWorkerGlobalScope & typeof globalThis & { skipWaiting(): void; __WB_MANIFEST: unknown; }; self.addEventListener('install', () => { self.skipWaiting(); }); self.addEventListener('activate', (event: ExtendableEvent) => { event.waitUntil(self.clients.claim()); }); self.addEventListener('message', (event: ExtendableMessageEvent) => { if (event.data?.type === 'SKIP_WAITING') { self.skipWaiting(); } else if (event.data?.type === 'APP_UPDATE_BADGE') { const rawCount = event.data?.count; let badgeCount: number | null = null; if (typeof rawCount === 'number' && Number.isFinite(rawCount)) { badgeCount = rawCount; } else if (typeof rawCount === 'string' && rawCount.length > 0) { const parsed = Number(rawCount); badgeCount = Number.isFinite(parsed) ? parsed : null; } event.waitUntil(updateAppBadge(badgeCount)); } }); interface PushPayload { title?: string; body?: string; icon?: string; badge?: string; data?: Record; } declare global { interface Navigator { setAppBadge?: (value?: number) => Promise; clearAppBadge?: () => Promise; } } const getBadgeCount = (payload: PushPayload): number | null => { const badgeValue = payload.data?.badge_count; if (typeof badgeValue === 'number' && Number.isFinite(badgeValue)) { return badgeValue; } if (typeof badgeValue === 'string' && badgeValue.length > 0) { const parsed = Number(badgeValue); return Number.isFinite(parsed) ? parsed : null; } return null; }; const updateAppBadge = async (count: number | null): Promise => { if (typeof navigator.setAppBadge !== 'function' && typeof navigator.clearAppBadge !== 'function') { return; } try { if (count !== null && count > 0) { if (typeof navigator.setAppBadge === 'function') { await navigator.setAppBadge(count); } } else if (typeof navigator.clearAppBadge === 'function') { await navigator.clearAppBadge(); } } catch (error) { console.error('[SW] Failed to update app badge', error); } }; const resolveTargetUrl = (url?: string): string | null => { if (!url) return null; if (url.startsWith('http://') || url.startsWith('https://')) { return url; } try { return new URL(url, self.location.origin).toString(); } catch { return null; } }; const postMessageToClients = async (message: Record): Promise> => { try { const clientList = (await self.clients.matchAll({ type: 'window', includeUncontrolled: true, })) as ReadonlyArray; for (const client of clientList) { client.postMessage(message); } return clientList; } catch (error) { console.error('[SW] Unable to broadcast to clients', error); return []; } }; const focusOrOpenClient = async (targetUrl: string, targetUserId?: string): Promise => { const message: Record = { type: 'NOTIFICATION_CLICK_NAVIGATE', url: targetUrl, }; if (targetUserId) { message.targetUserId = targetUserId; } const clientList = await postMessageToClients(message); const exact = clientList.find((c) => c.url === targetUrl); if (exact) { await exact.focus(); return; } const sameOrigin = clientList.find((c) => { try { return new URL(c.url).origin === self.location.origin; } catch { return false; } }); if (sameOrigin) { await sameOrigin.focus(); return; } if (self.clients.openWindow) { await self.clients.openWindow(targetUrl); } }; self.addEventListener('push', (event: PushEvent) => { const payload: PushPayload = event.data?.json?.() ?? { title: 'Fluxer', }; const title = payload.title ?? 'Fluxer'; const options: NotificationOptions = { body: payload.body ?? undefined, icon: payload.icon ?? undefined, badge: payload.badge ?? undefined, data: payload.data ?? undefined, }; const badgeCount = getBadgeCount(payload); event.waitUntil( (async () => { await Promise.all([self.registration.showNotification(title, options), updateAppBadge(badgeCount)]); })(), ); }); self.addEventListener('notificationclick', (event: NotificationEvent) => { event.notification.close(); const targetUrl = resolveTargetUrl(event.notification.data?.url as string | undefined); if (!targetUrl) return; const targetUserId = event.notification.data?.target_user_id as string | undefined; event.waitUntil( (async () => { await focusOrOpenClient(targetUrl, targetUserId); })(), ); }); self.addEventListener('pushsubscriptionchange', (event: ExtendableEvent) => { event.waitUntil( postMessageToClients({type: 'PUSH_SUBSCRIPTION_CHANGE'}) .then(() => undefined) .catch(() => undefined), ); });