Pull-to-refresh on every page
Pull-to-refresh is one of those interactions you do not notice when it works and notice immediately when it does not. We added it to every page on touch devices.
- Touch-only: the script gates on
(pointer: coarse)so desktop pointers and Apple Pencil are unaffected. - Threshold 80px: pull down 80px from the top of the page and the indicator flips from "Pull to refresh" to "Release to refresh." Animation eases at 60% of pull distance.
- Cancellable: drag back up before release and nothing happens.
- Context-aware: if your touch starts inside a scrolled inner container (e.g., a sticky messages sidebar that is mid-scroll), the gesture is ignored. The whole-page refresh only fires when the document itself is at scrollTop=0.
- Opt-out per page: put
data-no-ptron the body or any ancestor element to disable the gesture on a specific surface (we did this on the buy funnel so a stray over-pull does not nuke your checkout state).
Implementation is one ~150-line JS file with no dependencies. Built the SVG arrow and label with DOM APIs (no innerHTML) so it stays CSP-nonce-clean.
File downloads work on mobile
Until today, tapping a download link on a phone redirected you to a "this surface is desktop-only" explainer page. That was the right call in 2024 because the mobile file viewer was rough. The file viewer is fine now, so the gate is gone.
Two route registrations had their DesktopOnlyMiddleware dropped:
GET /files/free-download/{id}POST /files/download/{id}
The middleware's path inference was also narrowed. The downloads surface in DesktopOnlyMiddleware::inferSurface now only covers /files/launch-emulator. The reason that one stays gated is straightforward: the in-browser retro emulator needs keyboard input that mobile cannot reliably provide. If you have ever tried to play Atari with an on-screen keyboard, you know.
A real PWA with offline support
The manifest existed in name only (the layout linked to /manifest.json but the file was a 404). The service worker existed but was branded for the predecessor product. Both got fixed.
manifest.json
A new manifest.json with Mobieus branding: name and short_name "Mobieus," display: standalone, start_url: /, the full icon set including 192/512 maskable variants, and three app shortcuts (Feed, Messages, Notifications) so a long-press on the home-screen icon jumps straight in. Android Chrome will surface an install prompt; iOS members can Share → Add to Home Screen.
sw.js
Service worker rewritten with two clear caching lanes:
- HTML navigation: network-first. Always tries the network first; cached copy is updated whenever the page renders. If the network is down, falls back to the last cached copy of that page, and ultimately to a branded
/offline.htmlpage. - Static assets (
/css/, /js/, /assets/, /fonts/, /img/, /icons/, manifest, favicons): cache-first with network fallback.
The worker explicitly never intercepts /api/, /rss/, /sse/, /admin/, /sw.js — those always hit the network so the public REST API, RSS feeds, server-sent-events streams, and the admin UI never serve stale content.
Push notifications (which were already wired in our scheduled web-push worker) keep working with the upgrade. The default notification title is now "Mobieus."
offline.html
The fallback page is a small standalone HTML document that adapts to light and dark color schemes via prefers-color-scheme. It listens for the browser online event and auto-reloads when connectivity returns, so a member who lost signal on the subway gets back to where they were as soon as they pop above ground.
What changes for you as an operator
Nothing you have to configure. The PWA pieces ship with every tenant on the next deploy. The pull-to-refresh script loads from the base layout, so every page in your community gets the gesture for free. The download gate change is already live on dev, fort-smith-live, and support tenants.
What we did not do (and why)
- Not a native iOS or Android app. A real native app is months of work, two app-store approval processes, and ongoing maintenance burden for ~80% of the value a polished PWA already delivers. We will look at native once the install funnel on PWA tops out, not before.
- No background sync for posts. Posting a thread or reply still requires network. We could queue and replay on reconnect; right now we judged that the failure mode (someone thinking they posted when they did not) is worse than the inconvenience of "send when back online."
- No precaching of community content. The static asset cache is bounded; HTML pages are cached only after first visit. We do not want to spend the member's storage budget on pages they may never re-visit.
Where to look next
- If your community runs heavy on files (BBS, retrocomputing, software libraries): the download path on mobile is the main beneficiary. Test from your phone.
- If you want the install prompt to surface for your members, send them the URL on a Chrome-based mobile browser. iOS members install via Share → Add to Home Screen.
- If you have a page that needs to opt out of pull-to-refresh (long forms, modals, custom drag interactions): drop
data-no-ptron the<body>or any wrapper.

