Create sdks/web/ with a vanilla TypeScript web client: - IndexedDB local store for conversations, messages, and identity - WebSocket transport connecting to the server bridge - Service Worker with cache-first strategy for offline support - PWA manifest for installable web app - Dark-themed responsive UI with sidebar, messages, and input bar - Connection status badge and MLS epoch indicator in header - Unread message count badges on conversations
97 lines
2.3 KiB
JavaScript
97 lines
2.3 KiB
JavaScript
/**
|
|
* quicproquo Service Worker — provides offline caching and background
|
|
* notification support for the PWA.
|
|
*/
|
|
|
|
const CACHE_NAME = "qpq-cache-v1";
|
|
const STATIC_ASSETS = [
|
|
"/",
|
|
"/index.html",
|
|
"/style.css",
|
|
"/app.js",
|
|
"/manifest.json",
|
|
];
|
|
|
|
// Install: cache static assets.
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)),
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate: clean up old caches.
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(
|
|
keys
|
|
.filter((key) => key !== CACHE_NAME)
|
|
.map((key) => caches.delete(key)),
|
|
),
|
|
),
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
// Fetch: serve from cache, fall back to network.
|
|
self.addEventListener("fetch", (event) => {
|
|
// Only cache GET requests for same-origin resources.
|
|
if (event.request.method !== "GET") return;
|
|
|
|
event.respondWith(
|
|
caches.match(event.request).then((cached) => {
|
|
if (cached) return cached;
|
|
return fetch(event.request).then((response) => {
|
|
// Cache successful same-origin responses.
|
|
if (
|
|
response.ok &&
|
|
new URL(event.request.url).origin === self.location.origin
|
|
) {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
cache.put(event.request, clone);
|
|
});
|
|
}
|
|
return response;
|
|
});
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Push notifications (future: wired to server push events).
|
|
self.addEventListener("push", (event) => {
|
|
if (!event.data) return;
|
|
|
|
let payload;
|
|
try {
|
|
payload = event.data.json();
|
|
} catch {
|
|
payload = { title: "quicproquo", body: event.data.text() };
|
|
}
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(payload.title || "quicproquo", {
|
|
body: payload.body || "New message",
|
|
icon: "/icon-192.png",
|
|
badge: "/icon-192.png",
|
|
tag: payload.conversationId || "default",
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Notification click: open the app.
|
|
self.addEventListener("notificationclick", (event) => {
|
|
event.notification.close();
|
|
event.waitUntil(
|
|
self.clients
|
|
.matchAll({ type: "window" })
|
|
.then((clients) => {
|
|
if (clients.length > 0) {
|
|
return clients[0].focus();
|
|
}
|
|
return self.clients.openWindow("/");
|
|
}),
|
|
);
|
|
});
|