diff options
| author | Adam Soltys <[email protected]> | 2026-10-13 04:48:44 +0000 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-13 04:48:44 +0000 |
| commit | 75a5bec9490ce1e28b7c37b864f9ca9d6e621204 (patch) | |
| tree | da1d61d1cdcba4562a9895662cb80ce70da4fae9 | |
| parent | b22832ffd5df280811559cdbf0455305321e160c (diff) | |
| download | coinosv3-75a5bec9490ce1e28b7c37b864f9ca9d6e621204.tar.gz coinosv3-75a5bec9490ce1e28b7c37b864f9ca9d6e621204.zip | |
Feed pager: closing it lands on the post of the slide last looked at, and the clips ahead are looked for sooner. Adam swiped to the last clip (one more loaded, slowly), closed the pager and the feed was somewhere else. Closing used glideToPost: for a row never drawn — a post the pager went on to, maybe paged in while it was open — it guesses the place from the rows' average height, draws there, and glides once to wherever the row then is; rows of very different heights (a long post, a clip finding its shape) put the guess far off, and the glide was never corrected as they settled (in the test: 325px past it; a row still undrawn after the guess sent the feed to the top). Now landOnPost jumps (the pager was covering the feed), draws, and sets the row under the top again each frame until it has sat still a moment, 2.5 s have gone, the reader touches the page, or the page is sent elsewhere. And the pager keeps more ahead before paging the feed on: six slides for clips (fewer and further between, each watched a while), four for pictures (was three). feed-pager-ahead-test: some of the words-only posts are long now, and closing on the second-last picture checks the feed sits on its post after the pictures above have loaded (fails before: row top -325).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
| -rw-r--r-- | src/features/messages.js | 50 | ||||
| -rw-r--r-- | tools/feed-pager-ahead-test.js | 19 |
2 files changed, 59 insertions, 10 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 2580ed9..9cba810 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -11934,12 +11934,48 @@ export function messagesFeature(ctx) { let tries = 0; const go = () => { if (ui.mediaPager || !document.querySelector('.notes-feed')) { if (++tries < 90) requestAnimationFrame(go); return; } - const list = c.winList ? c.winList() : c.notes; - const i = list.findIndex((e) => e.id === rowId); - if (i >= 0) glideToPost(c, list, i); + landOnPost(c, rowId); }; requestAnimationFrame(go); } + // Put the feed on a post the reader hasn't been near — one the pager went + // on to, maybe paged in while it was open and never drawn. Its place is + // only a guess until it is drawn, and the rows around it settle as they + // draw (a long post, a picture finding its size), so: jump to the guess, + // draw, and set the row under the top again each frame — until it has sat + // still a moment, a couple of seconds have gone, or the reader touches the + // page. A jump, not a glide: the pager was covering the feed. + function landOnPost(c, rowId) { + const MARGIN = 8, t0 = Date.now(); + const find = () => document.querySelector('.notes-feed > [data-key="' + CSS.escape(rowId) + '"]'); + let still = 0, misses = 0, lastY = null; + const step = () => { + if (c !== feed || ui.mediaPager || !document.querySelector('.notes-feed') || lastInputAt > t0 || Date.now() - t0 > 2500) return; + // the page sent somewhere else meanwhile (not by a finger: a glide, a link) + if (lastY !== null && Math.abs(window.scrollY - lastY) > window.innerHeight / 2) return; + const el = find(); + if (!el) { + const list = c.winList ? c.winList() : c.notes; + const i = list.findIndex((e) => e.id === rowId); + if (i < 0 || ++misses > 20) return; + let sum = 0, n = 0; + for (const v of (c.heights || new Map()).values()) { sum += v; n++; } + const avg = n ? sum / n : 200; + let y = 0; + for (let j = 0; j < i; j++) y += ((c.heights && c.heights.get(list[j].id)) || avg) + ROW_GAP; + window.scrollTo(0, Math.max(0, (c.listTop || 0) + y - MARGIN)); + render(); + requestAnimationFrame(step); + return; + } + const off = el.getBoundingClientRect().top - MARGIN; + if (Math.abs(off) > 2) { window.scrollTo(0, Math.max(0, window.scrollY + off)); still = 0; } + else if (++still >= 20 && Date.now() - t0 > 600) return; + lastY = window.scrollY; + requestAnimationFrame(step); + }; + step(); + } // Leaving the pager for another page (a profile, sign-in, the zap amount): // only the feed's own render syncs the pager, and those pages don't paint // the feed — so the overlay goes now, not on the next feed render. @@ -12262,11 +12298,13 @@ export function messagesFeature(ctx) { } // A pager opened from the feed walks the feed's own posts, which page in // ten at a time as the feed is scrolled. Swiping is the scrolling here: - // with only a few slides left past the one on screen, the feed pages on + // with only a few slides left past the one on screen (PAGER_AHEAD), the feed pages on // (the posts it holds, then older ones from the relays) until there are // more — several pages if those posts carry none. A page that brings // nothing (the feed busy, or at its end) stops it; the tick tries again. - const PAGER_AHEAD = 3; + // how many slides ahead to keep: clips are fewer and further between + // (more pages to find the next) and each is watched a while, so further + const PAGER_AHEAD = { images: 4, videos: 6 }; let pagerFeeding = false; async function pagerFeedAhead() { if (pagerFeeding || !pager || !pager.fromFeed) return; @@ -12275,7 +12313,7 @@ export function messagesFeature(ctx) { const c = feedNow(); for (let pages = 0; pages < 8 && pager && pager.fromFeed; pages++) { const slides = [...pager.scroller.children]; - if (slides.length - 1 - slides.indexOf(pager.cur) > PAGER_AHEAD) break; + if (slides.length - 1 - slides.indexOf(pager.cur) > (PAGER_AHEAD[pager.kind] || 4)) break; const shown = c.shown, held = c.notes.length; await loadOlderFeed(); growPager(); diff --git a/tools/feed-pager-ahead-test.js b/tools/feed-pager-ahead-test.js index b5a20ba..504c122 100644 --- a/tools/feed-pager-ahead-test.js +++ b/tools/feed-pager-ahead-test.js @@ -1,5 +1,5 @@ // Swiping through the feed's pictures (or clips) in the pager is scrolling -// the feed: with three slides or fewer left past the one on screen, the +// the feed: with four slides or fewer left past the one on screen (six for clips), the // feed pages on, and the pager grows — past a run of posts with nothing to // show, too — so the reader never meets the end while there is more. // Run: bun tools/feed-pager-ahead-test.js @@ -28,7 +28,7 @@ const U = (n) => server.url.origin + '/p' + n + '.png'; const seeded = []; let ago = 100, pic = 0; for (let i = 0; i < 12; i++) seeded.push(post('picture number ' + pic + ' ' + U(pic++), ago += 60)); -for (let i = 0; i < 15; i++) seeded.push(post('just words ' + i + ' ' + 'lorem ipsum dolor sit amet. '.repeat(3), ago += 60)); +for (let i = 0; i < 15; i++) seeded.push(post('just words ' + i + ' ' + 'lorem ipsum dolor sit amet. '.repeat(i % 3 ? 3 : 90), ago += 60)); // some of them long: rows of very different heights for (let i = 0; i < 14; i++) seeded.push(post('picture number ' + pic + ' ' + U(pic++), ago += 60)); const TOTAL = pic; @@ -126,9 +126,9 @@ try { for (let step = 0; step < TOTAL + 4; step++) { const n = await slides(), i = await curIdx(); maxSlides = Math.max(maxSlides, n); - if (i >= 0 && n - 1 - i <= 3) { + if (i >= 0 && n - 1 - i <= 4) { // give the look-ahead a moment: it should get past three ahead again (or the feed's end) - await waitFor((k) => { const sc = document.querySelector('.mp-scroll'); const j = [...sc.children].findIndex((x) => Math.abs(x.offsetTop - sc.scrollTop) < 4); return sc.children.length - 1 - j > 3 || sc.children.length >= k; }, TOTAL, 6000); + await waitFor((k) => { const sc = document.querySelector('.mp-scroll'); const j = [...sc.children].findIndex((x) => Math.abs(x.offsetTop - sc.scrollTop) < 4); return sc.children.length - 1 - j > 4 || sc.children.length >= k; }, TOTAL, 6000); } const before = await curIdx(); await page.keyboard.press('ArrowDown'); @@ -139,6 +139,17 @@ try { check('every picture in the feed became reachable', (await slides()) === TOTAL, (await slides()) + ' of ' + TOTAL); check('…past fifteen posts with none', (await curName()) === 'p' + (TOTAL - 1) + '.png', await curName()); check('…in the feed\'s order', await page.evaluate((t) => [...document.querySelector('.mp-scroll').children].every((s, i) => s.getAttribute('data-k').endsWith('/p' + i + '.png')), TOTAL)); + + console.log('\n[closing lands the feed on the last picture\'s post]'); + await page.keyboard.press('ArrowUp'); + await waitFor((n) => { const sc = document.querySelector('.mp-scroll'); const s = [...sc.children].find((x) => Math.abs(x.offsetTop - sc.scrollTop) < 4); return s && s.getAttribute('data-k').endsWith('/p' + n + '.png'); }, TOTAL - 2, 3000); + const want = 'picture number ' + (TOTAL - 2) + ' '; + await page.keyboard.press('Escape'); + check('the pager closes', await waitFor(() => !document.querySelector('.media-pager') && !!document.querySelector('.notes-feed'), null, 3000)); + const top = () => page.evaluate((w) => { const r = [...document.querySelectorAll('.notes-feed > [data-key]')].find((n) => n.textContent.includes(w)); return r ? Math.round(r.getBoundingClientRect().top) : null; }, want); + await sleep(2500); // the pictures above it load meanwhile + const t = await top(); + check('…the feed sits on the post of the picture last looked at', t !== null && t >= -10 && t < 120, 'row top ' + t); check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); } finally { await browser.close(); server.stop(true); } console.log(ok ? '\n✅ the pager pages the feed on ahead of the reader' : '\n❌ failed'); |
