aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js46
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' },