Skip to content

feat: new-since-last-visit filter with keyboard-reachable filters - #35

Merged
ericboehs merged 1 commit into
masterfrom
feat/new-since-last-visit
Aug 31, 2026
Merged

feat: new-since-last-visit filter with keyboard-reachable filters#35
ericboehs merged 1 commit into
masterfrom
feat/new-since-last-visit

Conversation

@ericboehs

Copy link
Copy Markdown
Owner

What

Two things, both about getting through the listing faster.

New since last visit

The listing marks stories that were not there on your previous load, and a pill under the score filter narrows the page to just those.

It runs in the browser on purpose: the pages are served public with one ETag shared by every visitor, so per-visitor state cannot live on the server without sessions or giving up caching.

  • Baseline is a set of story ids, not a timestamp. A story is often hours old by the time its score lifts it into the listing; filtering on when it was posted would hide exactly those arrivals.
  • Five-minute grace window. Refreshes in quick succession keep the same marks rather than clearing them before they have been read.
  • Bounded state. The seen map lives in localStorage under hn.visit.v1 and expires after seven days.
  • No dead controls. The pill ships hidden and is revealed only after the script confirms it can write to storage, so a visitor without JS or in Safari private mode never sees a toggle that does nothing.

Date dividers are now rebuilt from the visible rows on every filter change. Inserting them once left them stranded above stories from another day as soon as rows were hidden; the rebuild also retires the old textContent.includes dedupe hack.

Keyboard-reachable filters

k past the first story steps up into the filters — the new-since pill, then the score pills, landing on the score already in effect — where h/l walks the row and space applies it. j walks back down.

  • h/l is clamped inside a filter row, so stepping past 0 or 1000 cannot drop focus into the page heading behind it.
  • Hidden rows are skipped, and the walk no longer throws at the ends of the list.
  • ? opens a cheatsheet listing the keys each page actually binds (the listing advertises the filter shortcut, story pages do not).
  • UIkit strips the focus ring from subnav links, which left the whole walk invisible; restored for keyboard focus only.

Testing

bundle exec rake test — 127 runs, 0 failures. The Ruby tests are structural guards (control ships hidden, row ids present, nav hooks intact); behaviour was driven end-to-end in headless Chrome over CDP with real key events, in both colour schemes:

  • first visit marks nothing, a later load marks only the arrivals, grace window holds across a quick refresh, empty state swaps the table for the placeholder
  • j → story, k → toggle, k → active score pill, h h → clamped at 0, space → navigates / toggles, ? → opens, ?/Esc → closes, nav keys stand down while it is open

The README documents a console snippet for exercising the filter without waiting for HN to post something.

The listing now marks stories that were not present on the previous load and
can be narrowed to just those. The filter runs in the browser on purpose: the
pages are served public with one ETag shared by every visitor, so per-visitor
state cannot live on the server without sessions or giving up caching.

The baseline is the set of story ids from the last load rather than a posting
timestamp -- a story is often hours old by the time its score lifts it into the
listing, and filtering on when it was posted would hide exactly those arrivals.
Refreshes within five minutes of each other keep the same marks instead of
clearing them before they have been read, and the seen map expires after seven
days so it cannot grow without bound. The control ships hidden and is revealed
only once the script has confirmed it can write to storage, so a visitor
without JS never sees a dead toggle.

Date dividers are now rebuilt from the visible rows on every filter change.
Inserting them once left them stranded above stories from another day as soon
as rows were hidden, and the rebuild also retires the textContent dedupe hack.

j/k reach the filters: past the first story they step up to the new-since
toggle and then the score pills, landing on the score already in effect, where
h/l walks the row and space applies it. h/l is clamped inside a filter row so
it cannot drop focus into the heading behind it, keystrokes skip hidden rows,
and ? opens a cheatsheet of the keys each page actually binds. UIkit strips the
focus ring from subnav links, which left the whole walk invisible, so it is
restored for keyboard focus only.
@ericboehs
ericboehs merged commit bb7c7a7 into master Aug 31, 2026
2 checks passed
@ericboehs
ericboehs deleted the feat/new-since-last-visit branch August 31, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant