Skip to content

refactor: cursor-based pagination, server-side bookmark filtering, and article-list cleanup - #248

Merged
0x2E merged 21 commits into
mainfrom
fix-pagination
Jul 12, 2026
Merged

refactor: cursor-based pagination, server-side bookmark filtering, and article-list cleanup#248
0x2E merged 21 commits into
mainfrom
fix-pagination

Conversation

@0x2E

@0x2E 0x2E commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Summary

Migrates items and bookmarks list endpoints from offset-based to cursor-based pagination (full-stack), pushes starred/bookmarks filtering to the backend, extracts a shared article-list hook, and cleans up dead code and doc gaps.

Changes

Cursor-based pagination (full-stack migration)

Eliminates the structural fragility where offset was derived from cached data.length. When items were marked read or bookmarks added/removed, the offset shifted, causing skipped or duplicated items across pages.

  • Backend: Items use (pub_date, id) cursor; bookmarks use (created_at, id) cursor. New before query param (opaque <timestamp>_<id> string). Response adds next_cursor: string | null (null = no more pages). total retained for display. paginatedListResponse and parseCursor shared helpers in handler.go. order_by=created_at + before is rejected with 400 (cursor only supports pub_date ordering). Fever API untouched (has its own since_id/max_id pagination).
  • Frontend: getNextPageParam simplified to lastPage.next_cursor ?? undefined (1 line, replacing all offset/count arithmetic and the unread-counting workaround). initialPageParam: null. pageParam type string | null.
  • Docs: openapi.yaml and frontend-design.md §7 updated.

Server-side bookmark filtering (#241)

  • feed_id / group_id filters pushed to backend; pagination correct in every scope
  • Migration 003_bookmark_feed_id.sql adds feed_id column to bookmarks
  • Starred mode read state reactivity and mark-all-as-read correctness
  • Bookmark delete removes item from cache

Refactors

  • Extract shared useArticleList hook; article-list and article-drawer consume it
  • Remove article-session store (starredOverrides eliminated)
  • Simplify useArticleList; skip items fetch in starred mode

Cleanup

  • Remove unused opmlAPI and orphaned ImportOpmlResponse type (dead code; real import flow uses client-side parseOPML + feedAPI.batchCreate)
  • Document feed_id / group_id query params on GET /bookmarks in OpenAPI

Breaking API changes

  • offset query param removed from GET /items and GET /bookmarks (replaced by before)
  • Response adds next_cursor field to items and bookmarks list endpoints
  • order_by=created_at cannot be combined with before (returns 400)

Test plan

  • go build ./... passes
  • go test ./... passes (store + handler cursor pagination tests)
  • npx tsc -b --noEmit passes
  • npx eslint passes
  • Items list: load page, mark items read, load more — verify no skip/duplicate
  • Bookmarks list: paginate within a feed and a group; verify filtering is server-side
  • next_cursor is null on the last page (no extra empty request)
  • Bookmark create/delete updates star indicators reactively
  • OPML import still works (unchanged client-side flow)

0x2E added 20 commits June 3, 2026 13:10
The starred view fetched only 100 bookmarks and filtered by feed/group
client-side via feed_name, so users with more bookmarks could not see
them and feed renames broke the association.

- add bookmarks.feed_id (nullable FK, ON DELETE SET NULL) migrated and
  backfilled from item_id; bookmarks stay independent of feeds
- ListBookmarks/CountBookmarks filter by feed_id/group_id server-side;
  ListBookmarks joins items to return per-bookmark unread
- frontend: starred list becomes an infinite, server-filtered query; the
  unfiltered lookup exposes the true total for the sidebar count
- mark-read now mirrors unread into bookmark caches optimistically,
  removing the per-article detail fetches in starred mode
Marking items read in the unread-filtered view only flipped the unread
flag but left the items in cache data and never adjusted page total.
The count-based getNextPageParam thus kept a stale data.length that
diverged from the server's shrinking unread total, making the load-more
button disappear after ~60 marked-read items while unread items still
existed. Remove now-mismatched items and decrement total on
unread-filtered caches, mirroring useDeleteBookmark.
…ation

Reverts the previous removal approach (4afae19): dropping read items from
the unread-filtered cache emptied the loaded list and tripped the
"no articles" empty state, since the load-more button only renders when
the list is non-empty.

Keep the optimistic flag flip for instant feedback, but invalidate the
items query in onSettled so the server re-returns the correct unread set
with a fresh total and offset. This keeps the list populated during the
background refetch and makes count-based getNextPageParam consistent
with the server. Mirrors useDeleteBookmark's optimistic + invalidate flow.
Reverts the invalidation approach (2298d8b): refetching removed the
just-marked-read articles from the list, but those are meant to linger
(greyed) so the user can undo.

Keep the optimistic flag flip (items stay visible) and instead change
getNextPageParam for the unread-filtered view to count only unread
items in cache, not total data.length. Since marked-read items no
longer count toward the offset or the hasMore comparison, pagination
stays consistent with the server's shrinking unread set while the read
items remain on screen for undo. The all-items view keeps using
data.length, unchanged.
@0x2E 0x2E changed the title fix: paginate starred bookmarks with server-side filtering and cleanup refactor: cursor-based pagination, server-side bookmark filtering, and article-list cleanup Jul 12, 2026
@0x2E
0x2E merged commit 5b18783 into main Jul 12, 2026
2 checks passed
@0x2E
0x2E deleted the fix-pagination branch July 12, 2026 12:08
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