Files
TX/artifacts/tx-os/public/sw.js
T
Replit Agent ceadff6944 Improve notification deep-linking and add Arabic meeting reminders
Enhance notification handling to preserve deep-link parameters, add relatedId and relatedType to notifications for better association, and implement Arabic pluralization for meeting reminders.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9abb57f5-4f2c-4eed-9fad-ab278885083e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fKZh36i
Replit-Helium-Checkpoint-Created: true
2026-06-01 14:48:26 +00:00

93 lines
3.0 KiB
JavaScript

/* Tx OS service worker — Web Push only.
*
* Scope: registered with `scope: BASE_URL`, so the SW only controls
* paths under the SPA's base path (e.g. `/`, or `/tx-os/` in a future
* subpath deployment). Every URL the SW touches — icons, navigation
* targets — is resolved against `self.registration.scope` so the same
* code works at root and at a subpath without code changes.
*
* We deliberately do NOT cache app assets here — Tx OS is self-hosted
* on a single machine, so the network is fast and reliable. Adding
* cache layers would risk shipping stale React bundles after an
* upgrade.
*/
function scopedUrl(relative) {
// self.registration.scope is the full origin + base path with a
// trailing slash (e.g. "https://host/" or "https://host/tx-os/").
// Strip a leading slash from `relative` so URL() treats it as a
// path under scope rather than origin-relative.
const cleaned = String(relative || "").replace(/^\/+/, "");
const resolved = new URL(cleaned, self.registration.scope);
// Preserve the query string and hash so notification deep-links
// (e.g. "/meetings?meeting=12" or "/notes?thread=7") survive the click
// handler — taking only .pathname would drop them and land the user on
// the generic page instead of the specific item.
return resolved.pathname + resolved.search + resolved.hash;
}
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("push", (event) => {
/** @type {{title?: string, body?: string, tag?: string, url?: string, type?: string}} */
let payload = {};
try {
payload = event.data ? event.data.json() : {};
} catch {
payload = { title: event.data ? event.data.text() : "Tx OS" };
}
const title = payload.title || "Tx OS";
const iconUrl = scopedUrl("icons/icon-192.png");
const options = {
body: payload.body || "",
tag: payload.tag || payload.type || "tx-os",
renotify: true,
icon: iconUrl,
badge: iconUrl,
data: {
url: payload.url || "/",
type: payload.type || null,
},
};
event.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const rawTarget = (event.notification.data && event.notification.data.url) || "/";
const targetPath = scopedUrl(rawTarget);
event.waitUntil(
(async () => {
const allClients = await self.clients.matchAll({
type: "window",
includeUncontrolled: true,
});
// Prefer an already-open Tx OS tab under our scope; navigate it
// to the target and focus.
for (const client of allClients) {
try {
await client.focus();
if ("navigate" in client) {
await client.navigate(targetPath);
}
return;
} catch {
/* try next */
}
}
if (self.clients.openWindow) {
await self.clients.openWindow(targetPath);
}
})(),
);
});