From af30d3cdc7da7f9e60bc552282f08ea7def3c0dd Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:10:18 -0700 Subject: Profiles: a remembered face beats a row asked for before the cache opened Since the profile cache moved to IndexedDB, a profile asked for during boot (a restored profile page, offline or with relays failing) left a loading or miss row in memory, and the re-warm after the cache opened skipped any pk already present, so the cached name never showed (profile-draft-test: the editor reopened with an empty Display name). Placeholder rows without a name/picture (and without a real kind 0 behind them) now give way to the cached one. Co-Authored-By: Claude Opus 5.5 --- src/features/messages.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index f98df3a..9745717 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -645,7 +645,13 @@ export function messagesFeature(ctx) { if (!wallet.mnemonic && !wallet.xprv && !wallet.xpub) return; profilesWarmed = true; const cached = profilesDisk(); - for (const [pk, p] of Object.entries(cached)) if (!profiles.has(pk)) profiles.set(pk, p); + // (a row asked for before the cache opened — still loading, or a miss + // while offline — gives way to the remembered face; a kind 0 that + // really named nothing keeps its say) + for (const [pk, p] of Object.entries(cached)) { + const cur = profiles.get(pk); + if (!cur || (!cur.name && !cur.picture && !cur.eventAt)) 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())) { -- cgit v1.2.3 From f5450cd46cf752a843205ff51e6d12cf0da4dac8 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:23:42 -0700 Subject: Communities: a room rebuilt with fresher keys folds its channels again When our own community list came back with a channel key the local copy lacked (every community we create: its list entry carries the keys), the room was torn down to be rebuilt with them, but the wraps it had already opened stayed in seenWraps. The rebuilt room's subscription then skipped the relay replay, so its Control fold held only editions newer than the rebuild: a second channel showed up as the only one (no picker; general gone from the fold), and the guestbook lost its joins. Each room now remembers the wrap ids it opened and hands them back when it is rebuilt. chat-layout-test reaches Messages through the bottom nav. Co-Authored-By: Claude Opus 5.5 --- src/features/messages.js | 13 ++++++++++++- tools/chat-layout-test.js | 5 ++--- 2 files changed, 14 insertions(+), 4 deletions(-) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index 9745717..02a19b4 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -1178,6 +1178,10 @@ export function messagesFeature(ctx) { folded: null, controlEntries: [], guestEntries: [], + // the wraps this room opened: a room rebuilt with fresher keys hands + // them back to seenWraps, or its new subscription would skip the + // replay and fold without them + wrapIds: new Set(), members: new Map(), byChannel: new Map(), edits: new Map(), @@ -1253,6 +1257,7 @@ export function messagesFeature(ctx) { subscribeOn(room.relays, { kinds: [1059], authors: [room.control.pk], limit: 500 }, (wrap) => { if (seenWraps.has(wrap.id)) return; seenWraps.add(wrap.id); + room.wrapIds.add(wrap.id); openWrapBg(wrap, room.control, (opened) => { if (!opened || opened.rumor.kind !== 3308) return; room.controlEntries.push(opened); @@ -1262,6 +1267,7 @@ export function messagesFeature(ctx) { subscribeOn(room.relays, { kinds: [1059], authors: [room.guestbook.pk], limit: 500 }, (wrap) => { if (seenWraps.has(wrap.id)) return; seenWraps.add(wrap.id); + room.wrapIds.add(wrap.id); openWrapBg(wrap, room.guestbook, (opened) => { if (!opened) return; room.guestEntries.push(opened); @@ -1288,6 +1294,7 @@ export function messagesFeature(ctx) { const u = subscribeOn(room.relays, { kinds: [1059, 21059], authors: [stream.pk], limit: 200 }, (wrap) => { if (seenWraps.has(wrap.id)) return; seenWraps.add(wrap.id); + room.wrapIds.add(wrap.id); openWrapBg(wrap, stream, (opened) => { if (!opened) return; onChat(room, id, opened); @@ -2508,7 +2515,11 @@ export function messagesFeature(ctx) { if (!newer && !fill) continue; Object.assign(have, jmSubset(jm), { name: have.name || jm.name, relays: have.relays && have.relays.length ? have.relays : jm.relays }); const stale = rooms.get(have.community_id); // rebuilt with the new keys on next open - if (stale) { for (const u of stale.unsubs || []) { try { u(); } catch {} } rooms.delete(have.community_id); } + if (stale) { + for (const u of stale.unsubs || []) { try { u(); } catch {} } + for (const id of stale.wrapIds || []) seenWraps.delete(id); + rooms.delete(have.community_id); + } changed = true; continue; } diff --git a/tools/chat-layout-test.js b/tools/chat-layout-test.js index 34c8dc0..875f75b 100644 --- a/tools/chat-layout-test.js +++ b/tools/chat-layout-test.js @@ -51,9 +51,8 @@ try { await page.type('textarea', generateMnemonic(wordlist)); await click('button', 'Open wallet'); await waitText('receive', 15000); - // .header-msgs is the shared header-button class — settings, lock and - // search wear it too, and the first one is no longer this one - await page.evaluate(() => [...document.querySelectorAll('button')] + // Messages is a button in the bottom nav + await page.evaluate(() => [...document.querySelectorAll('.app-nav-button')] .find((b) => /message/i.test(b.getAttribute('aria-label') || ''))?.click()); await sleep(800); await click('button', 'Create'); -- cgit v1.2.3 From f247eb1de2941c4ef51378f0b773d1d2a1434c7f Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:26:57 -0700 Subject: Feed URLs: the default feed is not opened (and fetched) a tick before the URL's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A /feed?r=… (or any /feed/… link) set the feed view up synchronously but chose its feed in a setTimeout, so the first paint opened the remembered/ default feed — Popular since c89050e — and fetched the shared snapshot for it before switching. The frame now holds (header only) for that tick, the way a note link's pending frame does. public-feed-browser-test: custom relays bypass the snapshot again. Co-Authored-By: Claude Opus 5.5 --- src/features/messages.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index 02a19b4..241ab5c 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2149,7 +2149,11 @@ export function messagesFeature(ctx) { // public data, and Back lands on the app's own front door. if (urlFeed) { ui.pubProf = true; ui.chatOpen = true; ui.msgView = 'feed'; + // hold the frame until the URL's feed is chosen: painted a tick early, + // the feed view would open (and fetch) the default feed first + ui.pubFeedPending = true; setTimeout(() => { + ui.pubFeedPending = null; if (openFeedUrl(urlFeed.path, urlFeed.search)) return; ui.pubProf = null; ui.chatOpen = false; ui.msgView = null; // not a feed we know: the front door render(); @@ -12829,6 +12833,7 @@ export function messagesFeature(ctx) { ctx.brandHeader(false), h('div', { class: 'card row gap6', style: 'justify-content:center;align-items:center;padding:16px' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small faint' }, t('noteRefLoading')))); + if (ui.pubFeedPending) return h('div', { class: 'col', style: 'gap:16px' }, ctx.brandHeader(false)); if (ui.pubProfPending) return h('div', { class: 'col', style: 'gap:16px' }, ctx.brandHeader(false), h('div', { class: 'card col', style: 'gap:12px' }, -- cgit v1.2.3 From d31bdc6067841e0707baf7e2ad3bf90a823e730a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:27:37 -0700 Subject: Popular: the single file opened from a disk does not ask file:///api/feed The shared Popular snapshot is fetched from a relative /api/feed. Opened from disk (dist/standalone.html) that is file:///api/feed, a request for a file beside it that never exists (standalone-file-test: 'nothing looked for beside the file'). Only ask when served over http(s); the relay path covers the rest, as it already did for a failed fetch. Co-Authored-By: Claude Opus 5.5 --- src/features/messages.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index 241ab5c..1a28adb 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -4806,8 +4806,9 @@ export function messagesFeature(ctx) { const curateFeed = (evs) => { const lang = getLang(); return evs.filter((e) => !isMachinePost(e.content) && inLanguage(e.content, lang)); }; async function popularPass(extra, merge, c, def) { // Only the default public feed shares a snapshot. Custom relays and older - // pages keep their own queries. A static/offline install falls back quickly. - if (extra.until == null && !feedRelays(def)) { + // pages keep their own queries. A static/offline install falls back quickly; + // the single file opened from a disk has no server to ask at all. + if (extra.until == null && !feedRelays(def) && /^https?:$/.test(location.protocol)) { try { const response = await fetch('/api/feed', { signal: AbortSignal.timeout(1500) }); if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) throw new Error('No feed cache'); -- cgit v1.2.3