From c82e042aa592e4e619d0c280b02486eeb859d358 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 13:10:43 -0700 Subject: Profile links paint whole on the first frame: the server renders /npub1…, /nprofile1… and / with the kind 0, latest posts, follow counts and mentioned faces inlined (stale-while-refresh cache, 2 s budget), the app seeds its caches from it, and the big avatar never flashes punk art while a lookup runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cold visit to a profile link showed a punk, '–' counts and no posts for 9 s. Server-side relay answers settle once events stop arriving (idle 400 ms) instead of waiting out a relay that never sends EOSE: a cold build is ~1.9 s. Co-Authored-By: Claude Opus 5.5 --- src/features/messages.js | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index 3ab64d9..39cc7b1 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2164,6 +2164,29 @@ export function messagesFeature(ctx) { }, 0); } + // (runs after construction: the caches it fills are declared further down) + function seedBootProfile(path) { + try { + const el = typeof document !== 'undefined' && document.getElementById('boot-profile'); + const d = el && JSON.parse(el.textContent); + if (!d || d.v !== 1 || !/^[0-9a-f]{64}$/.test(d.pk || '') || String(d.path || '').toLowerCase() !== path.toLowerCase()) return null; + const pk = d.pk; + for (const ev of [...(d.profiles || []), d.profile]) { + if (ev && ev.kind === 0 && /^[0-9a-f]{64}$/.test(ev.pubkey || '')) applyProfile(ev.pubkey, ev); + } + // the page's own copy (not marked fetched: the relays still freshen it) + if (d.profile && d.profile.pubkey === pk && !fullProfiles.has(pk)) { + try { const m = JSON.parse(d.profile.content); if (m && typeof m === 'object' && !Array.isArray(m)) fullProfiles.set(pk, m); } catch {} + } + const notes = (d.notes || []).filter((e) => e && e.pubkey === pk && PROFILE_KINDS.includes(e.kind)); + if (notes.length && !notesCache.has(pk)) notesCache.set(pk, { status: 'ready', notes, fetchedAt: 0 }); + if (d.counts && !socialCounts.get(pk)?.at) { + const num = (n) => (Number.isFinite(n) && n >= 0 ? n : null); + socialCounts.set(pk, { follows: num(d.counts.follows), followers: num(d.counts.followers), at: Date.now() }); + } + return pk; + } catch { return null; } + } if (urlProfile) { // Claim the first paint synchronously: these are set before the boot // render, so the visitor sees a profile shell from the first frame — @@ -2172,7 +2195,12 @@ export function messagesFeature(ctx) { ui.pubProfPending = urlProfile; (async () => { await new Promise((r) => setTimeout(r, 0)); // never render mid-boot - let pk = parseNostrPubkey(urlProfile); + // The server renders profile links with the page already in them + // (feed/profile-page.js): kind 0, latest posts, follow counts and the + // faces the posts mention. Into the caches the page reads they go, so + // it paints whole on its first frame; the usual fetches still freshen + // it behind. A coinos name arrives already resolved. + let pk = seedBootProfile(urlProfile) || parseNostrPubkey(urlProfile); if (!pk && /^[a-z0-9._-]{1,30}$/i.test(urlProfile)) { // same registrar + domain rule the names feature uses const domain = getNetwork() === 'mutinynet' ? 'staging.coinos.io' : 'coinos.io'; @@ -3571,11 +3599,14 @@ export function messagesFeature(ctx) { // after a miss; the request state lets // it fall back once we've finished looking. const sendFace = cls.includes('send-avatar'); + // A profile page's own face is the same: the one big picture on the + // page, never punk art while that person's lookup is still running. + const pageFace = cls.includes('profile-avatar'); const waiting = (p === null || (p && p.loading && !p.picture)) - && (sendFace ? p === null || profQueue.has(pk) || profInFlight.has(pk) : faceWaiting(pk)); + && (sendFace || pageFace ? p === null || profQueue.has(pk) || profInFlight.has(pk) || fullInFlight.has(pk) : faceWaiting(pk)); const node = feedPaint && !p.picture ? h('div', { class: cls + ' fallback' }, (p.name || npubOf(pk) || '??').slice(0, 2)) - : waiting && (sendFace || p || ui.noteThread || (ui.chatOpen && ui.msgView === 'room')) + : waiting && (sendFace || pageFace || p || ui.noteThread || (ui.chatOpen && ui.msgView === 'room')) ? h('div', { class: cls + ' fallback loading', style: sendFace ? 'visibility:hidden' : undefined }) : p && p.loading && !p.picture ? fallbackAvatar(h, pk, p.name, cls) -- cgit v1.2.3