diff options
| -rw-r--r-- | src/features/messages.js | 19 | ||||
| -rw-r--r-- | tools/feed-stability-test.js | 9 |
2 files changed, 20 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 7af2259..7cd8ce3 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -4154,7 +4154,7 @@ export function messagesFeature(ctx) { // An already warmed feed can paint synchronously on entry. if (!c.booting) c.presentations.clear(); watchFeed(); - c.boot.then(() => refreshFeed({}, c)); + refreshFeed({}, c); try { window.scrollTo({ top: 0 }); } catch {} render(); } @@ -4374,7 +4374,8 @@ export function messagesFeature(ctx) { // beat after the page painted, moving what you had started reading. Only // a first load (nothing to disturb) or a rebuilt follow list goes straight in. async function refreshFeed(opts = {}, c = feedNow()) { - if (c.booting) await c.boot; + // the relays are asked at once; what they answer waits for the warm-up + // at the door (mergeFeed), not the asking if (!feedHasQuery(feedDef(c.id))) { c.status = 'ready'; return; } if (!opts.force && Date.now() - c.at < 30_000) return; c.at = Date.now(); @@ -6759,13 +6760,18 @@ export function messagesFeature(ctx) { const authors = feedAuthors(def); const hasQuery = feedHasQuery(def); prepareFeedAhead(c); - const visible = c.booting ? [] : c.notes.filter((ev) => !hidden(ev)); + // Never a bare spinner over posts we already hold: while the opening + // page's faces and pictures are still being decoded, the cached rows + // paint as they are (live profiles, pictures as they load) and take + // their settled presentation once the warm-up lands. The spinner is + // for a feed with nothing cached at all. + const visible = c.notes.filter((ev) => !hidden(ev)); // Every child keyed by its post, hairlines included, so the morph // reconciles the list by post: a new one at the top is inserted as its // own node; the rest keep theirs. const rows = visible.slice(0, c.shown || FEED_PAGE).flatMap((ev, i) => [ i ? h('div', { 'data-key': 'hr:' + ev.id, style: 'height:1px;background:var(--border,rgba(128,128,128,.18));margin:0 -14px' }) : null, - feedRow(c, ev), + c.booting ? keyed(noteRow(ev.pubkey, ev, displayName(ev.pubkey)), ev.id) : feedRow(c, ev), ]); // Posts that went in above you while you were reading. A floating pill // that says how many are up there; the tap takes you up to them. @@ -6808,7 +6814,7 @@ export function messagesFeature(ctx) { ? h('div', { class: 'row gap6', style: 'justify-content:center;padding:12px 0' }, h('span', { class: 'spinner sm' })) : !visible.length ? h('div', { class: 'small faint', style: 'text-align:center;padding:12px 0' }, t('feedEmpty')) - : h('div', { class: 'card col notes-feed', style: 'gap:0' }, ...rows), + : h('div', { class: 'card col notes-feed', style: 'gap:0', 'data-booting': c.booting ? '1' : undefined }, ...rows), // Older posts prepare offscreen; a background fetch should not add // a spinner to an already readable feed. @@ -7280,6 +7286,9 @@ export function messagesFeature(ctx) { function homeView() { if (myPubkeys().length) syncFollowSets().catch(() => {}); // lists made elsewhere join the feeds (throttled inside) + // The feed is a tap away from here: have it warmed and asked for before + // the tap, so it opens on the latest posts rather than a spinner. + if (myPubkeys().length) { try { const c = feedNow(); if (!c.booting) refreshFeed({}, c); } catch {} } syncReports().catch(() => {}); // your follows' flags (throttled inside) startDMs(); // Threads you've since replied to should stop being strangers to the diff --git a/tools/feed-stability-test.js b/tools/feed-stability-test.js index a1b3a1e..a176a0b 100644 --- a/tools/feed-stability-test.js +++ b/tools/feed-stability-test.js @@ -113,10 +113,13 @@ try { await page.waitForSelector('.item'); await page.evaluate(() => [...document.querySelectorAll('.item')].find((e) => /feed/i.test(e.textContent))?.click()); await sleep(1000); - check('cached rows wait for their slow avatar', !(await page.$('.notes-feed > .row'))); + // Cached posts paint at once — never a spinner over posts we hold — and + // settle into their prepared presentation once faces and pictures are in. + check('cached rows paint at once, before their slow avatar has loaded', !!(await page.$('.notes-feed[data-booting] > .row'))); + await page.waitForSelector('.notes-feed:not([data-booting]) > .row', { timeout: 15000 }); await page.waitForSelector(row(seeded[0].id), { timeout: 15000 }); - check('quoted note and mention resolved before admission', await page.$eval(row(seeded[1].id), (n) => n.innerText.includes('Quoted content') && n.innerText.includes('@Mentioned Person'))); - check('all five gallery images decoded before admission', await page.$eval(row(seeded[8].id), (n) => n.querySelectorAll('.note-img').length === 5 && [...n.querySelectorAll('.note-img')].every((i) => i.complete && i.naturalWidth > 0))); + check('quoted note and mention resolved once settled', await page.$eval(row(seeded[1].id), (n) => n.innerText.includes('Quoted content') && n.innerText.includes('@Mentioned Person'))); + check('all five gallery images decoded once settled', await page.$eval(row(seeded[8].id), (n) => n.querySelectorAll('.note-img').length === 5 && [...n.querySelectorAll('.note-img')].every((i) => i.complete && i.naturalWidth > 0))); check('the next page was prefetched before scrolling', requested.has('/next.png')); check('an unavailable image remains a usable link', await page.$eval(row(seeded[3].id), (n) => !n.querySelector('img.note-img') && !!n.querySelector('a[href$="/late.png"]'))); const initial = await snapshot(); |
