v0.0.10: Progressive Web App (PWA)
- Web manifest (standalone mode, theme, icon) - Service worker: caches shell (HTML, WASM, icon) for offline - SVG app icon (chat bubble with encryption indicator) - iOS meta tags: apple-mobile-web-app-capable, status bar style - Android: beforeinstallprompt → /install command - Offline fallback: loads cached shell, shows reconnecting state - Cache versioning with automatic old cache cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -2647,7 +2647,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-client"
|
name = "warzone-client"
|
||||||
version = "0.0.9"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2680,7 +2680,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-mule"
|
name = "warzone-mule"
|
||||||
version = "0.0.9"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2689,7 +2689,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.9"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
@@ -2712,7 +2712,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-server"
|
name = "warzone-server"
|
||||||
version = "0.0.9"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2739,7 +2739,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-wasm"
|
name = "warzone-wasm"
|
||||||
version = "0.0.9"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.9"
|
version = "0.0.10"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ pub fn routes() -> Router<AppState> {
|
|||||||
.route("/", get(web_ui))
|
.route("/", get(web_ui))
|
||||||
.route("/wasm/warzone_wasm.js", get(wasm_js))
|
.route("/wasm/warzone_wasm.js", get(wasm_js))
|
||||||
.route("/wasm/warzone_wasm_bg.wasm", get(wasm_binary))
|
.route("/wasm/warzone_wasm_bg.wasm", get(wasm_binary))
|
||||||
|
.route("/manifest.json", get(pwa_manifest))
|
||||||
|
.route("/sw.js", get(service_worker))
|
||||||
|
.route("/icon.svg", get(pwa_icon))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn web_ui() -> Html<&'static str> {
|
async fn web_ui() -> Html<&'static str> {
|
||||||
@@ -32,12 +35,85 @@ async fn wasm_binary() -> impl IntoResponse {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn pwa_manifest() -> impl IntoResponse {
|
||||||
|
([(header::CONTENT_TYPE, "application/manifest+json")], r##"{
|
||||||
|
"name": "Warzone Messenger",
|
||||||
|
"short_name": "Warzone",
|
||||||
|
"description": "End-to-end encrypted messenger",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0a0a1a",
|
||||||
|
"theme_color": "#0a0a1a",
|
||||||
|
"icons": [{"src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable"}]
|
||||||
|
}"##)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn service_worker() -> impl IntoResponse {
|
||||||
|
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
||||||
|
const CACHE = 'wz-v1';
|
||||||
|
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||||
|
|
||||||
|
self.addEventListener('install', e => {
|
||||||
|
e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL)));
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', e => {
|
||||||
|
e.waitUntil(caches.keys().then(keys =>
|
||||||
|
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
|
||||||
|
));
|
||||||
|
e.waitUntil(clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', e => {
|
||||||
|
const url = new URL(e.request.url);
|
||||||
|
// API calls: network only
|
||||||
|
if (url.pathname.startsWith('/v1/')) return;
|
||||||
|
// WS: skip
|
||||||
|
if (url.protocol === 'ws:' || url.protocol === 'wss:') return;
|
||||||
|
// Shell: cache first, network fallback
|
||||||
|
e.respondWith(
|
||||||
|
caches.match(e.request).then(cached => cached || fetch(e.request).then(resp => {
|
||||||
|
if (resp.ok && SHELL.includes(url.pathname)) {
|
||||||
|
const clone = resp.clone();
|
||||||
|
caches.open(CACHE).then(c => c.put(e.request, clone));
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}).catch(() => {
|
||||||
|
if (e.request.mode === 'navigate') {
|
||||||
|
return caches.match('/');
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
"##)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pwa_icon() -> impl IntoResponse {
|
||||||
|
([(header::CONTENT_TYPE, "image/svg+xml")], r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="100" fill="#0a0a1a"/>
|
||||||
|
<path d="M128 140h256c22 0 40 18 40 40v152c0 22-18 40-40 40H210l-70 52v-52h-12c-22 0-40-18-40-40V180c0-22 18-40 40-40z" fill="#e94560"/>
|
||||||
|
<circle cx="200" cy="256" r="18" fill="#0a0a1a"/>
|
||||||
|
<circle cx="256" cy="256" r="18" fill="#0a0a1a"/>
|
||||||
|
<circle cx="312" cy="256" r="18" fill="#0a0a1a"/>
|
||||||
|
<rect x="180" y="340" width="30" height="8" rx="4" fill="#4ade80"/>
|
||||||
|
<rect x="220" y="340" width="30" height="8" rx="4" fill="#4ade80"/>
|
||||||
|
</svg>"##)
|
||||||
|
}
|
||||||
|
|
||||||
const WEB_HTML: &str = r##"<!DOCTYPE html>
|
const WEB_HTML: &str = r##"<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
|
||||||
<meta name="theme-color" content="#0a0a1a">
|
<meta name="theme-color" content="#0a0a1a">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Warzone">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/icon.svg">
|
||||||
<title>Warzone</title>
|
<title>Warzone</title>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@@ -160,7 +236,7 @@ let pollTimer = null;
|
|||||||
let ws = null; // WebSocket connection
|
let ws = null; // WebSocket connection
|
||||||
let wasmReady = false;
|
let wasmReady = false;
|
||||||
|
|
||||||
const VERSION = '0.0.9';
|
const VERSION = '0.0.10';
|
||||||
let DEBUG = true; // toggle with /debug command
|
let DEBUG = true; // toggle with /debug command
|
||||||
|
|
||||||
// ── Receipt tracking ──
|
// ── Receipt tracking ──
|
||||||
@@ -684,6 +760,11 @@ async function doSend() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text === '/clear') { $messages.innerHTML = ''; return; }
|
if (text === '/clear') { $messages.innerHTML = ''; return; }
|
||||||
|
if (text === '/install') {
|
||||||
|
if (deferredInstall) { deferredInstall.prompt(); deferredInstall = null; }
|
||||||
|
else addSys('Install not available (already installed or not supported)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (text === '/debug') { DEBUG = !DEBUG; addSys('Debug logging: ' + (DEBUG ? 'ON (check browser console)' : 'OFF')); return; }
|
if (text === '/debug') { DEBUG = !DEBUG; addSys('Debug logging: ' + (DEBUG ? 'ON (check browser console)' : 'OFF')); return; }
|
||||||
if (text === '/reset') {
|
if (text === '/reset') {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@@ -812,6 +893,17 @@ document.getElementById('btn-recover').onclick = () => doRecover();
|
|||||||
document.getElementById('btn-enter').onclick = () => enterChat();
|
document.getElementById('btn-enter').onclick = () => enterChat();
|
||||||
document.getElementById('send-btn').onclick = () => doSend();
|
document.getElementById('send-btn').onclick = () => doSend();
|
||||||
|
|
||||||
|
// PWA: service worker + install prompt
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js').catch(e => dbg('SW failed:', e));
|
||||||
|
}
|
||||||
|
let deferredInstall = null;
|
||||||
|
window.addEventListener('beforeinstallprompt', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
deferredInstall = e;
|
||||||
|
addSys('Tip: install as app for fullscreen + notifications. Type /install');
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize WASM and auto-load
|
// Initialize WASM and auto-load
|
||||||
(async function() {
|
(async function() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user