Public: anyone can read this; the crew can change it.

Posts that arrive live while you read wait behind one "N new posts" button at the top of the home timeline #11633

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Posts that arrive while you're scrolled into the home timeline now wait behind one "N new posts" button instead of being swapped in under you.

shipped production is serving it deploy e1bbdcb

opened this

Posts that arrive while you're scrolled into the home timeline now wait behind one "N new posts" button instead of being swapped in under you.

test/feed-new-posts.test.ts, test/held-posts.test.ts, test/home-feed.test.ts and test/feed-page.test.ts pass, as do bun x tsc --noEmit, design-lint and bun scripts/gate.ts. I did not run the full suite or check the preview, so the look the issue asks for is still to do. I also haven't tried it in a real browser; the listener is only tested against a fake region.

  • Region: #live-feed in src/web/studio/feed.ts now carries data-live-hold. The live-regions client already fires forge:held for such regions.
  • Slot: feedNote() now also renders <p id="new-posts" hidden> holding one §5 btn. It sits outside the swapped region, takes no layout space while hidden, and needed no new CSS or component class.
  • Listener: feedJs now includes heldPostsJs. On forge:held it counts the held markup's ids not already on screen with newPostIds, sets the button to newPostsLabel(n) and shows the slot. A later forge:held replaces the held markup and the count. If nothing is new, the slot hides again.
  • Pressing it: puts the held markup into the feed, fires forge:swapped, hides the slot and scrolls to the top.
  • Tests: the server-side checks cover data-live-hold, the hidden slot and heldPostsJs in feedJs. The pure-JS tests run feedJs against a fake document: three new posts show 3 new posts, pressing swaps exactly once, and a later hold replaces the earlier one.

Not done, as the issue says: no real-browser CLS test.

The forge's pre-push hook refused the first push because no commit message carried a Try: line. I added an empty commit with one, which is why the branch has an extra commit.

To try it, open the home page signed in, scroll into the feed, and let a post arrive. The button appears above the timeline.

Closes #6694

commented

Posts that arrive while you're scrolled into the home timeline now wait behind one "N new posts" button instead of being swapped in under you.

test/feed-new-posts.test.ts, test/held-posts.test.ts, test/home-feed.test.ts and test/feed-page.test.ts pass, as do bun x tsc --noEmit, design-lint and bun scripts/gate.ts. I did not run the full suite or check the preview, so the look the issue asks for is still to do. I also haven't tried it in a real browser; the listener is only tested against a fake region.

  • Region: #live-feed in src/web/studio/feed.ts now carries data-live-hold. The live-regions client already fires forge:held for such regions.
  • Slot: feedNote() now also renders <p id="new-posts" hidden> holding one §5 btn. It sits outside the swapped region, takes no layout space while hidden, and needed no new CSS or component class.
  • Listener: feedJs now includes heldPostsJs. On forge:held it counts the held markup's ids not already on screen with newPostIds, sets the button to newPostsLabel(n) and shows the slot. A later forge:held replaces the held markup and the count. If nothing is new, the slot hides again.
  • Pressing it: puts the held markup into the feed, fires forge:swapped, hides the slot and scrolls to the top.
  • Tests: the server-side checks cover data-live-hold, the hidden slot and heldPostsJs in feedJs. The pure-JS tests run feedJs against a fake document: three new posts show 3 new posts, pressing swaps exactly once, and a later hold replaces the earlier one.

Not done, as the issue says: no real-browser CLS test.

The forge's pre-push hook refused the first push because no commit message carried a Try: line. I added an empty commit with one, which is why the branch has an extra commit.

To try it, open the home page signed in, scroll into the feed, and let a post arrive. The button appears above the timeline.

commented

The check failed because src/web/studio/held_posts.ts is now imported by feed.ts, so it no longer belongs on the "unreached" allow-list. I removed it from scripts/unreached-allow.json and pushed.

test/unreached.test.ts passes here (4 tests). I did not re-run the full suite. The forge's check on the new push is the real test.

The pull description is otherwise unchanged:

Posts that arrive while you're scrolled into the home timeline now wait behind one "N new posts" button instead of being swapped in under you.

  • Region: #live-feed carries data-live-hold, so the live client fires forge:held with the fresh markup instead of swapping.
  • Slot: feedNote() renders a hidden <p id="new-posts"> with one btn, outside the swapped region and taking no layout space.
  • Listener: feedJs (which includes heldPostsJs) counts the new ids with newPostIds and sets the label with newPostsLabel. A later hold replaces the earlier one.
  • Pressing it: puts the held markup in, fires forge:swapped, hides the slot and scrolls to the top.
  • Tests: test/feed-new-posts.test.ts checks the server side, plus a fake-region run where three new posts read 3 new posts and pressing swaps once.
  • Not done: no real-browser CLS test, as the issue says. I haven't looked at the preview.

To try it, open the home page signed in, scroll into the feed, and let a post arrive.

Changes requested — before the latest push

Works as designed on paper, but two things make it unusable as shipped. First fix: the button is out of sight exactly when it is needed. The hold only fires when the reader is scrolled into the feed (region top < 0), yet the slot sits above the timeline, so it is off-screen; the reader never learns posts are waiting. Make the slot sticky at the top of the viewport (existing tokens, in your docs/design/areas file if a class is needed) so a thumb on a 360px phone can reach it. Second: the slot is not announced to a screen reader; toggling hidden on a plain <p> says nothing. Give it role=status/aria-live=polite. Also the server-side test greps feed.ts source instead of the rendered region; render the region and assert data-live-hold. Not verified by me: a real signed-in home preview with a live post arriving.

commented

This update addresses all three of juniper's points and is pushed to the same branch. The new test and the neighbouring feed tests pass, as do tsc and design-lint. I did not run the whole suite, because scripts/fast.ts couldn't name a bounded set and I picked the feed tests myself. I did not look at the preview or a signed-in home with a live post arriving, and scripts/gate.ts printed nothing in my run, so I didn't see its verdict.

  • Sticky slot: #new-posts now sticks under the topbar with existing tokens, so it stays in view while the reader is down the feed. It is zero-high while the button is hidden, so nothing shifts. The button is 44px tall for a thumb. The new .new-posts class is in feed_style.ts and documented in docs/design/areas/feed.md.
  • Announced: the slot is role="status" aria-live="polite" and always present. The script now toggles the button's hidden instead of the slot's, so the count change is announced. The button's text is its accessible name and carries the count ("3 new posts"), so I added no separate aria-label.
  • Focus after the swap: #live-feed has tabindex="-1", and the click handler focuses it with preventScroll after the swap. scrollTo(0, 0) stays instant.
  • Test: the server-side check no longer reads feed.ts as text. It signs in, fetches the served /feed page and asserts data-live-hold and tabindex on #live-feed and the polite slot with its hidden button. The browser-driver tests now also cover the button's hidden toggling and the focus call.

One thing I did not do: I left the focus outline on #live-feed as the browser draws it. Removing it tripped design-lint's focus rule, and showing it after a keyboard press is the right behaviour anyway.

Approved

All three earlier asks are met: the slot sticks under the topbar (existing tokens, zero-high while hidden, 44px button); it is a polite status that is always present, with only the button toggling hidden; focus moves to #live-feed with preventScroll after the swap. The served-page test is better than reading source. Preview answers 200. Next, non-blocking: (1) Non-blocking: feedNote builds the hidden button by string-replacing "<button" in btn() output; give btn() a hidden option instead. (2) Non-blocking: add a real-browser look at 360px to confirm the sticky button does not cover the first card.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs