aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-29 09:03:13 -0700
committerAdam Soltys <[email protected]>2026-09-29 09:03:13 -0700
commitd2b3c05995b0d54cfe586128fd2e958a563ac861 (patch)
tree96efd61a7a790d398c33c3f378bc069ac235a4c6 /src/features
parent4164d22809e49f1a5c55571f4f37eed8c2a70ba7 (diff)
downloadcoinosv3-d2b3c05995b0d54cfe586128fd2e958a563ac861.tar.gz
coinosv3-d2b3c05995b0d54cfe586128fd2e958a563ac861.zip
Someone with no profile picture paints as their punk from the first frame after a reload, not a blank circle that turns into one.
Faceless answers were never persisted (they'd evict real faces from the 150-row profile blob), so every reload re-asked and showed the loading circle until the relays said "nothing". A small pubkey->time list now remembers them: no kind 0 on two asks running (a cold relay isn't enough), or a kind 0 naming nothing; finding a face takes them off it. room-faces-test --refresh: reload inside the coinos room, faces keyed by author — 2 changed (wait→punk) before, 0 after. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Du1qT1wbkYwYK6zkGH79Rp
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js36
1 files changed, 32 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 38bc899..a86d0a9 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -555,6 +555,29 @@ export function messagesFeature(ctx) {
profilesWarmed = true;
const cached = profilesDisk();
for (const [pk, p] of Object.entries(cached)) if (!profiles.has(pk)) profiles.set(pk, p);
+ // someone known to have no face paints their punk from the first frame,
+ // instead of a blank circle that turns into one when the relays answer
+ for (const [pk, t] of Object.entries(facelessDisk())) {
+ if (!profiles.has(pk)) profiles.set(pk, { name: null, picture: null, t, miss: 1 });
+ }
+ }
+ // 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
+ // faces) as a small pubkey -> time map; one found with a face leaves it.
+ const FACELESS_MAX = 1000;
+ let facelessBlob = null, facelessFlush = null;
+ const facelessDisk = () => (facelessBlob ||= wallet.loadFeatureState('profilesFaceless', {}) || {});
+ function markFaceless(pk, on) {
+ const s = facelessDisk();
+ if (on ? s[pk] : !s[pk]) return;
+ if (on) s[pk] = Date.now(); else delete s[pk];
+ if (facelessFlush) return;
+ facelessFlush = setTimeout(() => {
+ facelessFlush = null;
+ const keys = Object.keys(s);
+ if (keys.length > FACELESS_MAX) for (const k of keys.sort((a, b) => s[a] - s[b]).slice(0, keys.length - FACELESS_MAX)) delete s[k];
+ wallet.saveFeatureState('profilesFaceless', s);
+ }, 500);
}
// The persisted profile blob (names, faces, thumbnails) lives parsed in
// memory and is written back once per burst: a batch of thirty profiles
@@ -592,9 +615,10 @@ export function messagesFeature(ctx) {
wallet.saveFeatureState('profiles', s);
}
function persistProfile(pk, p) {
- // A faceless answer is not a fact worth writing down — persisting
- // {name:null, picture:null} rows only evicts real faces from the cap and
- // spreads a cold-relay miss across sessions.
+ // A faceless answer is not a fact worth writing down here — persisting
+ // {name:null, picture:null} rows only evicts real faces from the cap.
+ // (It goes on the faceless list instead.)
+ if (p && p.eventAt) markFaceless(pk, !p.name && !p.picture);
if (!p || (!p.name && !p.picture)) return;
const s = profilesDisk();
s[pk] = { name: p.name || null, picture: p.picture || null, nip05: p.nip05 || null, lud16: p.lud16 || null,
@@ -927,7 +951,11 @@ export function messagesFeature(ctx) {
for (const pk of pks) {
if (found.has(pk)) continue;
const prev = profiles.get(pk);
- profiles.set(pk, { ...(prev || {}), t: Date.now(), miss: ((prev && prev.miss) || 0) + 1 });
+ const miss = ((prev && prev.miss) || 0) + 1;
+ profiles.set(pk, { ...(prev || {}), t: Date.now(), miss });
+ // twice running: not a cold relay, they have none (never someone
+ // whose face we do know)
+ if (miss >= 2 && !(prev && (prev.name || prev.picture))) markFaceless(pk, true);
}
scheduleRepaint();
} finally {