diff options
| author | Adam Soltys <[email protected]> | 2026-10-07 07:03:00 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-07 07:03:00 -0700 |
| commit | 40c4a18fff2460ccfb1dcf9a85fc88cdab69b841 (patch) | |
| tree | f2aca32304dba36487da436a4743e0fecf82f814 /src | |
| parent | 64c0afcb99846528029e06309369aee8b28adff6 (diff) | |
| download | coinosv3-40c4a18fff2460ccfb1dcf9a85fc88cdab69b841.tar.gz coinosv3-40c4a18fff2460ccfb1dcf9a85fc88cdab69b841.zip | |
Notifications: the face opens the sender's profile; faces of people you know stay cached; primal-hosted faces get thumbnails again
- A notification's avatar now opens the actor's profile (the name and the
rest of the row still open the note).
- The profile cache held 150 rows evicted by fetch time, and a phone's feed
churned all of them within twenty minutes, so a reload painted a punk and
an npub for people you hear from daily. 300 rows now, and follows, DM
peers and notification actors are evicted last (their thumbnails too).
- corsFetch's "is the host reachable?" probe was answered from the HTTP
cache, so any network blip blamed the host: a phone had ~100 hosts
written off as no-CORS, primal, nostr.build and coinos.io among them, and
those faces painted from multi-MB originals on every repaint. The probe
now goes past the cache and the polluted list is reset (v5).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 46 |
1 files changed, 28 insertions, 18 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 3eb9258..6f34d48 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -639,7 +639,7 @@ export function messagesFeature(ctx) { } } // People with no face: no kind 0 on two asks running, or one naming - // nothing. Kept apart from the profile blob (whose 150 rows are for real + // nothing. Kept apart from the profile blob (whose rows are for real // faces) as a small pubkey -> time map; one found with a face leaves it. const FACELESS_MAX = 1000; let facelessBlob = null, facelessFlush = null; @@ -661,34 +661,39 @@ export function messagesFeature(ctx) { // landing during a scroll used to parse and re-serialize the whole blob // thirty times over, on the main thread, mid-scroll. let profBlob = null, profFlush = null; + const PROFILE_ROWS = 300; // a row without a thumbnail is ~430 chars const profilesDisk = () => (profBlob ||= wallet.loadFeatureState('profiles', {})); function flushProfiles() { profFlush = null; const s = profBlob; if (!s) return; + // Who stays when space runs out: your own faces, the people you follow, + // talk to and hear from in Notifications. A feed scroll through a + // hundred strangers used to evict them all within minutes, and a reload + // then painted a punk and an npub while their kind 0 was asked for again. + const mine = new Set(myPubkeys()); + const keep = new Set([...mine, (hook('nostrLoginIdentity') || {}).pubkey, + wallet.nostrPubkey && wallet.nostrPubkey()].filter(Boolean)); + const known = new Set(keep); + try { for (const pk of followsNow().set) known.add(pk); } catch {} + try { for (const pk of threads.keys()) known.add(pk); } catch {} + try { for (const x of (notif ? notif.items : st().notifs) || []) if (x && x.actor) known.add(x.actor); } catch {} + const evictFirst = (keys) => keys.sort((a, b) => (known.has(a) - known.has(b)) || ((s[a].t || 0) - (s[b].t || 0))); // Thumbnails are the bulk of this blob, so they live on a budget: the - // least recently seen faces give theirs up first. The row itself stays — - // that face just paints the way it used to. + // least recently seen strangers give theirs up first. The row itself + // stays — that face just paints the way it used to. let size = JSON.stringify(s).length; if (size > THUMB_BUDGET) { // never our own: the header face is the one that must paint offline - const mine = new Set(myPubkeys()); - const oldestFirst = Object.keys(s).filter((k) => s[k].thumb && !mine.has(k)).sort((a, b) => (s[a].t || 0) - (s[b].t || 0)); - for (const k of oldestFirst) { + for (const k of evictFirst(Object.keys(s).filter((k) => s[k].thumb && !mine.has(k)))) { size -= (s[k].thumb || '').length; delete s[k].thumb; delete s[k].thumbFor; if (size <= THUMB_BUDGET) break; } } const keys = Object.keys(s); - if (keys.length > 150) { - // the user's own faces never churn out — their avatar going punk over - // a busy community room is the one eviction people actually notice - const keep = new Set([(hook('nostrLoginIdentity') || {}).pubkey, - wallet.nostrPubkey && wallet.nostrPubkey()].filter(Boolean)); - const evictable = keys.filter((k) => !keep.has(k)); - for (const k of evictable.sort((a, b) => (s[a].t || 0) - (s[b].t || 0)).slice(0, keys.length - 150)) delete s[k]; - } + if (keys.length > PROFILE_ROWS) + for (const k of evictFirst(keys.filter((k) => !keep.has(k))).slice(0, keys.length - PROFILE_ROWS)) delete s[k]; wallet.saveFeatureState('profiles', s); } function persistProfile(pk, p) { @@ -796,7 +801,9 @@ export function messagesFeature(ctx) { // is left alone for a week rather than asked about every new face. // v2: the first list was mostly wrong (primal's CDN, coinos.io) — see corsFetch // v3: v2 also caught dropped connections (primal again, on a phone) - const NOCORS_KEY = 'coinos-thumb-nocors-v4'; + // v5: v4's reachability probe was answered from the HTTP cache + const NOCORS_KEY = 'coinos-thumb-nocors-v5'; + try { for (const v of ['', '-v2', '-v3', '-v4']) localStorage.removeItem('coinos-thumb-nocors' + v); } catch {} const NOCORS_MS = 7 * 24 * 3600_000; let noCors = null; // host -> { n, until } const noCorsNow = () => { if (!noCors) { try { noCors = JSON.parse(localStorage.getItem(NOCORS_KEY) || '{}'); } catch { noCors = {}; } } return noCors; }; @@ -831,8 +838,11 @@ export function messagesFeature(ctx) { return await fetch(url, { mode: 'cors', cache: 'reload', signal }); } catch (e2) { if (!e2 || e2.name !== 'TypeError') throw e2; + // past the cache: the face was just painted from this URL, so a + // cached copy would answer even with the network gone — and a dropped + // connection got primal, nostr.build and coinos.io itself blamed let reachable = false; - try { await fetch(url, { mode: 'no-cors', signal }); reachable = true; } catch {} + try { await fetch(url, { mode: 'no-cors', cache: 'reload', signal }); reachable = true; } catch {} if (reachable) { e2.corsRefused = true; throw e2; } const net = new Error('network'); net.name = 'NetworkError'; throw net; @@ -12629,8 +12639,8 @@ export function messagesFeature(ctx) { style: 'gap:10px;align-items:flex-start;padding:10px 0;cursor:pointer', onClick: (e) => { if (e.target && e.target.closest && e.target.closest('button')) return; open(); }, }, - // the whole row opens the note: face and name aren't profile links here - avatar(x.actor, 'chat-avatar note-avatar', false), + // the face opens the sender's profile; the rest of the row opens the note + avatar(x.actor, 'chat-avatar note-avatar'), h('div', { class: 'col grow', style: 'min-width:0;gap:3px' }, h('div', { class: 'row', style: 'gap:7px;align-items:baseline;min-width:0' }, h('span', { style: 'font-weight:600;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis' }, |
