Skip to content

feat(feed): cursor pagination for the inbox — scroll-end loads older conversations - #5860

Draft
thomaspblock wants to merge 2 commits into
inbox-feed-windowingfrom
inbox-feed-pagination
Draft

feat(feed): cursor pagination for the inbox — scroll-end loads older conversations#5860
thomaspblock wants to merge 2 commits into
inbox-feed-windowingfrom
inbox-feed-pagination

Conversation

@thomaspblock

Copy link
Copy Markdown
Contributor

What

Cursor-based pagination for the Inbox, stacked on #5834 (per-conversation feed windowing). Scrolling to the end of the inbox list now loads the next page of older conversations instead of stopping at the newest window.

Why

#5834 fixed one chatty thread starving the inbox, but capped the inbox at the newest N conversations — anything older was unreachable. Follow-up agreed with @Thomasp: an inclusive until cursor that pages by conversation.

How

Cursor semantics (the load-bearing decision): the cursor bounds conv_latest — each conversation's newest activity — not event time. Pages are whole conversations at-or-older than the cursor. An event-time cursor would skip every conversation whose latest activity falls between the oldest event in hand and that conversation's conv_latest. The boundary conversation intentionally reappears on the next page (inclusive nostr until) and the client dedupes by event id.

  • Relay/DB (buzz-db/src/feed.rs): build_mentions_query gains until on conv_latest; the candidate CTE stays unbounded by the cursor so page membership is stable. Flat feeds (needs_action, activity) bound event time directly. Threaded through all routed Db wrappers and the bridge feed path (filter.until).
  • Desktop: get_feed accepts until; getOldestConversationActivity derives the cursor as min(conv_latest) across conversations in hand, mirroring the relay's conversation key (dm:<channel> / NIP-10 root — gated on channel list load for DM keys); useHomeFeedPagination layers fetched-once older pages under the live 30s-polled first page; InboxListPane fetches within one viewport of scroll-end with a tail spinner.
  • e2e: bridge mirrors until; new inbox-pagination-screenshots.spec.ts proves scroll-end paging against a live isolated relay; the windowing spec's "after" test now step-scrolls the virtualized list (rows outside the viewport aren't in the DOM) and tolerates sibling-spec conversations.

Pagination depth is bounded by FEED_WINDOW_SCAN_CAP (2000 events) — documented, acceptable.

Proof (Playwright, isolated relay on :3030)

60 single-message conversations seeded; page 1 holds the newest 50 — the oldest markers are beyond it. Scroll-end pages them in.

Before scrolling — first page only (marker 0 absent, asserted):

before

After scroll-end pagination — oldest conversation (paged mention 0) loaded:

after

Verification

  • buzz-db feed suite 31/31 against Postgres, incl. new paging test (9 conversations, limit 3, cursor walk with inclusive boundary) and a SQL-shape test asserting the cursor doesn't leak into the candidate scan.
  • buzz-relay lib suite: 870 passed; 9 failures pre-existing on base 4d74e8e (api::media/admin/telemetry — verified via stash).
  • desktop check + typecheck + tests 4778/4778; tauri tests 2415.
  • All 3 inbox Playwright integration tests green together against the live relay.

Deliberately out of scope (unchanged from #5834): needs-action promotion for open PRs, participated-threads recall.

Wintermute and others added 2 commits August 14, 2026 09:18
…conversations

PR #5834 windowed the mentions feed per conversation, which fixed
starvation but capped the inbox at the newest N conversations with no
way to reach older ones. This adds an inclusive `until` cursor across
the whole path so scroll-end loads the next page of older conversations.

Relay/DB:
- build_mentions_query takes `until` bounding conv_latest (the
  conversation's newest activity), NOT event time: pages are whole
  conversations, and no conversation whose latest activity falls
  between an event-time cursor and its conv_latest can be skipped.
  The candidate CTE stays unbounded by the cursor so page membership
  is stable.
- needs_action / activity queries bound event time directly (flat feeds).
- All Db wrappers and routed variants (replica/writer/fallback) thread
  `until`; the bridge feed path parses `filter.until` into all three.

Desktop:
- get_feed accepts `until`; e2e bridge mirrors the wire shape.
- getOldestConversationActivity derives the cursor as min(conv_latest)
  across conversations in hand, mirroring the relay's conversation key
  (dm:<channel> / NIP-10 thread root) — pagination is gated on the
  channel list so DM keys resolve.
- useHomeFeedPagination layers fetched-once older pages under the live
  30s-polled first page; merge dedupes the inclusive boundary overlap
  by event id; `exhausted` stops fetching when a page adds nothing.
- InboxListPane triggers fetchOlder within one viewport of scroll-end,
  re-checked on items growth so short pages chain; tail spinner
  (testid home-inbox-loading-older).

Verification:
- buzz-db feed suite 31/31 (incl. new Postgres paging test: 9
  conversations, limit 3, cursor walks pages with inclusive boundary);
  SQL-shape test asserts the cursor does not leak into the candidate scan.
- buzz-relay lib suite: 870 passed, 9 failures pre-existing on the base
  commit (api::media/admin/telemetry, verified via stash).
- desktop: check/typecheck/test 4778 green; tauri tests 2415 green.
- New Playwright spec inbox-pagination-screenshots.spec.ts proves
  scroll-end pages older conversations in against a live isolated
  relay (before/after screenshots); the windowing spec's "after" test
  now step-scrolls the virtualized list and tolerates sibling-spec
  conversations on a shared relay.

Depth is bounded by FEED_WINDOW_SCAN_CAP (2000 events) — acceptable:
the cursor reaches conversations far beyond the first window, and the
cap keeps the scan predictable.

Co-authored-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
The cursor pagination `until` parameter pushed query_feed_mentions_routed
and query_feed_needs_action_routed to 8 args, tripping
clippy::too_many_arguments under -D warnings. Same allow already used on
the neighboring routed wrappers in this file.

Co-authored-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
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