Development notes

Chat cursor pagination

The inbox used to download every conversation and sort them into tabs on the client. It now asks the server for one tab page. History dropped timestamps and started using a cursor. The first history page also returns send permissions, so the extra validation POST is gone.

t3code/explain-chat-api-pagination Jon Snow staging on :4202 6 Oct 2026

What changed

Three files. Query strings and body keys live in messages.constant.ts. The rest is message-container.

BeforeNow
One GET with search_key. The client split rows by featured, broadcast, and empty last message. One GET per open tab. tab=featured|general|broadcast for creators. Fans send tab=all. Always counts=true.
The left list had no next page. Scroll down sends the last cursor. Null cursor stops. A tab change or a new search starts page 1 again.
Tab pills were unread sums of whatever had already loaded. Pills read counts.featured and counts.general. The sidebar number is general + featured + broadcast.
History POST sent timeStamp. Scroll-up was commented out. First page is { groupId, include_validation: true }. Next page is { groupId, cursor }. Older messages prepend and the viewport stays put.
Permissions were a second POST to ChatUserValidation. First history page returns validation. Retry refetches that first page. Broadcast and followers still skip it.

Empty search is omitted, not sent as search_key=. Opening an unread row still zeros that row and subtracts its unread from the stored counts so the pills do not bounce back until the next inbox GET.

Requests I actually saw

Logged in as Jon Snow. Network log on localhost:4202/messages.

GET api/get-chatGroups-and-users?tab=featured&counts=true
GET api/get-chatGroups-and-users?tab=general&counts=true
GET api/get-chatGroups-and-users?tab=broadcast&counts=true
GET api/get-chatGroups-and-users?tab=featured&counts=true&search_key=Noel
GET api/get-chatGroups-and-users?tab=general&counts=true&search_key=Prateek

POST api/getpreviousMessagesFromHttp
{ "groupId": "CHATGROUP#1787300718906", "include_validation": true }

Every inbox and history reply on this account had cursor: null. That is why I could prove first pages and "stop", and not the append page.

Verified

CheckResult
Priority first page Verified. tab=featured&counts=true, no cursor. 1 row, Noel Huber.
General first page Verified. tab=general&counts=true, no cursor. 30 rows.
Message Lists first page Verified. tab=broadcast&counts=true, no cursor. 0 rows, empty state.
Inbox response shape Verified. data, counts, cursor on every inbox reply.
Counts and sidebar total Verified. { general: 0, featured: 0, broadcast: 0 }. Messages had no sidebar badge.
Tab change resets the page Verified. Each tab requested its own first page. No leftover cursor.
Search is a new first page Verified. search_key=Noel on featured, search_key=Prateek on general. Clearing search dropped search_key.
Inbox stops on a null cursor Verified. I scrolled the 30-row General list. No extra inbox GET.
History first page Verified. Body was groupId plus include_validation: true. Reply had response, cursor, validation.
History stops on a null cursor Verified. Noel Huber returned 8 messages and cursor: null. Scroll-up did not fire again.

Not verified

The next-page code is in the component. I never saw a non-null cursor on this account, so I did not watch an append request leave the browser.

CheckWhy it is still open
Inbox next page Not run. Need a tab whose first page returns a cursor. The next GET must keep the same tab and search and add that cursor. Rows append. No duplicate groupId.
History next page Not run. Need a long thread. Next POST should be { groupId, cursor } with no include_validation. Older bubbles appear above. You stay near the same message.
Non-zero tab pills Not run. All counts were 0, so I never saw a live Priority or General number.
Fan inbox tab=all Not run. I only logged in as the creator.
Broadcast history Not run. Message Lists was empty. That path should still send from and skip validation.
Unread decrement Not run. No unread rows, so I did not watch a pill drop when a thread opened.

Finish these on a fatter account

Same two URLs in DevTools. Disable cache. There should be no chat-user-validation when you open a normal DM.

  1. Find a tab with a cursor. Scroll the left list. Confirm the next GET adds that cursor and then stops when the server sends null.
  2. Open a long DM. Scroll to the top. Confirm the next POST is only groupId and cursor.
  3. Open an unread thread. The row badge, the tab pill, and the sidebar should drop by that unread count and stay down until the next inbox GET.
  4. Log in as a fan. No tab UI. First GET is tab=all&counts=true.
  5. Open a message list. History still sends from. No include_validation.
  6. Hit /messages?user=someone for a person who is not on page 1. It should still open them.
  7. Open thread A, then B quickly. B's messages and validation win. A late A page must not prepend into B.