# Gửi tin nhắn DM đầu tiên (First DM Message) > Tài liệu này hướng dẫn Frontend cách gửi tin nhắn DM khi **chưa có conversation** giữa 2 ngườidùng. --- ## Tóm tắt Trước đây, để gửi tin nhắn DM đầu tiên cho một ngườimới, FE phải gọi **2 API**: 1. `POST /dms` → tạo conversation 2. `POST /dms/:conversationId/messages` → gửi tin nhắn Giờ đây, bạn chỉ cần **1 API duy nhất** để gửi tin nhắn đầu tiên. Hệ thống sẽ tự động tạo conversation nếu chưa có. --- ## HTTP API ### Endpoint ``` POST /dms/messages ``` ### Request Headers ``` Authorization: Bearer Content-Type: application/json ``` ### Request Body ```typescript { // Chọn 1 trong 2: "recipientId": "uuid-of-user-b", // Dùng khi bắt đầu conversation mới "conversationId": "existing-uuid", // Dùng khi conversation đã tồn tại // Bắt buộc: "content": "Nội dung tin nhắn" // Tùy chọn: "replyToId": "uuid-of-replied-message" // Reply tin nhắn khác } ``` ### Scenarios #### Scenario A: Gửi tin nhắn đầu tiên (chưa có conversation) ```json { "recipientId": "550e8400-e29b-41d4-a716-446655440000", "content": "Chào bạn!" } ``` #### Scenario B: Gửi tin nhắn vào conversation đã có ```json { "conversationId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "content": "Bạn khỏe không?" } ``` ### Response #### Success 201 — Conversation mới được tạo ```json { "success": true, "data": { "conversationId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "isNewConversation": true, "conversation": { "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "user1_id": "user-a-uuid", "user2_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:00:00Z", "other_user": { "id": "550e8400-e29b-41d4-a716-446655440000", "username": "johndoe", "display_name": "John Doe", "avatar_url": "https://...", "status": "online", "last_seen": "2024-01-15T10:05:00Z" }, "last_message": null, "unread_count": 0, "is_new": true }, "message": { "id": "message-uuid", "conversation_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "sender_id": "user-a-uuid", "sender_username": "alice", "content": "Chào bạn!", "is_read": false, "created_at": "2024-01-15T10:10:00Z", "sender": { "id": "user-a-uuid", "username": "alice", "display_name": "Alice", "avatar_url": null, "status": "online" } } } } ``` #### Success 201 — Conversation đã tồn tại ```json { "success": true, "data": { "conversationId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "isNewConversation": false, "message": { "id": "message-uuid", "conversation_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "sender_id": "user-a-uuid", "sender_username": "alice", "content": "Bạn khỏe không?", "is_read": false, "created_at": "2024-01-15T10:15:00Z", "sender": { ... } } } } ``` > **Lưu ý:** Khi `isNewConversation: false`, field `conversation` **không có** trong response. ### Error Responses | Status | Code | Message | Nguyên nhân | |--------|------|---------|-------------| | 400 | `BAD_REQUEST` | Either recipientId or conversationId is required | Không cung cấp cả 2 field | | 404 | `NOT_FOUND` | User not found | `recipientId` không tồn tại | | 404 | `NOT_FOUND` | Conversation not found | `conversationId` không tồn tại hoặc user không thuộc conversation | | 403 | `FORBIDDEN` | You have blocked this user or they have blocked you | 2 user đã block nhau | | 400 | `BAD_REQUEST` | Cannot create conversation with yourself | Tự nhắn tin với chính mình | --- ## WebSocket (Real-time) ### Gửi tin nhắn qua WebSocket ```typescript socket.emit('sendDM', { recipientId: 'uuid-of-user-b', // Dùng cho conversation mới // conversationId: 'existing-uuid', // Dùng cho conversation đã có content: 'Chào bạn!', tempId: 'client-temp-id-123' // ID tạm của FE để match response }); ``` ### Listen events #### `dmSent` — ACK ngay lập tức ```typescript socket.on('dmSent', (data) => { // data = { success: true, tempId: 'client-temp-id-123' } // → Cập nhật UI: tin nhắn đang được xử lý }); ``` #### `conversationCreated` — Khi conversation mới được tạo (chỉ sender nhận) ```typescript socket.on('conversationCreated', (data) => { // data = { // conversation: { id, user1_id, user2_id, other_user, ... }, // tempId: 'client-temp-id-123' // } // → Thêm conversation mới vào danh sách }); ``` #### `newDM` — Tin nhắn mới (cả sender và receiver đều nhận) ```typescript socket.on('newDM', (data) => { // data = { // id: 'message-uuid', // conversationId: 'conv-uuid', // sender_id: 'user-a-uuid', // content: 'Chào bạn!', // tempId: 'client-temp-id-123', // timestamp: '2024-01-15T10:10:00Z', // ... // } // → Hiển thị tin nhắn trong UI }); ``` #### `error` — Khi có lỗi ```typescript socket.on('error', (error) => { // error = { message: '...', code: 'ERROR', tempId: '...' } // → Hiển thị lỗi, có thể retry }); ``` --- ## Flow gợi ý cho Frontend ### Flow A: Gửi tin nhắn đầu tiên qua HTTP ```typescript async function sendFirstMessage(recipientId: string, content: string) { const response = await fetch('/dms/messages', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ recipientId, content }), }); const { data } = await response.json(); if (data.isNewConversation) { // Thêm conversation mới vào sidebar/list addConversation(data.conversation); } // Hiển thị tin nhắn trong chat UI addMessage(data.message); // Join WebSocket room để nhận real-time updates socket.emit('joinDM', { conversationId: data.conversationId }); return data; } ``` ### Flow B: Gửi tin nhắn đầu tiên qua WebSocket ```typescript function sendFirstMessageWS(recipientId: string, content: string) { const tempId = generateTempId(); // Gửi qua WebSocket socket.emit('sendDM', { recipientId, content, tempId, }); // Lắng nghe các events socket.once('conversationCreated', (data) => { if (data.tempId === tempId) { addConversation(data.conversation); // Auto join room socket.emit('joinDM', { conversationId: data.conversation.id }); } }); socket.once('newDM', (data) => { if (data.tempId === tempId) { // Replace temp message với real message replaceTempMessage(tempId, data); } }); socket.once('error', (error) => { if (error.tempId === tempId) { showError(error.message); markMessageFailed(tempId); } }); } ``` ### Flow C: Gửi tin nhắn vào conversation đã có ```typescript // HTTP async function sendMessage(conversationId: string, content: string) { const response = await fetch('/dms/messages', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ conversationId, content }), }); const { data } = await response.json(); addMessage(data.message); } // HOẶC WebSocket function sendMessageWS(conversationId: string, content: string) { socket.emit('sendDM', { conversationId, content, tempId: generateTempId(), }); } ``` --- ## Backward Compatibility | Endpoint / Event | Trạng thái | |------------------|------------| | `POST /dms` | ✅ Giữ nguyên | | `POST /dms/:conversationId/messages` | ✅ Giữ nguyên | | `POST /dms/messages` | 🆕 Mới | | WS `sendDM` với `conversationId` | ✅ Vẫn hoạt động | | WS `sendDM` với `recipientId` | 🆕 Mới | > Các endpoint và events cũ vẫn hoạt động bình thường. FE có thể migrate dần sang endpoint mới. --- ## Checklist cho FE Developer - [ ] Cập nhật API call từ 2-step (`POST /dms` + `POST /dms/:id/messages`) thành 1-step (`POST /dms/messages`) - [ ] Handle `isNewConversation` flag để thêm conversation mới vào UI - [ ] Lắng nghe `conversationCreated` event qua WebSocket - [ ] Cập nhật `sendDM` payload để hỗ trợ `recipientId` - [ ] Test race condition: 2 ngườicùng gửi tin nhắn đầu tiên cho nhau --- ## Ví dụ đầy đủ: React Component ```tsx import { useState } from 'react'; import { socket } from './socket'; function ChatInput({ recipientId, conversationId }: { recipientId?: string; conversationId?: string; }) { const [content, setContent] = useState(''); const handleSend = async () => { if (!content.trim()) return; if (conversationId) { // Conversation đã có → dùng HTTP hoặc WS socket.emit('sendDM', { conversationId, content, tempId: Date.now() }); } else if (recipientId) { // Conversation mới → dùng endpoint mới const res = await fetch('/dms/messages', { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ recipientId, content }), }); const { data } = await res.json(); if (data.isNewConversation) { // Thêm conversation mới vào state dispatch({ type: 'ADD_CONVERSATION', payload: data.conversation }); // Join WS room socket.emit('joinDM', { conversationId: data.conversationId }); } dispatch({ type: 'ADD_MESSAGE', payload: data.message }); } setContent(''); }; return (
setContent(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSend()} placeholder="Nhập tin nhắn..." />
); } ``` --- ## Q&A **Q: Tôi có bắt buộc phải chuyển sang endpoint mới không?** A: Không. Endpoint cũ vẫn hoạt động. Chuyển sang khi FE ready. **Q: Nếu tôi gửi `recipientId` nhưng conversation đã tồn tại?** A: Hệ thống sẽ dùng conversation hiện có, không tạo mới. `isNewConversation` sẽ là `false`. **Q: Có thể gửi cả `recipientId` và `conversationId` không?** A: Không nên. Nếu gửi cả 2, `conversationId` sẽ được ưu tiên. **Q: WebSocket `sendDM` với `recipientId` có tự động join room không?** A: Không. FE vẫn phải tự `emit('joinDM', { conversationId })` sau khi nhận `conversationCreated`.