aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-02 10:11:37 -0700
committerAdam Soltys <[email protected]>2026-10-02 10:11:37 -0700
commitef6729ceaf49d2d85bc8920c2c7756086ee7450d (patch)
tree08c40899ded610444511b546681d8034cb869443
parent8249fbe964ae63fa5a34b150b2e5819972d93e5c (diff)
downloadcoinosv3-ef6729ceaf49d2d85bc8920c2c7756086ee7450d.tar.gz
coinosv3-ef6729ceaf49d2d85bc8920c2c7756086ee7450d.zip
New-post tint fades the moment any of the post is on screen
It waited for half the row to sit in view for two seconds; scrolling reset the clock and a replaced node lost it, so a tall post read in passing or one the window remounted could stay green for good. Now the first visible pixel starts a 3s fade (the row's background transition), marked seen by id, whether the reader stays or scrolls on. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
-rw-r--r--src/features/messages.js29
-rw-r--r--src/style.css8
-rw-r--r--tools/feed-new-posts-test.js14
3 files changed, 25 insertions, 26 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 10c9d56..176c779 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -5305,31 +5305,24 @@ export function messagesFeature(ctx) {
finally { feedPaint = prev; }
}
- // New posts stay tinted until they've been in view for FRESH_SEEN_MS: a
- // row at least half on screen (or filling most of it) starts its clock,
- // leaving view stops it. Seen, it loses the tint in place — no repaint.
- const FRESH_SEEN_MS = 2000;
+ // A new post wears its tint until any of it reaches the screen; from that
+ // moment it fades out (the row's 3s background transition, in the CSS)
+ // whether you stay or scroll on. It used to wait for half the row to sit
+ // in view for two seconds, with the clock reset by every scroll and lost
+ // when the row's node was replaced, so a tall post read in passing, or one
+ // the window remounted, could stay green for good.
let freshObserver = null;
- const freshTimers = new Map();
function watchFresh(c) {
if (typeof IntersectionObserver === 'undefined') return;
freshObserver ||= new IntersectionObserver((entries) => {
for (const en of entries) {
+ if (!en.isIntersecting) continue;
const el = en.target, id = el.getAttribute('data-key');
- const inView = en.isIntersecting && (en.intersectionRatio >= 0.5 || en.intersectionRect.height >= window.innerHeight * 0.5);
- if (inView && !freshTimers.has(id)) {
- freshTimers.set(id, setTimeout(() => {
- freshTimers.delete(id);
- if (!el.isConnected) return;
- feed?.fresh?.delete(id);
- el.classList.remove('note-fresh');
- freshObserver.unobserve(el);
- }, FRESH_SEEN_MS));
- } else if (!inView && freshTimers.has(id)) {
- clearTimeout(freshTimers.get(id)); freshTimers.delete(id);
- }
+ feed?.fresh?.delete(id); // by id: the row may be re-rendered mid-fade
+ el.classList.remove('note-fresh');
+ freshObserver.unobserve(el);
}
- }, { threshold: [0, 0.5, 1] });
+ }, { threshold: 0 });
// after the paint, when the row nodes are the mounted ones
requestAnimationFrame(() => {
for (const el of document.querySelectorAll('.notes-feed > .row.note-fresh')) freshObserver.observe(el);
diff --git a/src/style.css b/src/style.css
index 8918c37..7ef428c 100644
--- a/src/style.css
+++ b/src/style.css
@@ -959,10 +959,10 @@ html.no-scroll { overflow: hidden; scrollbar-gutter: auto; }
border-left: none; border-right: none; border-radius: 0;
box-shadow: none;
}
-/* A post that arrived while you were reading wears a tint until it has been
- on screen a couple of seconds; then it fades to the plain page. The side
- shadows carry the tint through the list's padding to its edges. */
-.notes-feed > .row { transition: background-color .9s ease, box-shadow .9s ease; }
+/* A post that arrived while you were reading wears a tint until any of it
+ comes on screen; then it fades to the plain page over three seconds. The
+ side shadows carry the tint through the list's padding to its edges. */
+.notes-feed > .row { transition: background-color 3s ease-out, box-shadow 3s ease-out; }
.notes-feed > .row.note-fresh {
--fresh-bg: color-mix(in srgb, var(--green) 9%, var(--paper));
background-color: var(--fresh-bg);
diff --git a/tools/feed-new-posts-test.js b/tools/feed-new-posts-test.js
index c3db381..4f5b8fb 100644
--- a/tools/feed-new-posts-test.js
+++ b/tools/feed-new-posts-test.js
@@ -88,10 +88,16 @@ try {
assert(await page.evaluate(()=>scrollY)>0,'not scrolled all the way to the top');
assert(!(await page.$('.feed-new-pill')),'pill is gone after the tap');
await page.screenshot({path:'/tmp/feed-new-posts.png'});
- await sleep(2600);
- assert(!(await page.$('.notes-feed > .row.note-fresh[data-key="'+F(2)+'"]')),'the post you looked at loses its tint');
+ await sleep(300);
+ assert(!(await page.$('.notes-feed > .row.note-fresh[data-key="'+F(2)+'"]')),'the post on screen starts losing its tint at once');
assert(await page.$('.notes-feed > .row.note-fresh[data-key="'+F(0)+'"]'),'the newest, still offscreen above, keeps its tint');
- await page.evaluate(()=>window.scrollTo(0,0)); await sleep(2600);
+ // (this page asks for reduced motion, which rightly turns the fade off; the rule itself says three seconds)
+ assert(await page.evaluate(()=>[...document.styleSheets].flatMap(s=>[...s.cssRules]).some(r=>r.selectorText==='.notes-feed > .row'&&/background-color 3s/.test(r.style.transition))),'...and fades over three seconds');
+ // a sliver on screen is enough: the newest's last few pixels at the top edge
+ await page.evaluate((id)=>{const r=document.querySelector('.notes-feed > [data-key="'+id+'"]').getBoundingClientRect();window.scrollTo(0,scrollY+r.bottom-4);},F(0));
+ await sleep(300);
+ assert(!(await page.$('.notes-feed > .row.note-fresh[data-key="'+F(0)+'"]')),'a post barely on screen starts fading too');
+ await page.evaluate(()=>window.scrollTo(0,0)); await sleep(300);
assert.deepEqual(await fresh(),[],'back at the top, the rest fade once seen');
// swipe the pill away: it goes, the page doesn't move, and it's back once more arrive
await page.evaluate((id)=>{const r=document.querySelector('.notes-feed > [data-key="'+id+'"]');window.scrollTo(0,scrollY+r.getBoundingClientRect().top-100);},reading);
@@ -118,5 +124,5 @@ try {
assert.equal(await page.evaluate(()=>scrollY),y1,'and is not a tap');
assert.equal(await page.$eval('.feed-new-pill',e=>e.style.transform),'','sprung back into place');
assert.deepEqual(errors,[]);
- console.log('✓ New posts: pill lands on the oldest new post; tints stay until each post has been in view ~2s; the reading row stays put; swiping the pill dismisses it until more arrive');
+ console.log('✓ New posts: pill lands on the oldest new post; a tint starts its 3s fade the moment any of the post is on screen; the reading row stays put; swiping the pill dismisses it until more arrive');
} finally { await page.close(); await browser.close(); server.stop(true); }