189 lines
10 KiB
Markdown
189 lines
10 KiB
Markdown
---
|
|
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 SHKeeper confirms payment, `shkeeperWebhook.ts:606-618` calls `chatService.createChat` to ensure 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; metadata.lastActivity=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)
|