Chat / DM Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task.
Goal: Implement direct messaging and chat rooms with real-time-like polling for the Circle.so clone.
Architecture: Chat rooms identified by UUID. Messages use rich_text_body (TipTap JSON). DMs are chat rooms with kind: "direct" and 2 participants. Cursor-based pagination for messages. Polling for new messages (no WebSocket in backend spec).
Tech Stack: React 19, TanStack Router, TanStack React Query, TipTap (existing), shadcn/ui
File Structure
src/
├── lib/api/
│ └── chat.ts # Chat API hooks (queries + mutations)
├── components/chat/
│ ├── ChatList.tsx # Sidebar list of chat rooms
│ ├── ChatWindow.tsx # Main chat message view
│ ├── ChatMessage.tsx # Single message bubble
│ ├── ChatInput.tsx # Message input with TipTap
│ ├── ChatThread.tsx # Thread view (replies)
│ └── NewChatDialog.tsx # Start new DM/group chat
├── routes/
│ └── chat.tsx # Chat page (layout with list + window)Task 1: Chat API Hooks
Files:
Create:
src/lib/api/chat.ts[ ] Step 1: Create chat API layer
Create React Query hooks for:
useChatRooms()— GET /api/headless/v1/messages (list rooms)useChatMessages(uuid)— GET /api/headless/v1/messages/{uuid}/chat_room_messages (cursor pagination)useSendMessage(uuid)— POST /api/headless/v1/messages/{uuid}/chat_room_messagesuseCreateChatRoom()— POST /api/headless/v1/messages (create DM/group)useMarkChatRead(uuid)— POST /api/headless/v1/messages/{uuid}/mark_all_as_readuseUnreadChatRooms()— GET /api/headless/v1/messages/unread_chat_roomsuseChatThreads()— GET /api/headless/v1/chat_threadsuseChatThread(id)— GET /api/headless/v1/chat_threads/[ ] Step 2: Commit
Task 2: ChatMessage Component
Files:
Create:
src/components/chat/ChatMessage.tsx[ ] Step 1: Create message bubble component
Renders a single chat message with:
Sender avatar + name
Message body (render TipTap JSON as HTML using existing
jsonToHtml)Timestamp
Reactions display
Thread indicator (replies_count)
Hover actions (reply, react)
[ ] Step 2: Commit
Task 3: ChatInput Component
Files:
Create:
src/components/chat/ChatInput.tsx[ ] Step 1: Create message input component
Uses existing TipTapEditor in minimal mode. On submit:
Serializes content to rich_text_body format
Calls useSendMessage mutation
Clears editor on success
[ ] Step 2: Commit
Task 4: ChatWindow Component
Files:
Create:
src/components/chat/ChatWindow.tsx[ ] Step 1: Create main chat view
Fetches messages via useChatMessages(uuid)
Renders message list with auto-scroll to bottom
Cursor-based "load more" for older messages
Shows ChatInput at bottom
Empty state when no room selected
[ ] Step 2: Commit
Task 5: ChatList Component
Files:
Create:
src/components/chat/ChatList.tsx[ ] Step 1: Create chat room list sidebar
Fetches rooms via useChatRooms()
Shows room name, last message preview, timestamp
Unread badge per room
Click to select room
"New chat" button at top
[ ] Step 2: Commit
Task 6: NewChatDialog Component
Files:
Create:
src/components/chat/NewChatDialog.tsx[ ] Step 1: Create new chat dialog
Search members (reuse useSearchMembers from existing API)
Select one or more members
Create DM (2 members) or group chat (3+ members)
Navigate to the new chat room
[ ] Step 2: Commit
Task 7: Chat Page Route
Files:
Create:
src/routes/chat.tsx[ ] Step 1: Create chat page
Layout: sidebar (ChatList) + main area (ChatWindow)
Two-panel layout on desktop
Mobile: show list, tap to show messages
Selected room state via URL param or local state
[ ] Step 2: Commit
Task 8: Verify Build
- [ ] Step 1: Run typecheck
- [ ] Step 2: Run build
- [ ] Step 3: Fix any issues