aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-07 13:10:43 -0700
committerAdam Soltys <[email protected]>2026-10-07 13:10:43 -0700
commitc82e042aa592e4e619d0c280b02486eeb859d358 (patch)
tree6fb03c99cf227789ea7773128f935a317fc40411 /src
parent6faf6b6fe49b8e51a38e0106c858341faf2145ef (diff)
downloadcoinosv3-c82e042aa592e4e619d0c280b02486eeb859d358.tar.gz
coinosv3-c82e042aa592e4e619d0c280b02486eeb859d358.zip
Profile links paint whole on the first frame: the server renders /npub1…, /nprofile1… and /<name> 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
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 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js37
1 files changed, 34 insertions, 3 deletions
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)