diff --git a/backend/migrations/20251218192220-allow-null-message-content.js b/backend/migrations/20251218192220-allow-null-message-content.js new file mode 100644 index 0000000..e10bfd1 --- /dev/null +++ b/backend/migrations/20251218192220-allow-null-message-content.js @@ -0,0 +1,21 @@ +"use strict"; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn("Messages", "content", { + type: Sequelize.TEXT, + allowNull: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + // First update any null content to empty string before reverting + await queryInterface.sequelize.query( + `UPDATE "Messages" SET content = '' WHERE content IS NULL` + ); + await queryInterface.changeColumn("Messages", "content", { + type: Sequelize.TEXT, + allowNull: false, + }); + }, +}; diff --git a/backend/models/Message.js b/backend/models/Message.js index 26b09ad..37698b3 100644 --- a/backend/models/Message.js +++ b/backend/models/Message.js @@ -25,7 +25,7 @@ const Message = sequelize.define('Message', { }, content: { type: DataTypes.TEXT, - allowNull: false + allowNull: true }, isRead: { type: DataTypes.BOOLEAN, @@ -36,7 +36,15 @@ const Message = sequelize.define('Message', { allowNull: true } }, { - timestamps: true + timestamps: true, + validate: { + contentOrImage() { + const hasContent = this.content && this.content.trim().length > 0; + if (!hasContent && !this.imageFilename) { + throw new Error('Message must have content or an image'); + } + } + } }); module.exports = Message; \ No newline at end of file diff --git a/backend/routes/messages.js b/backend/routes/messages.js index c7a49bc..c731c08 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -316,7 +316,7 @@ router.post('/', authenticateToken, async (req, res, next) => { error: error.message, stack: error.stack, senderId: req.user.id, - receiverId: req.body.receiverId + receiverId: req.body?.receiverId }); next(error); } diff --git a/frontend/src/components/ChatWindow.tsx b/frontend/src/components/ChatWindow.tsx index 47bc9a6..6158676 100644 --- a/frontend/src/components/ChatWindow.tsx +++ b/frontend/src/components/ChatWindow.tsx @@ -6,7 +6,7 @@ import React, { useCallback, } from "react"; import { messageAPI } from "../services/api"; -import { getSignedUrl } from "../services/uploadService"; +import { getSignedUrl, uploadFile } from "../services/uploadService"; import { User, Message } from "../types"; import { useAuth } from "../contexts/AuthContext"; import { useSocket } from "../contexts/SocketContext"; @@ -374,15 +374,18 @@ const ChatWindow: React.FC = ({ } try { - // Build FormData for message (with or without image) - const formData = new FormData(); - formData.append("receiverId", recipient.id); - formData.append("content", messageContent || " "); // Send space if only image + // Upload image to S3 first if present + let imageFilename: string | undefined; if (imageToSend) { - formData.append("image", imageToSend); + const { key } = await uploadFile("message", imageToSend); + imageFilename = key; } - const response = await messageAPI.sendMessage(formData); + const response = await messageAPI.sendMessage({ + receiverId: recipient.id, + content: messageContent || "", + imageFilename, + }); // Add message to sender's chat immediately for instant feedback // Socket will handle updating the receiver's chat @@ -559,7 +562,7 @@ const ChatWindow: React.FC = ({ /> )} - {message.content.trim() && ( + {message.content?.trim() && (

{message.content}

diff --git a/frontend/src/components/MessageModal.tsx b/frontend/src/components/MessageModal.tsx index 1625401..1c49030 100644 --- a/frontend/src/components/MessageModal.tsx +++ b/frontend/src/components/MessageModal.tsx @@ -20,11 +20,10 @@ const MessageModal: React.FC = ({ show, onClose, recipient, o setSending(true); try { - const formData = new FormData(); - formData.append('receiverId', recipient.id); - formData.append('content', content); - - await messageAPI.sendMessage(formData); + await messageAPI.sendMessage({ + receiverId: recipient.id, + content, + }); setContent(''); onClose(); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 636c099..2a8e4c7 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -248,12 +248,11 @@ export const messageAPI = { getSentMessages: () => api.get("/messages/sent"), getConversations: () => api.get("/messages/conversations"), getMessage: (id: string) => api.get(`/messages/${id}`), - sendMessage: (formData: FormData) => - api.post("/messages", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }), + sendMessage: (data: { + receiverId: string; + content: string; + imageFilename?: string; + }) => api.post("/messages", data), markAsRead: (id: string) => api.put(`/messages/${id}/read`), getUnreadCount: () => api.get("/messages/unread/count"), };