Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 1x 1x 1x 1x 4x 4x 4x 4x 4x 1x 1x | 'use client' /** * The app as an app: the service worker (offline pages, notifications), * whether it's been added to the home screen, the browser's own "Install" * offer, and turning notifications on and off for this device. */ /** Running from the home screen (or as an installed desktop app). */ export function isInstalled() { if (typeof window === 'undefined') return false return ( window.matchMedia?.('(display-mode: standalone)').matches || window.matchMedia?.('(display-mode: fullscreen)').matches || (navigator as Navigator & {standalone?: boolean}).standalone === true ) } export type Platform = 'ios' | 'android' | 'desktop' export function platform(): Platform { if (typeof navigator === 'undefined') return 'desktop' const ua = navigator.userAgent // iPadOS says it's a Mac; it's the one with a touchscreen if ( /iPad|iPhone|iPod/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1) ) return 'ios' if (/Android/.test(ua)) return 'android' return 'desktop' } /** "iPhone", "iPad", "Android", "Mac"…: so Account can name each device. */ export function deviceName() { const ua = navigator.userAgent if (/iPhone/.test(ua)) return 'iPhone' if (/iPad/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1)) return 'iPad' if (/Android/.test(ua)) return /Mobile/.test(ua) ? 'Android phone' : 'Android tablet' if (/Macintosh/.test(ua)) return 'Mac' if (/Windows/.test(ua)) return 'Windows' if (/Linux/.test(ua)) return 'Linux' return 'This device' } /** This browser can show notifications at all (iPhone: only as an app). */ export function pushSupported() { return ( typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window ) } export function registerServiceWorker() { if (typeof window === 'undefined' || !('serviceWorker' in navigator)) return // The public demo is static files; nothing to keep offline if (process.env.NEXT_PUBLIC_DEMO === '1') return const dev = process.env.NODE_ENV !== 'production' // The dev server only when asked: localStorage ms:sw-dev = "1" (pages // only; its code changes on every save) or "full" (as in production, to // try offline performance mode) let devMode: string | null = null try { devMode = localStorage.getItem('ms:sw-dev') } catch {} if (dev && !devMode) return const url = dev && devMode !== 'full' ? '/sw.js?dev=1' : '/sw.js' navigator.serviceWorker.register(url).catch(() => {}) } /** Forget every saved page and photo (signing out, switching band). */ export async function clearSaved() { try { await Promise.all([caches.delete('pages'), caches.delete('media')]) } catch {} } /** * Save pages ahead of time, so they open without signal. Resolves once the * service worker has them: how many of them it kept (null: no answer). */ export function saveForOffline( urls: string[], ): Promise<{saved: number; total: number} | null> { return new Promise((resolve) => { const sw = navigator.serviceWorker if (!sw) return resolve(null) const timer = setTimeout(() => resolve(null), 120_000) sw.ready .then((reg) => { const channel = new MessageChannel() channel.port1.onmessage = (e) => { clearTimeout(timer) resolve(e.data) } reg.active?.postMessage({type: 'save', urls}, [channel.port2]) }) .catch(() => resolve(null)) }) } // --------------------------------------------------------------------------- // The browser's "Install" offer (Chrome, Edge, Android). iPhone and iPad // have none: Share → Add to Home Screen. // --------------------------------------------------------------------------- type InstallEvent = Event & { prompt: () => Promise<void> userChoice: Promise<{outcome: string}> } let offer: InstallEvent | null = null const listeners = new Set<() => void>() export function watchInstallOffer() { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault() offer = e as InstallEvent listeners.forEach((l) => l()) }) window.addEventListener('appinstalled', () => { offer = null listeners.forEach((l) => l()) }) } export function canOfferInstall() { return offer !== null } export function onInstallOffer(l: () => void) { listeners.add(l) return () => { listeners.delete(l) } } export async function install() { if (!offer) return false await offer.prompt() const {outcome} = await offer.userChoice offer = null listeners.forEach((l) => l()) return outcome === 'accepted' } // --------------------------------------------------------------------------- // Notifications on this device // --------------------------------------------------------------------------- function fromBase64Url(s: string) { const b64 = (s + '='.repeat((4 - (s.length % 4)) % 4)) .replace(/-/g, '+') .replace(/_/g, '/') return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)) } /** This device's subscription, if notifications are on here. */ export async function currentSubscription() { if (!pushSupported()) return null const reg = await navigator.serviceWorker.getRegistration() return (await reg?.pushManager.getSubscription()) ?? null } /** Ask (the browser shows its own question), then tell the server. */ export async function turnOn(): Promise<'on' | 'denied' | 'unavailable'> { if (!pushSupported()) return 'unavailable' const {key} = await (await fetch('/api/push/key')).json() if (!key) return 'unavailable' const permission = await Notification.requestPermission() if (permission !== 'granted') return 'denied' const reg = await navigator.serviceWorker.ready const sub = (await reg.pushManager.getSubscription()) ?? (await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: fromBase64Url(key), })) const r = await fetch('/api/push/subscribe', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({...sub.toJSON(), device: deviceName()}), }) return r.ok ? 'on' : 'unavailable' } export async function turnOff() { const sub = await currentSubscription() if (!sub) return await fetch('/api/push/subscribe', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({endpoint: sub.endpoint}), }).catch(() => {}) await sub.unsubscribe().catch(() => {}) } |