Spaces:
Sleeping
Sleeping
Commit ·
1835767
1
Parent(s): 76b92d9
Sửa để gửi được ảnh và pdf
Browse files- src/App.jsx +2 -1
- src/components/ChatArea.jsx +54 -3
- src/services/message.service.js +10 -0
src/App.jsx
CHANGED
|
@@ -250,7 +250,7 @@ function App() {
|
|
| 250 |
const id = data.id;
|
| 251 |
const tempId = data.tempId;
|
| 252 |
|
| 253 |
-
if (!roomId || !content) return;
|
| 254 |
|
| 255 |
console.log("[App] newMessage parsed:", { roomId, id, tempId, author, senderId });
|
| 256 |
|
|
@@ -276,6 +276,7 @@ function App() {
|
|
| 276 |
isPinned: data.is_pinned || false,
|
| 277 |
isOwn: isOwnMessage,
|
| 278 |
tempId,
|
|
|
|
| 279 |
},
|
| 280 |
}),
|
| 281 |
);
|
|
|
|
| 250 |
const id = data.id;
|
| 251 |
const tempId = data.tempId;
|
| 252 |
|
| 253 |
+
if (!roomId || (!content && !data.attachments?.length)) return;
|
| 254 |
|
| 255 |
console.log("[App] newMessage parsed:", { roomId, id, tempId, author, senderId });
|
| 256 |
|
|
|
|
| 276 |
isPinned: data.is_pinned || false,
|
| 277 |
isOwn: isOwnMessage,
|
| 278 |
tempId,
|
| 279 |
+
attachments: data.attachments || [],
|
| 280 |
},
|
| 281 |
}),
|
| 282 |
);
|
src/components/ChatArea.jsx
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
| 28 |
clearRoomUnreadCount,
|
| 29 |
} from "../store/slices/spaceSlice";
|
| 30 |
import socketService from "../services/socket.service";
|
|
|
|
| 31 |
|
| 32 |
function ChatArea({
|
| 33 |
activeView,
|
|
@@ -507,6 +508,18 @@ function ChatArea({
|
|
| 507 |
});
|
| 508 |
})();
|
| 509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
return {
|
| 511 |
id: msg.id,
|
| 512 |
sender: senderName,
|
|
@@ -522,6 +535,8 @@ function ChatArea({
|
|
| 522 |
pending: msg.pending || false,
|
| 523 |
is_read: msg.is_read,
|
| 524 |
created_at: msg.created_at,
|
|
|
|
|
|
|
| 525 |
};
|
| 526 |
});
|
| 527 |
|
|
@@ -942,6 +957,41 @@ function ChatArea({
|
|
| 942 |
const msgTempId = `temp-${Date.now()}`;
|
| 943 |
const contentTrimmed = content.trim();
|
| 944 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
// Optimistic UI - same format as DM
|
| 946 |
const optimisticMsg = {
|
| 947 |
id: msgTempId,
|
|
@@ -955,6 +1005,7 @@ function ChatArea({
|
|
| 955 |
created_at: new Date().toISOString(),
|
| 956 |
is_read: false,
|
| 957 |
pending: true,
|
|
|
|
| 958 |
};
|
| 959 |
|
| 960 |
// Track sending message
|
|
@@ -980,9 +1031,9 @@ function ChatArea({
|
|
| 980 |
}, 10000);
|
| 981 |
|
| 982 |
dispatch(addMessage({ roomId: room, message: optimisticMsg }));
|
| 983 |
-
|
| 984 |
-
// Send with tempId so BE can echo it back
|
| 985 |
-
socketService.sendMessage({ roomId: room, content: contentTrimmed, tempId: msgTempId });
|
| 986 |
|
| 987 |
if (files?.length > 0 && /@StudyBot/i.test(contentTrimmed)) {
|
| 988 |
callStudyBotWithFile(contentTrimmed, files[0], null);
|
|
|
|
| 28 |
clearRoomUnreadCount,
|
| 29 |
} from "../store/slices/spaceSlice";
|
| 30 |
import socketService from "../services/socket.service";
|
| 31 |
+
import { messageService } from "../services/message.service";
|
| 32 |
|
| 33 |
function ChatArea({
|
| 34 |
activeView,
|
|
|
|
| 508 |
});
|
| 509 |
})();
|
| 510 |
|
| 511 |
+
const rawAttachments = msg.attachments || [];
|
| 512 |
+
const normalizedAttachments = rawAttachments.map((att) => ({
|
| 513 |
+
name: att.file_name || att.name || "file",
|
| 514 |
+
type: (att.file_type || att.type || "").includes("image")
|
| 515 |
+
? "image"
|
| 516 |
+
: (att.file_name || att.name || "").toLowerCase().endsWith(".pdf") ||
|
| 517 |
+
(att.file_type || att.type || "").includes("pdf")
|
| 518 |
+
? "pdf"
|
| 519 |
+
: "other",
|
| 520 |
+
url: att.file_url || att.url || att.previewUrl || "",
|
| 521 |
+
}));
|
| 522 |
+
|
| 523 |
return {
|
| 524 |
id: msg.id,
|
| 525 |
sender: senderName,
|
|
|
|
| 535 |
pending: msg.pending || false,
|
| 536 |
is_read: msg.is_read,
|
| 537 |
created_at: msg.created_at,
|
| 538 |
+
hasAttachment: normalizedAttachments.length > 0,
|
| 539 |
+
attachments: normalizedAttachments,
|
| 540 |
};
|
| 541 |
});
|
| 542 |
|
|
|
|
| 957 |
const msgTempId = `temp-${Date.now()}`;
|
| 958 |
const contentTrimmed = content.trim();
|
| 959 |
|
| 960 |
+
// Upload file first if present
|
| 961 |
+
let attachment = null;
|
| 962 |
+
let optimisticAttachments = [];
|
| 963 |
+
if (files?.length > 0) {
|
| 964 |
+
const fileObj = files[0];
|
| 965 |
+
// Show preview immediately in optimistic message
|
| 966 |
+
if (fileObj.preview || fileObj.file) {
|
| 967 |
+
const previewUrl = fileObj.preview || URL.createObjectURL(fileObj.file);
|
| 968 |
+
const isPdf =
|
| 969 |
+
fileObj.file?.type === "application/pdf" ||
|
| 970 |
+
(fileObj.name || "").toLowerCase().endsWith(".pdf");
|
| 971 |
+
optimisticAttachments = [{
|
| 972 |
+
file_name: fileObj.name || fileObj.file?.name || "file",
|
| 973 |
+
file_type: fileObj.file?.type || "",
|
| 974 |
+
file_url: previewUrl,
|
| 975 |
+
previewUrl,
|
| 976 |
+
}];
|
| 977 |
+
}
|
| 978 |
+
try {
|
| 979 |
+
const uploadRes = await messageService.uploadFileToStorage(fileObj.file, { roomId: room });
|
| 980 |
+
const fileData = uploadRes.data?.data || uploadRes.data;
|
| 981 |
+
attachment = {
|
| 982 |
+
file_url: fileData.fileUrl || fileData.file_url,
|
| 983 |
+
file_name: fileData.fileName || fileData.file_name || fileObj.name,
|
| 984 |
+
file_type: fileData.fileType || fileData.file_type,
|
| 985 |
+
file_size: fileData.fileSize || fileData.file_size,
|
| 986 |
+
mime_type: fileData.mimeType || fileData.mime_type,
|
| 987 |
+
};
|
| 988 |
+
// Update optimistic preview with real URL
|
| 989 |
+
optimisticAttachments = [{ ...attachment }];
|
| 990 |
+
} catch (err) {
|
| 991 |
+
console.error("[ChatArea] File upload failed:", err);
|
| 992 |
+
}
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
// Optimistic UI - same format as DM
|
| 996 |
const optimisticMsg = {
|
| 997 |
id: msgTempId,
|
|
|
|
| 1005 |
created_at: new Date().toISOString(),
|
| 1006 |
is_read: false,
|
| 1007 |
pending: true,
|
| 1008 |
+
attachments: optimisticAttachments,
|
| 1009 |
};
|
| 1010 |
|
| 1011 |
// Track sending message
|
|
|
|
| 1031 |
}, 10000);
|
| 1032 |
|
| 1033 |
dispatch(addMessage({ roomId: room, message: optimisticMsg }));
|
| 1034 |
+
|
| 1035 |
+
// Send with tempId (and attachment if uploaded) so BE can echo it back
|
| 1036 |
+
socketService.sendMessage({ roomId: room, content: contentTrimmed, tempId: msgTempId, attachment });
|
| 1037 |
|
| 1038 |
if (files?.length > 0 && /@StudyBot/i.test(contentTrimmed)) {
|
| 1039 |
callStudyBotWithFile(contentTrimmed, files[0], null);
|
src/services/message.service.js
CHANGED
|
@@ -36,6 +36,16 @@ export const messageService = {
|
|
| 36 |
});
|
| 37 |
},
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
// Pin a message
|
| 40 |
pinMessage: (roomId, messageId) =>
|
| 41 |
api.post(`/rooms/${roomId}/messages/${messageId}/pin`),
|
|
|
|
| 36 |
});
|
| 37 |
},
|
| 38 |
|
| 39 |
+
// Upload file to storage and get back URL (POST /files)
|
| 40 |
+
uploadFileToStorage: (file, options = {}) => {
|
| 41 |
+
const formData = new FormData();
|
| 42 |
+
formData.append("file", file);
|
| 43 |
+
if (options.roomId) formData.append("roomId", options.roomId);
|
| 44 |
+
return api.post("/files", formData, {
|
| 45 |
+
headers: { "Content-Type": "multipart/form-data" },
|
| 46 |
+
});
|
| 47 |
+
},
|
| 48 |
+
|
| 49 |
// Pin a message
|
| 50 |
pinMessage: (roomId, messageId) =>
|
| 51 |
api.post(`/rooms/${roomId}/messages/${messageId}/pin`),
|