Fix password reset landing on the feed; show who is going on feed cards
This commit is contained in:
parent
42249b6046
commit
e34648b5af
4 changed files with 1267 additions and 1 deletions
1034
_expo/static/js/web/entry-9f9c184d0d0f667c1a13ee2c5fb624d0.js
Normal file
1034
_expo/static/js/web/entry-9f9c184d0d0f667c1a13ee2c5fb624d0.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -32,6 +32,6 @@
|
||||||
</noscript>
|
</noscript>
|
||||||
<!-- The root element for your Expo app. -->
|
<!-- The root element for your Expo app. -->
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script src="/_expo/static/js/web/entry-bdf450540d0a1dff60e59848513ca861.js" defer></script>
|
<script src="/_expo/static/js/web/entry-9f9c184d0d0f667c1a13ee2c5fb624d0.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
164
sw.js
Normal file
164
sw.js
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
/* Pull Up service worker.
|
||||||
|
*
|
||||||
|
* HOW THIS FILE GETS SHIPPED, because the next person will look for a build
|
||||||
|
* step and there isn't one:
|
||||||
|
*
|
||||||
|
* `app/public/` is Expo's public folder. `npx expo export --platform web`
|
||||||
|
* copies it verbatim into `dist/` (@expo/cli export/exportApp.js calls
|
||||||
|
* copyPublicFolderAsync before it writes anything else), and `npx expo start
|
||||||
|
* --web` serves it from the dev server through ServeStaticMiddleware. So this
|
||||||
|
* file lands at `dist/sw.js` and is served at `/sw.js` on both.
|
||||||
|
*
|
||||||
|
* That path matters. A service worker's default scope is the directory it was
|
||||||
|
* served from, so a worker at `/sw.js` controls `/` and a worker tucked under
|
||||||
|
* `/_expo/` or `/assets/` would control only that subtree and would never see
|
||||||
|
* a push aimed at the app. Nothing here needs `Service-Worker-Allowed`
|
||||||
|
* precisely because it sits at the root already.
|
||||||
|
*
|
||||||
|
* It is NOT bundled by Metro. It never imports from `app/lib/*` and it cannot:
|
||||||
|
* a service worker is a separate JS realm with no DOM, no React, and no
|
||||||
|
* module graph in common with the app. Everything below is plain ES2020 that
|
||||||
|
* runs as-is.
|
||||||
|
*
|
||||||
|
* WHAT IT DOES, AND DELIBERATELY DOES NOT DO
|
||||||
|
*
|
||||||
|
* It does not cache anything. This is not an offline app -- it is a live feed of
|
||||||
|
* who is playing in the next two hours, and a stale cached bundle is a worse
|
||||||
|
* outcome than a spinner. A push receiver is the entire job.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Take over as soon as a new copy is installed. Without these two, a returning
|
||||||
|
* user keeps whatever worker their tab already had until every tab is closed,
|
||||||
|
* which for a site people leave open is effectively never. */
|
||||||
|
self.addEventListener('install', () => self.skipWaiting());
|
||||||
|
self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
|
||||||
|
|
||||||
|
/* ── where a notification goes when you tap it ───────────────────────────────
|
||||||
|
*
|
||||||
|
* The app's own resolver is `linkFor` in app/lib/notifications.ts and that is
|
||||||
|
* the source of truth. This worker cannot import it (see above), so the click
|
||||||
|
* handler below prefers to hand the notification back to an open tab and let
|
||||||
|
* the real `linkFor` decide.
|
||||||
|
*
|
||||||
|
* `data.url` is the cold-start fallback, computed server-side by
|
||||||
|
* ops/reminder-mailer.js. Two copies of one rule is a real cost, and the
|
||||||
|
* alternative -- opening `/` and making somebody find the thing themselves --
|
||||||
|
* is a worse one. ops/reminder-mailer.js has a self-check that asserts its copy
|
||||||
|
* still agrees with linkFor's cases; run `node ops/reminder-mailer.js --selftest`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
event.waitUntil(handlePush(event));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handlePush(event) {
|
||||||
|
let payload = {};
|
||||||
|
try {
|
||||||
|
payload = event.data ? event.data.json() : {};
|
||||||
|
} catch {
|
||||||
|
// A push with no payload, or one we cannot read, still means "something
|
||||||
|
// happened". Saying so vaguely beats saying nothing: the browser will show
|
||||||
|
// its own "This site has been updated in the background" otherwise, which
|
||||||
|
// is worse than anything we could write.
|
||||||
|
payload = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = payload.title || 'Pull Up';
|
||||||
|
const body = payload.body || '';
|
||||||
|
|
||||||
|
await self.registration.showNotification(title, {
|
||||||
|
body,
|
||||||
|
// The maskable icon already in the export. No emoji anywhere in this app.
|
||||||
|
icon: '/assets/icon.png',
|
||||||
|
badge: '/assets/icon.png',
|
||||||
|
/* Collapse by conversation. Five chat messages from one game should be one
|
||||||
|
* line on the lock screen, not five -- `renotify` still buzzes for each so
|
||||||
|
* nothing is silently swallowed. A notification with no session falls back
|
||||||
|
* to its own id so unrelated things never overwrite each other. */
|
||||||
|
tag: payload.tag || payload.id || 'pullup',
|
||||||
|
renotify: true,
|
||||||
|
/* False, so a notification that arrives while you are staring at the tab
|
||||||
|
* still appears. The click handler is what avoids the silly case of
|
||||||
|
* re-navigating a tab that is already on the right screen. */
|
||||||
|
silent: false,
|
||||||
|
timestamp: payload.at ? Date.parse(payload.at) || Date.now() : Date.now(),
|
||||||
|
data: {
|
||||||
|
id: payload.id || null,
|
||||||
|
kind: payload.kind || null,
|
||||||
|
payload: payload.data || null,
|
||||||
|
url: payload.url || '/',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
event.waitUntil(openFor(event.notification.data || {}));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openFor(data) {
|
||||||
|
const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
||||||
|
|
||||||
|
/* An open tab is the good case: it has the router, the session and the real
|
||||||
|
* `linkFor`, so it can navigate without a reload. Focus it and tell it what
|
||||||
|
* was tapped; app/lib/webPush.ts is listening. */
|
||||||
|
for (const client of all) {
|
||||||
|
if (client.url && new URL(client.url).origin === self.location.origin) {
|
||||||
|
client.postMessage({
|
||||||
|
type: 'pullup-notification-click',
|
||||||
|
id: data.id,
|
||||||
|
kind: data.kind,
|
||||||
|
data: data.payload,
|
||||||
|
url: data.url,
|
||||||
|
});
|
||||||
|
if ('focus' in client) {
|
||||||
|
try {
|
||||||
|
await client.focus();
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Some browsers refuse focus() outside a user gesture they recognise.
|
||||||
|
// Fall through and open a window instead of failing silently.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = typeof data.url === 'string' && data.url.startsWith('/') ? data.url : '/';
|
||||||
|
if (self.clients.openWindow) await self.clients.openWindow(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The browser rotates a subscription occasionally, and when it does the old
|
||||||
|
* endpoint stops working. There is no session in here to re-save the new one
|
||||||
|
* with, so this worker does the half it can -- get a fresh subscription using
|
||||||
|
* the same application server key -- and tells any open tab, which has the
|
||||||
|
* session and can call save_push_subscription.
|
||||||
|
*
|
||||||
|
* If no tab is open the new subscription is simply lost, and that is fine:
|
||||||
|
* app/lib/webPush.ts re-reads and re-saves the live subscription on every app
|
||||||
|
* start, so the next visit repairs it. Support for this event is patchy enough
|
||||||
|
* that it could not be relied on either way.
|
||||||
|
*/
|
||||||
|
self.addEventListener('pushsubscriptionchange', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const old = event.oldSubscription || (await self.registration.pushManager.getSubscription());
|
||||||
|
const key = old && old.options ? old.options.applicationServerKey : null;
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
let fresh;
|
||||||
|
try {
|
||||||
|
fresh = await self.registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: key,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
||||||
|
for (const client of all) {
|
||||||
|
client.postMessage({ type: 'pullup-subscription-changed', subscription: fresh.toJSON() });
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue