CI / Build and push docker image (push) Successful in 1m42s
Add an opt-in bell to the game HUD: the first click asks for the browser's notification permission, and the choice is remembered per device. A play by any other seat then raises a desktop notification with the player's name, the card and the next player, while the page is in the background and the viewer is actually seated in the game. Detect the newest play across the game families: scopone and scopa report it in last_move, while briscola and briscola chiamata only expose the trick, which mid-trick or once completed ends with the card just played. The first snapshot after every (re)connection is a resync baseline, so reconnects and the initial load never replay old moves; so are the viewer's own plays and the server's auto-plays. Deliver through the service worker registration (installed PWAs on Android reject page-context notifications), falling back to a page notification when no worker is active, under a per-game tag that replaces the previous move. sw.js now focuses the game window when a notification is clicked.
74 lines
2.6 KiB
JavaScript
74 lines
2.6 KiB
JavaScript
// Tavolo service worker: installability and notification clicks, no
|
|
// offline caching.
|
|
//
|
|
// The app is a thin client for a live game server: state arrives over
|
|
// /api and /ws and is never safe to replay from a cache, and a new build
|
|
// changes the hashed asset names, so stale cached files would break the
|
|
// app until the cache expired. The worker therefore only performs the
|
|
// passthrough fetch below — enough for the browser to treat the app as
|
|
// installable — and leaves every request's caching to the network stack.
|
|
// It also handles clicks on the move notifications raised by the game
|
|
// page (see src/notify.rs).
|
|
|
|
self.addEventListener("install", () => {
|
|
// A new worker takes over on the next reload instead of waiting for
|
|
// every tab of the old version to close.
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil(self.clients.claim());
|
|
});
|
|
|
|
// Clicking a move notification focuses the game: an existing window on
|
|
// the game is focused as is, any other window of the app is navigated
|
|
// there, and when the app is closed a new window is opened.
|
|
self.addEventListener("notificationclick", (event) => {
|
|
event.notification.close();
|
|
const url = (event.notification.data && event.notification.data.url) || "/";
|
|
event.waitUntil(
|
|
(async () => {
|
|
const target = new URL(url, self.location.origin);
|
|
const windows = await self.clients.matchAll({
|
|
type: "window",
|
|
includeUncontrolled: true,
|
|
});
|
|
const onGame = windows.find((client) => {
|
|
const current = new URL(client.url);
|
|
return current.origin === target.origin && current.pathname === target.pathname;
|
|
});
|
|
if (onGame) {
|
|
return onGame.focus();
|
|
}
|
|
const anyWindow = windows.find(
|
|
(client) => new URL(client.url).origin === target.origin,
|
|
);
|
|
if (anyWindow) {
|
|
await anyWindow.focus();
|
|
try {
|
|
return await anyWindow.navigate(url);
|
|
} catch {
|
|
return anyWindow;
|
|
}
|
|
}
|
|
return self.clients.openWindow(url);
|
|
})(),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const url = new URL(event.request.url);
|
|
// Let the browser handle other origins and the live endpoints directly
|
|
// (respondWith would only add a hop; /auth navigations must be allowed
|
|
// to follow their redirects untouched).
|
|
if (
|
|
url.origin !== self.location.origin ||
|
|
url.pathname.startsWith("/api/") ||
|
|
url.pathname.startsWith("/auth/") ||
|
|
url.pathname.startsWith("/ws")
|
|
) {
|
|
return;
|
|
}
|
|
event.respondWith(fetch(event.request));
|
|
});
|