Skip to content

feat(inbox): page through an inbox larger than one screenful - #292

Merged
imshashank merged 5 commits into
mainfrom
feat/inbox-pagination
Aug 11, 2026
Merged

feat(inbox): page through an inbox larger than one screenful#292
imshashank merged 5 commits into
mainfrom
feat/inbox-pagination

Conversation

@imshashank

@imshashank imshashank commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The inbox asked for fifty notifications and threw away the cursor the
server handed back. Past fifty, the rest of the inbox was unreachable,
and nothing on the screen admitted it existed.

listInbox already paged, so most of this is wiring. A GET on
/api/notifications returns a page and its cursor, the server
component passes the first cursor down with the first page, and the
list offers the older ones at its end. The offer loads itself when it
scrolls into view, so reading down the list keeps going, and it stays
a real button so a reader who never scrolls it into view, or who is on
a keyboard, can still ask for more. It retires when the server stops
handing back a cursor.

The one thing that was not wiring

The cursor lives in a ref, not only in state. Two things can ask to
load at the same moment, the observer and a click, and a request that
started against an older cursor would otherwise resolve last and write
that older cursor back, so the offer never retired and the same page
was fetched again. Reading the cursor at call time means the second
caller sees what the first already recorded. There is an in flight
guard too, but on its own it is not enough: it clears before React has
re-rendered, so the next caller would still hold the stale cursor.

Verified

Against a real inbox of 126 notifications: 50, then 100, then 126,
the offer retires on the last page, and no notification appears twice.
Eight unit tests cover the offer appearing and retiring, the cursor
actually asked for, appending under what is already read, paging more
than once, dropping a duplicate that a shifting page can return, and
saying so without losing the offer when a page fails.

Elsewhere

Issue lists and the Slack channel picker already page. Three surfaces
still truncate silently, each returning a cursor the client drops:
issue comments and doc comments both stop at fifty, and issue activity
returns an activityCursor nothing consumes. They are the same shape
of gap as this one, and each wants its own change since loading older
comments reads from the top of a thread rather than the bottom.

Greptile Summary

The PR adds cursor-based inbox pagination while keeping pagination failures separate from read-state failures.

  • Adds an authenticated notifications endpoint that returns bounded pages and continuation cursors.
  • Passes the initial cursor through the server and realtime components into the inbox view.
  • Appends deduplicated pages through an observable, keyboard-accessible load-more button.
  • Adds endpoint and component coverage for cursor progression, retries, deduplication, and independent error lifecycles.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the successful-retry path clears only the pagination complaint, while successful pagination leaves an unrelated failed read-state message intact.

Important Files Changed

Filename Overview
apps/web/src/features/inbox/inbox-view.tsx Adds cursor tracking, guarded page loading, deduplicated appends, and a pagination-specific error whose successful retry now clears it without affecting read-state errors.
apps/web/src/app/api/notifications/route.ts Adds an authenticated, page-size-bounded endpoint that maps notification records into the inbox response shape.
apps/web/src/features/inbox/data.ts Exposes the initial continuation cursor and centralizes conversion from notification records to inbox items.
apps/web/tests/features/inbox/inbox-pagination.test.tsx Covers cursor progression, retries, duplicate suppression, filtered empty states, and separation between pagination and read-state errors.
apps/web/tests/app/api/notifications/route.test.ts Covers endpoint pagination, page-size bounds, response conversion, cursor continuation, and authentication.

Sequence Diagram

sequenceDiagram
  participant Page as InboxPage
  participant View as InboxView
  participant API as GET /api/notifications
  participant Service as listInbox
  Page->>Service: Request initial page
  Service-->>Page: Items and nextCursor
  Page->>View: Initial items and cursor
  View->>API: Request page with cursor
  API->>Service: Request bounded next page
  Service-->>API: Older items and nextCursor
  API-->>View: Serialized page
  View->>View: Deduplicate and append items
  alt More pages remain
    View->>View: Retain load-more offer
  else Last page
    View->>View: Retire load-more offer
  end
Loading

Reviews (5): Last reviewed commit: "fix(inbox): keep older pages reachable w..." | Re-trigger Greptile

The inbox asked the server for fifty notifications and dropped the
cursor it handed back, so the fifty first oldest were simply the end
of the inbox as far as the reader was concerned. Nothing said there
was more, and nothing could reach it.

listInbox already paged, so this is mostly wiring: a GET on
/api/notifications hands back a page and its cursor, the server
component passes the first cursor down, and the list offers the older
ones at its end. The offer loads itself when it scrolls into view, so
reading down the list keeps going, and it stays a button so a reader
who never scrolls it into view, or who is on a keyboard, can still ask.

The cursor lives in a ref rather than only in state. Two things can
ask to load at once, the observer and the click, and a request that
started against an older cursor would otherwise finish last and put
that older cursor back, leaving the offer up forever. Reading the
cursor at call time means the second caller sees what the first one
already recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@imshashank
imshashank requested a review from pulkitxm as a code owner August 10, 2026 19:59
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orbit Ready Ready Preview Aug 11, 2026 4:31am

Request Review

@github-actions github-actions Bot added tests Test coverage and test infrastructure area: web The Next.js app and its UI labels Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The inbox now supports cursor-based notification pagination. The API returns mapped items and cursors. InboxView loads additional pages through a button or IntersectionObserver, handles errors, removes duplicates, and preserves read-save errors.

Changes

Inbox pagination

Layer / File(s) Summary
Pagination contracts and API
apps/web/src/features/inbox/data.ts, apps/web/src/app/api/notifications/route.ts
The inbox data model now includes nextCursor. The notifications endpoint validates pagination parameters, retrieves the principal’s inbox, and returns mapped items with the next cursor.
Inbox pagination flow
apps/web/src/app/(app)/inbox/page.tsx, apps/web/src/features/inbox/inbox-realtime.tsx, apps/web/src/features/inbox/inbox-view.tsx
The cursor flows into InboxView, which validates responses, fetches pages, appends unique items, manages loading and paging errors, and renders automatic and manual load controls.
Pagination validation and fixtures
apps/web/tests/features/inbox/inbox-pagination.test.tsx, apps/web/tests/components/keyboard-hints.test.tsx, apps/web/tests/features/inbox/inbox-{issue,open}.test.tsx
Tests cover cursor propagation, multi-page loading, duplicate suppression, completion, retry behavior, and separation of paging and read-save errors. Existing fixtures provide the new prop.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InboxPage
  participant InboxRealtime
  participant InboxView
  participant NotificationsAPI
  InboxPage->>InboxRealtime: pass nextCursor
  InboxRealtime->>InboxView: forward nextCursor
  InboxView->>NotificationsAPI: request next page
  NotificationsAPI-->>InboxView: return items and nextCursor
  InboxView->>InboxView: append unique items and render state
Loading

Possibly related PRs

  • Noveum/orbit#33: Modifies notification catch-up and inbox synchronization behavior.
  • Noveum/orbit#90: Modifies inbox notification handling and selection behavior.
  • Noveum/orbit#106: Modifies inbox read-state behavior in inbox-view.tsx.

Suggested reviewers: pulkitxm

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes adding pagination for inboxes larger than one screenful.
Description check ✅ Passed The description accurately explains the inbox pagination endpoint, cursor flow, loading behavior, error handling, and tests.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/inbox-pagination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread apps/web/src/features/inbox/inbox-view.tsx
A failed page left its message on screen even after the next attempt
succeeded, so the inbox went on saying it could not reach the older
notifications while showing them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread apps/web/src/features/inbox/inbox-view.tsx Outdated
Both complaints shared one slot, so clearing the paging message on a
successful page also wiped a read state save that had failed and
rolled back, and the reader lost the only sign their change had not
stuck.

Paging keeps its own message now, and it sits under the offer at the
end of the list rather than in the header, which is where the reader
was looking when they asked for more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/web/src/features/inbox/inbox-view.tsx (1)

222-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a shared interaction helper.

Line 222 adds raw transition-colors and hover:text-muted classes. Replace them with the applicable shared interaction helper so hover and focus motion uses the required tokens and reduced-motion behavior.

As per coding guidelines, "hover/focus state color transitions must be implemented only via the shared motion/color helpers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/inbox/inbox-view.tsx` at line 222, Replace the raw
transition-colors and hover:text-muted classes on the inbox view element with
the applicable shared motion/color interaction helper, preserving the existing
muted hover appearance while inheriting the required hover/focus tokens and
reduced-motion behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/api/notifications/route.ts`:
- Around line 7-18: Add direct tests for the GET route that invoke the exported
GET handler rather than mocking fetch. Cover cursor parsing and forwarding,
enforcement of the INBOX_PAGE_SIZE limit, and mapping the listInbox result into
notifications with the returned nextCursor.

In `@apps/web/src/features/inbox/inbox-view.tsx`:
- Around line 644-657: Update the empty filtered-state branch around visible to
retain the LoadMoreRow when cursor is non-null, rather than bypassing pagination
with EmptyState alone. Reuse the existing loadMore, loadingMore, pagingError,
and inbox-paging-error behavior, and add a regression test covering an empty
first filtered page that can load older matching rows.
- Around line 390-417: Replace the component-owned pagination flow in loadMore
with a TanStack Query infinite query for notifications, using query pages and
page parameters as the source of truth instead of rows and cursorRef. Update the
rendered list from the infinite-query data, preserve deduplication and
loading/error states, and integrate realtime updates by patching or invalidating
the query cache without triggering a full refetch of the viewed list.

---

Nitpick comments:
In `@apps/web/src/features/inbox/inbox-view.tsx`:
- Line 222: Replace the raw transition-colors and hover:text-muted classes on
the inbox view element with the applicable shared motion/color interaction
helper, preserving the existing muted hover appearance while inheriting the
required hover/focus tokens and reduced-motion behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d15b523e-b9c8-40b9-8409-14ca2c2265d7

📥 Commits

Reviewing files that changed from the base of the PR and between 64f7972 and 15e4b49.

📒 Files selected for processing (9)
  • apps/web/src/app/(app)/inbox/page.tsx
  • apps/web/src/app/api/notifications/route.ts
  • apps/web/src/features/inbox/data.ts
  • apps/web/src/features/inbox/inbox-realtime.tsx
  • apps/web/src/features/inbox/inbox-view.tsx
  • apps/web/tests/components/keyboard-hints.test.tsx
  • apps/web/tests/features/inbox/inbox-issue.test.tsx
  • apps/web/tests/features/inbox/inbox-open.test.tsx
  • apps/web/tests/features/inbox/inbox-pagination.test.tsx

Comment thread apps/web/src/app/api/notifications/route.ts
Comment thread apps/web/src/features/inbox/inbox-view.tsx
Comment thread apps/web/src/features/inbox/inbox-view.tsx
A tab whose matches all sat in pages the reader had not fetched yet
showed inbox zero and no way forward: the empty state replaced the
list, and the offer to load older notifications went with it. Unread
was the easy way to hit it, fifty read notifications deep.

Inbox zero now means the inbox really is empty, nothing loaded and
nothing left to fetch. While the server still has pages, the list
stands with the offer in it even when the filter matches none of what
is loaded.

The route gets its own tests as well: the tests that came with it
mocked fetch, so nothing exercised the cursor, the page size cap or
the shape the inbox reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@imshashank
imshashank merged commit fe483e7 into main Aug 11, 2026
13 checks passed
@imshashank
imshashank deleted the feat/inbox-pagination branch August 11, 2026 04:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: web The Next.js app and its UI tests Test coverage and test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant