"Since you last visited" cue + PWA install (add to home screen)

Two calm returning-reader features.

Since-last-visit (Highlights companion, not a nav lane — per Codex):
* queries.feed gains a `since` filter; GET /api/since?ts= returns the count +
  a few accepted/non-dup/visible articles discovered since the reader's last
  visit (boundary-respecting; invalid/future ts → 0, no error).
* Home stores last_seen in localStorage (reads prev, then stamps now); on
  Highlights, a gentle "Since you were last here, N new calm reads came in"
  note with a "See what's new" reveal of a compact inline section. Dismissible.
  No badges, no unread counts, no "missed" language.

PWA:
* Real PNG icons (192/512 + full-bleed maskable) rasterized from favicon.svg;
  manifest fixed (azure theme to match the brand, PNG icons); apple-touch-icon.
* Minimal service worker: precache the app shell, always-fresh API + /a/ pages.
* Gentle, dismissible install banner (beforeinstallprompt → Install; iOS → the
  Share → Add to Home Screen hint). Never nags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-09 20:38:12 -04:00
parent 008364e922
commit d0fb153e46
11 changed files with 217 additions and 3 deletions
+42
View File
@@ -0,0 +1,42 @@
// Calm PWA install state. Captures the browser's install prompt (Android/desktop
// Chrome), detects iOS (which has no prompt — needs the Share → Add to Home Screen
// hint), and remembers a dismissal so we never nag.
import { browser } from '$app/environment';
export const pwa = $state({ canInstall: false, isIOS: false, isStandalone: false, dismissed: false });
let deferred = null;
if (browser) {
try {
pwa.isStandalone =
window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
pwa.isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent);
pwa.dismissed = localStorage.getItem('goodnews:pwa_dismissed') === '1';
} catch {
/* private mode / unavailable */
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferred = e;
pwa.canInstall = true;
});
window.addEventListener('appinstalled', () => {
pwa.canInstall = false;
pwa.isStandalone = true;
});
}
export async function installApp() {
if (!deferred) return;
deferred.prompt();
try { await deferred.userChoice; } catch { /* dismissed */ }
deferred = null;
pwa.canInstall = false;
}
export function dismissPwa() {
pwa.dismissed = true;
if (browser) {
try { localStorage.setItem('goodnews:pwa_dismissed', '1'); } catch { /* ignore */ }
}
}