Created
February 6, 2026 15:02
-
-
Save matthew-gerstman/d70fefe003d3db47e01c262d019ba0df to your computer and use it in GitHub Desktop.
OBV-1846: Message pagination implementation plan for threads with >200 messages
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
| # OBV-1846: Message Pagination — Remaining Work | |
| ## Problem | |
| Threads with >200 messages have older messages permanently inaccessible. | |
| Mirror hydration loads 200 messages per thread. There is no UI to load more. | |
| ## Architecture | |
| ### Current data flow | |
| 1. **Hydration** (`/hydrate/project/:scopeId`) loads 200 most recent messages per thread | |
| 2. **useThreadSync** loads 200 messages for threads granted mid-session | |
| 3. **useThreadMessages** reads from Mirror — no fallback to API | |
| 4. **ChatThread** has unused props: `onLoadMore`, `isLoadingMore`, `hasMoreMessages` (line 89-91) | |
| ### API already supports pagination | |
| - `chatApi.getMessages(threadId, { limit, skip, orderDirection })` — frontend client | |
| - `messageService.findMany(threadId, { limit, skip, orderBy })` — server service | |
| - `GET /threads/:threadId/messages?limit=N&skip=N&orderDirection=desc` — REST endpoint | |
| ## Implementation Plan | |
| ### 1. API: Return `hasMore` flag | |
| **File: `apps/api/src/routes/messages.ts`** — GET `/` handler | |
| Fetch `limit + 1` rows. If result exceeds limit, `hasMore = true`. | |
| Return `{ messages, hasMore }` instead of raw array. | |
| ### 2. Dashboard API client: Update return type | |
| **File: `dashboard/src/api/chat.api.ts`** — `getMessages` | |
| Add `getMessagesPaginated` method returning `{ messages: Message[], hasMore: boolean }`. | |
| Keep `getMessages` returning `Message[]` for backward compatibility. | |
| ### 3. New hook: `useLoadOlderMessages` | |
| **New file: `dashboard/src/features/chat/chat-with-events/hooks/use-load-older-messages.ts`** | |
| ```typescript | |
| interface UseLoadOlderMessagesArgs { | |
| threadId: string | null | undefined | |
| projectId: string | |
| mirror: MirrorInstance | |
| /** Number of messages currently in Mirror for this thread */ | |
| currentMessageCount: number | |
| /** The hydration/initial load limit (200) — if count >= this, assume more exist */ | |
| initialLoadLimit: number | |
| } | |
| interface UseLoadOlderMessagesResult { | |
| loadOlderMessages: () => Promise<void> | |
| isLoadingMore: boolean | |
| hasOlderMessages: boolean | |
| } | |
| ``` | |
| - Tracks `hasMore` state: initially true if `currentMessageCount >= initialLoadLimit` | |
| - `loadOlderMessages()`: calls `chatApi.getMessagesPaginated(threadId, { limit: 50, skip: currentCount, orderDirection: 'desc' })` | |
| - Upserts loaded messages into Mirror via `mirror.upsertResource()` | |
| - Updates `hasMore` from API response | |
| ### 4. Wire through component tree | |
| **Files to modify (prop threading):** | |
| - `dashboard/src/features/chat/chat-with-events.tsx` — call hook, pass props | |
| - `dashboard/src/features/chat/components/chat-content.tsx` — pass through | |
| - `dashboard/src/features/chat/chat-with-events/elements/chat-body.element.tsx` — pass through | |
| - `dashboard/src/features/chat/components/chat-thread/chat-thread.tsx` — wire existing props to element | |
| - `dashboard/src/features/chat/components/chat-thread/chat-thread.element.tsx` — render button | |
| ### 5. UI: "Load older messages" button | |
| **File: `dashboard/src/features/chat/components/chat-thread/chat-thread.element.tsx`** | |
| Add button at top of message list (inside `contentRef` div, before `ChatGroupedMessageParts`): | |
| - Text: "Load older messages" (or spinner when loading) | |
| - Preserve scroll position: record `scrollHeight` before load, restore offset after | |
| - Use existing `scrollContainerRef` for position management | |
| ### 6. Scroll position preservation | |
| When loading older messages at the top: | |
| ```typescript | |
| const container = scrollContainerRef.current | |
| const prevScrollHeight = container.scrollHeight | |
| await loadOlderMessages() | |
| // After messages render, adjust scroll to maintain visual position | |
| requestAnimationFrame(() => { | |
| const newScrollHeight = container.scrollHeight | |
| container.scrollTop += newScrollHeight - prevScrollHeight | |
| }) | |
| ``` | |
| ## Key files reference | |
| - `dashboard/src/features/chat/chat-with-events/hooks/use-thread-sync.ts` — initial message loading | |
| - `dashboard/src/hooks/use-thread-messages.ts` — message display from Mirror | |
| - `dashboard/src/api/chat.api.ts` — API client with existing pagination params | |
| - `apps/api/src/services/message.service.ts` — `findMany` with limit/skip/orderBy | |
| - `apps/api/src/routes/messages.ts` — REST endpoint | |
| - `apps/api/src/hydration/resolvers.ts` — hydration limit (currently 200) | |
| - `dashboard/src/features/chat/components/chat-thread/hooks.ts` — scroll management |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment