203 lines
5.3 KiB
TypeScript
203 lines
5.3 KiB
TypeScript
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/// <reference lib="webworker" />
|
|
|
|
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<string, unknown>;
|
|
}
|
|
|
|
declare global {
|
|
interface Navigator {
|
|
setAppBadge?: (value?: number) => Promise<void>;
|
|
clearAppBadge?: () => Promise<void>;
|
|
}
|
|
}
|
|
|
|
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<void> => {
|
|
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<string, unknown>): Promise<ReadonlyArray<WindowClient>> => {
|
|
try {
|
|
const clientList = (await self.clients.matchAll({
|
|
type: 'window',
|
|
includeUncontrolled: true,
|
|
})) as ReadonlyArray<WindowClient>;
|
|
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<void> => {
|
|
const message: Record<string, unknown> = {
|
|
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),
|
|
);
|
|
});
|