Initial commit: nick docs
This commit is contained in:
188
04 - Flows/Chat Flow.md
Normal file
188
04 - Flows/Chat Flow.md
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user