refactor: cursor-based pagination, server-side bookmark filtering, and article-list cleanup - #248
Merged
Conversation
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.
…o cursor pagination
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
offsetwas derived from cacheddata.length. When items were marked read or bookmarks added/removed, the offset shifted, causing skipped or duplicated items across pages.(pub_date, id)cursor; bookmarks use(created_at, id)cursor. Newbeforequery param (opaque<timestamp>_<id>string). Response addsnext_cursor: string | null(null = no more pages).totalretained for display.paginatedListResponseandparseCursorshared helpers inhandler.go.order_by=created_at+beforeis rejected with 400 (cursor only supportspub_dateordering). Fever API untouched (has its ownsince_id/max_idpagination).getNextPageParamsimplified tolastPage.next_cursor ?? undefined(1 line, replacing all offset/count arithmetic and the unread-counting workaround).initialPageParam: null. pageParam typestring | null.openapi.yamlandfrontend-design.md§7 updated.Server-side bookmark filtering (#241)
feed_id/group_idfilters pushed to backend; pagination correct in every scope003_bookmark_feed_id.sqladdsfeed_idcolumn to bookmarksRefactors
useArticleListhook; article-list and article-drawer consume itstarredOverrideseliminated)useArticleList; skip items fetch in starred modeCleanup
opmlAPIand orphanedImportOpmlResponsetype (dead code; real import flow uses client-sideparseOPML+feedAPI.batchCreate)feed_id/group_idquery params onGET /bookmarksin OpenAPIBreaking API changes
offsetquery param removed fromGET /itemsandGET /bookmarks(replaced bybefore)next_cursorfield to items and bookmarks list endpointsorder_by=created_atcannot be combined withbefore(returns 400)Test plan
go build ./...passesgo test ./...passes (store + handler cursor pagination tests)npx tsc -b --noEmitpassesnpx eslintpassesnext_cursoris null on the last page (no extra empty request)