fix: consolidate PWA banners — install in browser, update in PWA only
Remove duplicate purple update banner from website/shell.ts that overlapped with the server/shell.ts banner. Now a single banner system: - Browser: "Install rSpace app" prompt (via beforeinstallprompt) - Installed PWA: "New version available" prompt (via SW update detection) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
26c07a7672
commit
bb19b4bc89
|
|
@ -350,10 +350,15 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import '/shell.js';
|
import '/shell.js';
|
||||||
// ── Service worker registration + update detection ──
|
// ── PWA: single consolidated banner ──
|
||||||
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|
||||||
|
|| window.navigator.standalone === true;
|
||||||
|
|
||||||
|
// Service worker registration + update detection
|
||||||
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
||||||
navigator.serviceWorker.register("/sw.js?v=5").then((reg) => {
|
navigator.serviceWorker.register("/sw.js?v=5").then((reg) => {
|
||||||
function showUpdateBanner() {
|
function showUpdateBanner() {
|
||||||
|
if (!isStandalone) return; // Only show update prompt in installed PWA
|
||||||
const b = document.getElementById('pwa-update-banner');
|
const b = document.getElementById('pwa-update-banner');
|
||||||
if (!b || b.style.display !== 'none') return;
|
if (!b || b.style.display !== 'none') return;
|
||||||
b.style.display = '';
|
b.style.display = '';
|
||||||
|
|
@ -375,31 +380,36 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// ── PWA install banner ──
|
|
||||||
window.addEventListener("beforeinstallprompt", (e) => {
|
// Install banner — browser only (not shown in installed PWA)
|
||||||
e.preventDefault();
|
if (!isStandalone) {
|
||||||
window.__rspaceInstallPrompt = () => { e.prompt(); return e.userChoice; };
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
window.dispatchEvent(new CustomEvent("rspace-install-available"));
|
e.preventDefault();
|
||||||
if (localStorage.getItem('rspace_install_dismissed') !== '1') {
|
window.__rspaceInstallPrompt = () => { e.prompt(); return e.userChoice; };
|
||||||
const b = document.getElementById('pwa-install-banner');
|
window.dispatchEvent(new CustomEvent("rspace-install-available"));
|
||||||
if (b) { b.style.display = ''; document.body.classList.add('rspace-banner-visible'); }
|
if (localStorage.getItem('rspace_install_dismissed') !== '1') {
|
||||||
}
|
const b = document.getElementById('pwa-install-banner');
|
||||||
});
|
if (b) { b.style.display = ''; document.body.classList.add('rspace-banner-visible'); }
|
||||||
document.getElementById('pwa-install-btn')?.addEventListener('click', async () => {
|
|
||||||
if (window.__rspaceInstallPrompt) {
|
|
||||||
const choice = await window.__rspaceInstallPrompt();
|
|
||||||
if (choice?.outcome === 'accepted') {
|
|
||||||
document.getElementById('pwa-install-banner').style.display = 'none';
|
|
||||||
document.body.classList.remove('rspace-banner-visible');
|
|
||||||
localStorage.setItem('rspace_install_dismissed', '1');
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
document.getElementById('pwa-install-btn')?.addEventListener('click', async () => {
|
||||||
document.getElementById('pwa-install-close')?.addEventListener('click', () => {
|
if (window.__rspaceInstallPrompt) {
|
||||||
document.getElementById('pwa-install-banner').style.display = 'none';
|
const choice = await window.__rspaceInstallPrompt();
|
||||||
document.body.classList.remove('rspace-banner-visible');
|
if (choice?.outcome === 'accepted') {
|
||||||
localStorage.setItem('rspace_install_dismissed', '1');
|
document.getElementById('pwa-install-banner').style.display = 'none';
|
||||||
});
|
document.body.classList.remove('rspace-banner-visible');
|
||||||
|
localStorage.setItem('rspace_install_dismissed', '1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('pwa-install-close')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('pwa-install-banner').style.display = 'none';
|
||||||
|
document.body.classList.remove('rspace-banner-visible');
|
||||||
|
localStorage.setItem('rspace_install_dismissed', '1');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update banner button handlers (always wire, shown conditionally above)
|
||||||
document.getElementById('pwa-update-btn')?.addEventListener('click', () => {
|
document.getElementById('pwa-update-btn')?.addEventListener('click', () => {
|
||||||
navigator.serviceWorker?.getRegistration().then((reg) => { reg?.waiting?.postMessage({ type: 'SKIP_WAITING' }); });
|
navigator.serviceWorker?.getRegistration().then((reg) => { reg?.waiting?.postMessage({ type: 'SKIP_WAITING' }); });
|
||||||
});
|
});
|
||||||
|
|
@ -407,10 +417,6 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
document.getElementById('pwa-update-banner').style.display = 'none';
|
document.getElementById('pwa-update-banner').style.display = 'none';
|
||||||
document.body.classList.remove('rspace-banner-visible');
|
document.body.classList.remove('rspace-banner-visible');
|
||||||
});
|
});
|
||||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
|
||||||
const ib = document.getElementById('pwa-install-banner');
|
|
||||||
if (ib) ib.style.display = 'none';
|
|
||||||
}
|
|
||||||
// ── Header minimize toggle ──
|
// ── Header minimize toggle ──
|
||||||
if (localStorage.getItem('rspace_headers_minimized') === '1') {
|
if (localStorage.getItem('rspace_headers_minimized') === '1') {
|
||||||
document.body.classList.add('rspace-headers-minimized');
|
document.body.classList.add('rspace-headers-minimized');
|
||||||
|
|
|
||||||
|
|
@ -133,88 +133,5 @@ document.addEventListener("auth-change", (e) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── SW Update Banner ──
|
// SW update banner is handled by the inline script in server/shell.ts
|
||||||
// Show "new version available" when a new service worker activates.
|
// (single #pwa-update-banner element, no duplicate)
|
||||||
// The SW calls skipWaiting() so it activates immediately — we detect the
|
|
||||||
// controller change and prompt the user to reload for the fresh content.
|
|
||||||
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
|
||||||
// Only listen if there's already a controller (skip first-time install)
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
||||||
showUpdateBanner();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also detect waiting workers (edge case: skipWaiting didn't fire yet)
|
|
||||||
navigator.serviceWorker.getRegistration().then((reg) => {
|
|
||||||
if (!reg) return;
|
|
||||||
if (reg.waiting && navigator.serviceWorker.controller) {
|
|
||||||
showUpdateBanner();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reg.addEventListener("updatefound", () => {
|
|
||||||
const newWorker = reg.installing;
|
|
||||||
if (!newWorker) return;
|
|
||||||
newWorker.addEventListener("statechange", () => {
|
|
||||||
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
|
|
||||||
showUpdateBanner();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showUpdateBanner() {
|
|
||||||
if (document.getElementById("sw-update-banner")) return;
|
|
||||||
|
|
||||||
const banner = document.createElement("div");
|
|
||||||
banner.id = "sw-update-banner";
|
|
||||||
banner.setAttribute("role", "alert");
|
|
||||||
banner.innerHTML = `
|
|
||||||
<span>New version available</span>
|
|
||||||
<button id="sw-update-btn">Tap to update</button>
|
|
||||||
<button id="sw-update-dismiss" aria-label="Dismiss">×</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const style = document.createElement("style");
|
|
||||||
style.textContent = `
|
|
||||||
#sw-update-banner {
|
|
||||||
position: fixed; top: 0; left: 0; right: 0; z-index: 10000;
|
|
||||||
display: flex; align-items: center; justify-content: center; gap: 12px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
||||||
color: white; font-size: 14px; font-weight: 500;
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,0.3);
|
|
||||||
animation: sw-slide-down 0.3s ease-out;
|
|
||||||
}
|
|
||||||
@keyframes sw-slide-down {
|
|
||||||
from { transform: translateY(-100%); }
|
|
||||||
to { transform: translateY(0); }
|
|
||||||
}
|
|
||||||
#sw-update-btn {
|
|
||||||
padding: 5px 14px; border-radius: 6px; border: 1.5px solid rgba(255,255,255,0.5);
|
|
||||||
background: rgba(255,255,255,0.15); color: white;
|
|
||||||
font-size: 13px; font-weight: 600; cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
#sw-update-btn:hover { background: rgba(255,255,255,0.3); }
|
|
||||||
#sw-update-dismiss {
|
|
||||||
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
|
|
||||||
background: none; border: none; color: rgba(255,255,255,0.7);
|
|
||||||
font-size: 20px; cursor: pointer; padding: 4px 8px; line-height: 1;
|
|
||||||
}
|
|
||||||
#sw-update-dismiss:hover { color: white; }
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.head.appendChild(style);
|
|
||||||
document.body.prepend(banner);
|
|
||||||
|
|
||||||
banner.querySelector("#sw-update-btn")!.addEventListener("click", () => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
banner.querySelector("#sw-update-dismiss")!.addEventListener("click", () => {
|
|
||||||
banner.remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue