--- title: Chat Flow tags: [flow, chat, socket-io, messaging] related_models: ["[[Chat]]", "[[Message]]", "[[User]]"] related_apis: ["POST /api/chat", "POST /api/chat/:chatId/messages", "GET /api/chat/:chatId/messages", "POST /api/chat/:chatId/read"] --- # Chat Flow Real-time messaging between buyer & seller (direct), three-way dispute mediation (group), and user-to-support (support). Backed by `Chat` documents with embedded `messages[]` and a Socket.IO room per chat for live updates. ## Actors - **User A (initiator)** — typically a buyer. - **User B (recipient)** — typically a seller. - **Support agent** — for `type: 'support'` chats (user is `support@amn.gg`). - **Admin** — added as a third participant in dispute chats. - **Frontend** — `frontend/src/sections/chat/` (chat list, conversation view, message composer). - **Backend** — `ChatService` (`backend/src/services/chat/ChatService.ts`), routes under `/api/chat`. - **MongoDB** — `chats` collection with embedded `messages`, `participants`, `unreadCounts`, `settings`, `metadata`. - **Socket.IO** — events `new-message`, `chat-notification`, `messages-read`, `user-typing`, `user-status-change`. ## Preconditions - Both parties authenticated. - For `direct` chats tied to a purchase request, the request exists and both users are participants in it. ## Chat lifecycle ```mermaid stateDiagram-v2 [*] --> Created: ChatService.createChat\n(or auto on first contact) Created --> Active: messages flowing Active --> Active: send / read / typing Active --> Archived: settings.isArchived=true Archived --> Active: unarchive Active --> [*]: chat deleted (rare) ``` ## Step-by-step narrative ### Creation 1. **Direct chat (find-or-create)** — when buyer clicks "Chat with seller" on a request detail, frontend POSTs `POST /api/chat` with `{ type: 'direct', participantIds: [buyer, seller], relatedTo: { type: 'PurchaseRequest', id } }`. 2. `ChatService.createChat` (`ChatService.ts:90-192`): - For `direct` with exactly 2 participants, runs `Chat.findOne({ type: 'direct', 'participants.userId': { $all: [...] }, 'participants.isActive': true })` and returns the existing chat if found. - Otherwise creates a new `Chat` with `participants` (each with `role:'member'`, `joinedAt`, `isActive:true`), zeroed `unreadCounts`, default `settings`, `metadata.createdBy`. - Appends a system welcome message (`messageType: 'system'`). - If `relatedTo.type === 'PurchaseRequest'`, also writes `"چت برای درخواست خرید \"{title}\" ایجاد شد"` system line. - Re-loads with `populate('participants.userId', 'firstName lastName profile.avatar email')` for the response. 3. **Group chat (dispute)** — same pattern, but `type: 'group'`, all three participants (buyer, seller, admin) added (admin is added later by `DisputeService.assignAdmin`). 4. **Support chat** — `ChatService.createSupportChat(userId)` (`:41-88`) auto-discovers `User.findOne({ email: 'support@amn.gg' })` and creates a `type: 'support'` chat with a welcome message. Idempotent. 5. **Post-payment auto-chat** — when payment is confirmed, the payment-state cascade ensures a direct chat exists between buyer and winning seller. ### Joining the room (real-time) 6. On chat page mount, the frontend emits `socket.emit('join-chat-room', chatId)` (`backend/src/app.ts:130-133`). The socket joins room `chat-{chatId}`. 7. Optionally `socket.emit('user-online', userId)` so other clients see green status (`app.ts:161-169`). ### Sending a message 8. User types and hits send. Frontend POSTs `POST /api/chat/:chatId/messages` with `{ content, messageType?, fileUrl?, fileName?, fileSize?, replyTo? }`. 9. `ChatService.sendMessage` (`:195-260`): - Loads chat, verifies the sender is in `participants[]` and `isActive`. - Builds `Message`: `{ senderId, content, messageType: 'text', fileUrl?, fileName?, fileSize?, replyTo?, timestamp, isRead: false, isEdited: false }`. - `chat.addMessage(messageData)` — schema method that pushes the message, updates `metadata.lastActivity`, increments `unreadCounts` for non-senders, and updates the cached `lastMessage` summary. - Persists. - Emits **`new-message`** to `chat-{chatId}` (everyone in the room sees the message immediately). - Emits **`chat-notification`** to each non-sender's `user-{userId}` room (drives the chat-list unread badge and the toast/notification bell if the user is not currently viewing the chat). 10. Frontend reconciles its own message list (the sender either appends optimistically and then matches the server echo or waits for the round-trip). ### Attachments 11. To attach a file, the user picks a file → frontend calls `chatService.uploadChatFile(chatId, file)` (or the equivalent `POST /api/chat/:chatId/upload`) — backend persists the upload via `fileService` (returns `{ fileUrl, fileName, fileSize }`). 12. The send-message call then includes `messageType: 'image' | 'file'` and the file metadata. Files are served from `/uploads`. ### Read receipts 13. When the user opens a chat, frontend POSTs `POST /api/chat/:chatId/read` (optionally with `messageIds: string[]`). 14. `ChatService.markMessagesAsRead` (`:438-483`): - Calls `chat.markAsRead(userId, messageObjectIds)` (schema method that flips `isRead` on the relevant messages and zeros the user's `unreadCounts` entry). - Emits **`messages-read`** to `chat-{chatId}` so the sender sees the double-tick. ### Typing indicator 15. On `input` events, frontend emits `socket.emit('typing-start', { chatId, userId, userName })`; on idle/blur emits `typing-stop`. 16. Backend `app.ts:142-158` relays to `chat-{chatId}` as `user-typing` (excluding the sender). No DB persistence. ## Sequence diagram ```mermaid sequenceDiagram autonumber actor A as User A actor B as User B participant FE_A as Frontend A participant FE_B as Frontend B participant BE as Backend participant DB as MongoDB participant IO as Socket.IO A->>FE_A: Open conversation FE_A->>BE: POST /api/chat {type:direct, participantIds, relatedTo} BE->>DB: find-or-create Chat BE-->>FE_A: { chat } FE_A->>IO: emit 'join-chat-room' chatId FE_B->>IO: emit 'join-chat-room' chatId (when B opens too) A->>FE_A: type & send FE_A->>BE: POST /api/chat/{id}/messages {content} BE->>DB: chat.addMessage and update metadata.lastActivity to now BE->>IO: emit chat-{id} 'new-message' IO-->>FE_A: 'new-message' (echo) IO-->>FE_B: 'new-message' (live) BE->>IO: emit user-{B} 'chat-notification' (badge) B->>FE_B: opens chat FE_B->>BE: POST /api/chat/{id}/read BE->>DB: chat.markAsRead(B) BE->>IO: emit chat-{id} 'messages-read' IO-->>FE_A: 'messages-read' (double-tick) A->>IO: emit 'typing-start' IO-->>FE_B: 'user-typing' {isTyping:true} ``` ## API calls | Method | Endpoint | Purpose | |---|---|---| | `POST` | `/api/chat` | Find-or-create chat | | `GET` | `/api/chat` | List user's chats | | `GET` | `/api/chat/:chatId/messages` | Paginated message history | | `POST` | `/api/chat/:chatId/messages` | Send message | | `POST` | `/api/chat/:chatId/upload` | Upload attachment | | `POST` | `/api/chat/:chatId/read` | Mark read | | `POST` | `/api/chat/support` | Create/get support chat | ## Database writes - **`chats`**: insert on create; `$push` into `messages[]`; `$set` `metadata.lastActivity`; `unreadCounts.$.count` increment per recipient; `settings.isArchived` toggled; `participants.$.isActive` flipped on leave. ## Socket events emitted - **`new-message`** → `chat-{chatId}` (every message). - **`chat-notification`** → `user-{recipientId}` for non-senders (badge). - **`messages-read`** → `chat-{chatId}` after read mark. - **`user-typing`** → `chat-{chatId}` (relayed by `app.ts`). - **`user-status-change`** → broadcast when `user-online` is emitted. - **`new-message`** (system) for system welcome lines on chat creation. ## Side effects - **`metadata.lastActivity`** drives the chat-list sort order. - **`lastMessage`** cache lets the chat-list render previews without loading the entire `messages[]` array. - **`unreadCounts`** is the source-of-truth for badge counts; resetting on read also drives global unread totals. - **Embedded messages array** can grow large; consider migrating to a separate `messages` collection if conversations exceed several thousand messages. ## Error / edge cases - **Sender not a participant** → `403 "User is not a participant in this chat"` (`:209-211`). - **Chat not found** → `404` on `getChatMessages`. - **Direct duplicate** → idempotent — `createChat` returns existing chat. - **Empty content** — currently allowed (system messages are typically non-empty though); add a min-length validator if needed. - **Files served from `/uploads`** — anonymous access is allowed. Sensitive attachments (KYC docs, dispute evidence) should be protected by signed URLs or per-user authorisation; currently any user with the URL can fetch. - **Long conversations** — `getChatMessages` slices an in-memory copy of the messages array. For >10k messages this is inefficient. Use aggregation `$slice` or a separate collection. - **Race on `markAsRead`** — two parallel reads may double-zero the counter, which is harmless. - **Typing indicator spam** — clients should debounce `typing-start` and emit `typing-stop` on a 2s idle. > [!warning] Notification message uses placeholder sender name > `ChatService.sendMessage` posts `chat-notification` with `senderName: "کاربر"` (`:248`) — the literal Persian word for "user". Resolve `senderName` from `participant.userId.firstName` for a better UX. ## Linked flows - [[Notification Flow]] — `chat-notification` is one of the inputs. - [[Negotiation Flow]] — uses chats heavily. - [[Dispute Flow]] — three-way group chat. - [[Authentication Flow]] — supplies the `user-${userId}` rooms via `join-user-room`. ## Source files - Backend: `backend/src/services/chat/ChatService.ts` - Backend: `backend/src/services/chat/chatController.ts` - Backend: `backend/src/services/chat/routes.ts` (under `/api/chat`) - Backend: `backend/src/models/Chat.ts` - Backend: `backend/src/app.ts:130-179` (Socket.IO chat handlers) - Frontend: `frontend/src/sections/chat/` - Frontend: `frontend/src/contexts/socket-context.tsx` (or equivalent socket provider)