Spaces:
Sleeping
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:
POST /dms→ tạo conversationPOST /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 <access_token>
Content-Type: application/json
Request Body
{
// 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)
{
"recipientId": "550e8400-e29b-41d4-a716-446655440000",
"content": "Chào bạn!"
}
Scenario B: Gửi tin nhắn vào conversation đã có
{
"conversationId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"content": "Bạn khỏe không?"
}
Response
Success 201 — Conversation mới được tạo
{
"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
{
"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, fieldconversationkhô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
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
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)
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)
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
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
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
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ó
// 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
isNewConversationflag để thêm conversation mới vào UI - Lắng nghe
conversationCreatedevent qua WebSocket - Cập nhật
sendDMpayload để 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
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 (
<div>
<input
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
placeholder="Nhập tin nhắn..."
/>
<button onClick={handleSend}>Gửi</button>
</div>
);
}
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.