aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-11 20:56:32 -0700
committerAdam Soltys <[email protected]>2026-10-11 20:56:32 -0700
commita34769e3f60aa5b6920e4e2c9e0a9ee1013cacd1 (patch)
tree86ec1a56bec9beffda756df571b4dc496f50a603 /src
parent8f5f0b5fb1ea24390568b5fdce2af4def7cedef2 (diff)
downloadcoinosv3-a34769e3f60aa5b6920e4e2c9e0a9ee1013cacd1.tar.gz
coinosv3-a34769e3f60aa5b6920e4e2c9e0a9ee1013cacd1.zip
Feed: the wheel coast no longer stalls or reverses as pictures land. A row that painted before its picture's size was known (a cached row on a feed just opened, a link card, a clip learning its shape) grew after it was measured, and the stale height is what the spacer took when the row left the window: the page shrank by the difference, the browser's scroll anchoring pulled the reader back by as much, and a coasting scroll lost ground (or ran backwards) right as the media appeared — on a feed opened fresh, 18 such frames in four seconds. Rows are now watched by a ResizeObserver (after layout, nothing forced) so the heights stay live; originals decode async so a big one never holds a paint. Tried and dropped: shrinking no-CORS originals through createImageBitmap — it re-decodes on the main thread. tools/feed-scroll-jank.js grows a hands-off coast scenario (--coast: quick-notch bursts, then let go; dips/jumps in the coast's own travel next to what landed: media loads, layout shifts, row batches, page height, scrollTo/scrollBy callers), --trace (a Chrome trace: the browser's work inside each long frame — decode, layout, paint, GC — and the compositor's dropped-frame count), --url for a never-seen topic feed, --only settled|coast, --start px. Measured: the fresh-feed coast went from 18 dips to 8 small ones; a fresh topic feed coasts with one 33 ms frame in five seconds. Still open: a 9528×6328 original from a host without CORS (primal's) once froze a scroll step for 400 ms on its texture upload — only a resizing proxy fixes that one. feed-stability's "a small feed query starts before opening the feed" and "an unavailable image remains a usable link" fail on 491904e too.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js23
1 files changed, 22 insertions, 1 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 278e66f..bb2d788 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -7098,17 +7098,37 @@ export function messagesFeature(ctx) {
if (changed && repaint && c.win && (c.win.top || c.win.bottom)) holdScroll(render);
});
}
+ // A row changes height after it was measured — a picture lands in a row
+ // that painted before its size was known, a clip learns its shape, a link
+ // card arrives. The stale number is what the spacer uses when the row
+ // leaves the window: the page then shrinks or grows by the difference,
+ // the browser's scroll anchoring pulls the reader back by as much, and a
+ // coasting wheel scroll stalled or even reversed right as the media
+ // appeared. Keep the measurements live instead (no layout forced: the
+ // observer runs after layout).
+ const rowSizer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver((entries) => {
+ for (const en of entries) {
+ const r = en.target, c = r.parentElement && r.parentElement._winC;
+ if (!c || !r.isConnected) continue;
+ const hgt = r.getBoundingClientRect().height, key = r.getAttribute('data-key');
+ if (hgt > 0 && key) (c.heights || (c.heights = new Map())).set(key, hgt);
+ }
+ });
function measureNow(c) {
const list = document.querySelector('.notes-feed');
if (!list) return false;
c.listTop = list.getBoundingClientRect().top + (window.scrollY || 0);
+ list._winC = c;
const heights = c.heights || (c.heights = new Map());
let changed = false;
+ const seen = new Set();
for (const r of list.querySelectorAll(':scope > .row[data-key]')) {
const hgt = r.getBoundingClientRect().height;
const key = r.getAttribute('data-key');
if (hgt > 0 && Math.abs((heights.get(key) || 0) - hgt) > 0.5) { heights.set(key, hgt); changed = true; }
+ if (rowSizer) { seen.add(r); if (!(list._sized ||= new Set()).has(r)) { list._sized.add(r); rowSizer.observe(r); } }
}
+ if (rowSizer && list._sized) for (const r of list._sized) if (!seen.has(r)) { list._sized.delete(r); rowSizer.unobserve(r); }
return changed;
}
// On scroll: replenish the buffer before the reader reaches its edge,
@@ -7897,7 +7917,8 @@ export function messagesFeature(ctx) {
const known = size || mediaReady.get(url);
return h('img', {
src: mediaSrc(url, known), class: 'note-img clickable', loading: feedPaint || known ? 'eager' : 'lazy', alt: label || '',
- decoding: known && known.thumb ? 'sync' : undefined, // a few-ms decode: paint it with the page
+ // a few-ms decode of the copy paints with the page; an original never holds the page for its decode
+ decoding: known && known.thumb ? 'sync' : 'async',
width: size?.width, height: size?.height,
style: size ? 'height:auto;aspect-ratio:' + size.width + '/' + size.height + ';--ar:' + (size.width / size.height).toFixed(4) : undefined,
onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); ctx.openImage && ctx.openImage(url); },