diff --git a/backend/migrations/20241124000007-create-messages.js b/backend/migrations/20241124000007-create-messages.js new file mode 100644 index 0000000..2bc81b3 --- /dev/null +++ b/backend/migrations/20241124000007-create-messages.js @@ -0,0 +1,61 @@ +"use strict"; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("Messages", { + id: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + }, + senderId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: "Users", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + receiverId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: "Users", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + content: { + type: Sequelize.TEXT, + allowNull: false, + }, + isRead: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, + imagePath: { + type: Sequelize.STRING, + allowNull: true, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + }, + }); + + // Add indexes + await queryInterface.addIndex("Messages", ["senderId"]); + await queryInterface.addIndex("Messages", ["receiverId"]); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("Messages"); + }, +}; diff --git a/backend/models/Message.js b/backend/models/Message.js index 7269131..724f569 100644 --- a/backend/models/Message.js +++ b/backend/models/Message.js @@ -23,10 +23,6 @@ const Message = sequelize.define('Message', { key: 'id' } }, - subject: { - type: DataTypes.STRING, - allowNull: false - }, content: { type: DataTypes.TEXT, allowNull: false @@ -35,14 +31,6 @@ const Message = sequelize.define('Message', { type: DataTypes.BOOLEAN, defaultValue: false }, - parentMessageId: { - type: DataTypes.UUID, - allowNull: true, - references: { - model: 'Messages', - key: 'id' - } - }, imagePath: { type: DataTypes.STRING, allowNull: true diff --git a/backend/models/index.js b/backend/models/index.js index de20370..a794e96 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -27,11 +27,6 @@ User.hasMany(Message, { as: "sentMessages", foreignKey: "senderId" }); User.hasMany(Message, { as: "receivedMessages", foreignKey: "receiverId" }); Message.belongsTo(User, { as: "sender", foreignKey: "senderId" }); Message.belongsTo(User, { as: "receiver", foreignKey: "receiverId" }); -Message.hasMany(Message, { as: "replies", foreignKey: "parentMessageId" }); -Message.belongsTo(Message, { - as: "parentMessage", - foreignKey: "parentMessageId", -}); // Forum associations User.hasMany(ForumPost, { as: "forumPosts", foreignKey: "authorId" }); diff --git a/backend/routes/messages.js b/backend/routes/messages.js index c1fc02d..f7f2d33 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -171,11 +171,11 @@ router.get('/sent', authenticateToken, async (req, res) => { } }); -// Get a single message with replies +// Get a single message router.get('/:id', authenticateToken, async (req, res) => { try { const message = await Message.findOne({ - where: { + where: { id: req.params.id, [require('sequelize').Op.or]: [ { senderId: req.user.id }, @@ -192,15 +192,6 @@ router.get('/:id', authenticateToken, async (req, res) => { model: User, as: 'receiver', attributes: ['id', 'firstName', 'lastName', 'profileImage'] - }, - { - model: Message, - as: 'replies', - include: [{ - model: User, - as: 'sender', - attributes: ['id', 'firstName', 'lastName', 'profileImage'] - }] } ] }); @@ -248,7 +239,7 @@ router.get('/:id', authenticateToken, async (req, res) => { // Send a new message router.post('/', authenticateToken, uploadMessageImage, async (req, res) => { try { - const { receiverId, subject, content, parentMessageId } = req.body; + const { receiverId, content } = req.body; // Check if receiver exists const receiver = await User.findByPk(receiverId); @@ -267,9 +258,7 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res) => { const message = await Message.create({ senderId: req.user.id, receiverId, - subject, content, - parentMessageId, imagePath }); @@ -308,8 +297,7 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res) => { reqLogger.info("Message sent", { senderId: req.user.id, receiverId: receiverId, - messageId: message.id, - isReply: !!parentMessageId + messageId: message.id }); res.status(201).json(messageWithSender); diff --git a/backend/services/email/domain/MessagingEmailService.js b/backend/services/email/domain/MessagingEmailService.js index 6ebd3c2..8f7c5bd 100644 --- a/backend/services/email/domain/MessagingEmailService.js +++ b/backend/services/email/domain/MessagingEmailService.js @@ -39,7 +39,6 @@ class MessagingEmailService { * @param {string} sender.firstName - Sender's first name * @param {string} sender.lastName - Sender's last name * @param {Object} message - Message object - * @param {string} message.subject - Message subject * @param {string} message.content - Message content * @param {Date} message.createdAt - Message creation timestamp * @returns {Promise<{success: boolean, messageId?: string, error?: string}>} @@ -61,7 +60,6 @@ class MessagingEmailService { const variables = { recipientName: receiver.firstName || "there", senderName: `${sender.firstName} ${sender.lastName}`.trim() || "A user", - subject: message.subject, messageContent: message.content, conversationUrl: conversationUrl, timestamp: timestamp, diff --git a/backend/templates/emails/newMessageToUser.html b/backend/templates/emails/newMessageToUser.html index 8a1b092..633b5c4 100644 --- a/backend/templates/emails/newMessageToUser.html +++ b/backend/templates/emails/newMessageToUser.html @@ -134,13 +134,6 @@ border-radius: 6px; } - .message-box .subject { - font-size: 16px; - font-weight: 600; - color: #495057; - margin: 0 0 15px 0; - } - .message-box .content-text { color: #212529; line-height: 1.6; @@ -226,7 +219,6 @@

{{senderName}} sent you a message on RentAll.

-
Subject: {{subject}}
{{messageContent}}
Sent {{timestamp}}
diff --git a/backend/tests/unit/routes/messages.test.js b/backend/tests/unit/routes/messages.test.js index 7db4203..1d85ec3 100644 --- a/backend/tests/unit/routes/messages.test.js +++ b/backend/tests/unit/routes/messages.test.js @@ -56,7 +56,6 @@ describe('Messages Routes', () => { id: 1, senderId: 2, receiverId: 1, - subject: 'Test Message', content: 'Hello there!', isRead: false, createdAt: '2024-01-15T10:00:00.000Z', @@ -71,7 +70,6 @@ describe('Messages Routes', () => { id: 2, senderId: 3, receiverId: 1, - subject: 'Another Message', content: 'Hi!', isRead: true, createdAt: '2024-01-14T10:00:00.000Z', @@ -122,7 +120,6 @@ describe('Messages Routes', () => { id: 3, senderId: 1, receiverId: 2, - subject: 'My Message', content: 'Hello Jane!', isRead: false, createdAt: '2024-01-15T12:00:00.000Z', @@ -171,7 +168,6 @@ describe('Messages Routes', () => { id: 1, senderId: 2, receiverId: 1, - subject: 'Test Message', content: 'Hello there!', isRead: false, createdAt: '2024-01-15T10:00:00.000Z', @@ -187,19 +183,6 @@ describe('Messages Routes', () => { lastName: 'Doe', profileImage: 'john.jpg' }, - replies: [ - { - id: 4, - senderId: 1, - content: 'Reply message', - sender: { - id: 1, - firstName: 'John', - lastName: 'Doe', - profileImage: 'john.jpg' - } - } - ], update: jest.fn() }; @@ -207,7 +190,7 @@ describe('Messages Routes', () => { mockMessageFindOne.mockResolvedValue(mockMessage); }); - it('should get message with replies for receiver', async () => { + it('should get message for receiver and mark as read', async () => { mockMessage.update.mockResolvedValue(); const response = await request(app) @@ -218,7 +201,6 @@ describe('Messages Routes', () => { id: 1, senderId: 2, receiverId: 1, - subject: 'Test Message', content: 'Hello there!', isRead: false, createdAt: '2024-01-15T10:00:00.000Z', @@ -233,20 +215,7 @@ describe('Messages Routes', () => { firstName: 'John', lastName: 'Doe', profileImage: 'john.jpg' - }, - replies: [ - { - id: 4, - senderId: 1, - content: 'Reply message', - sender: { - id: 1, - firstName: 'John', - lastName: 'Doe', - profileImage: 'john.jpg' - } - } - ] + } }); expect(mockMessage.update).toHaveBeenCalledWith({ isRead: true }); }); @@ -263,7 +232,6 @@ describe('Messages Routes', () => { id: 1, senderId: 1, receiverId: 2, - subject: 'Test Message', content: 'Hello there!', isRead: false, createdAt: '2024-01-15T10:00:00.000Z', @@ -278,20 +246,7 @@ describe('Messages Routes', () => { firstName: 'John', lastName: 'Doe', profileImage: 'john.jpg' - }, - replies: [ - { - id: 4, - senderId: 1, - content: 'Reply message', - sender: { - id: 1, - firstName: 'John', - lastName: 'Doe', - profileImage: 'john.jpg' - } - } - ] + } }); expect(mockMessage.update).not.toHaveBeenCalled(); }); @@ -340,9 +295,7 @@ describe('Messages Routes', () => { id: 5, senderId: 1, receiverId: 2, - subject: 'New Message', - content: 'Hello Jane!', - parentMessageId: null + content: 'Hello Jane!' }; const mockMessageWithSender = { @@ -364,9 +317,7 @@ describe('Messages Routes', () => { it('should create a new message', async () => { const messageData = { receiverId: 2, - subject: 'New Message', - content: 'Hello Jane!', - parentMessageId: null + content: 'Hello Jane!' }; const response = await request(app) @@ -378,31 +329,8 @@ describe('Messages Routes', () => { expect(mockMessageCreate).toHaveBeenCalledWith({ senderId: 1, receiverId: 2, - subject: 'New Message', content: 'Hello Jane!', - parentMessageId: null - }); - }); - - it('should create a reply message with parentMessageId', async () => { - const replyData = { - receiverId: 2, - subject: 'Re: Original Message', - content: 'This is a reply', - parentMessageId: 1 - }; - - const response = await request(app) - .post('/messages') - .send(replyData); - - expect(response.status).toBe(201); - expect(mockMessageCreate).toHaveBeenCalledWith({ - senderId: 1, - receiverId: 2, - subject: 'Re: Original Message', - content: 'This is a reply', - parentMessageId: 1 + imagePath: null }); }); @@ -413,7 +341,6 @@ describe('Messages Routes', () => { .post('/messages') .send({ receiverId: 999, - subject: 'Test', content: 'Test message' }); @@ -426,7 +353,6 @@ describe('Messages Routes', () => { .post('/messages') .send({ receiverId: 1, // Same as sender ID - subject: 'Self Message', content: 'Hello self!' }); @@ -441,7 +367,6 @@ describe('Messages Routes', () => { .post('/messages') .send({ receiverId: 2, - subject: 'Test', content: 'Test message' }); @@ -596,62 +521,5 @@ describe('Messages Routes', () => { expect(response.status).toBe(200); expect(response.body).toEqual([]); }); - - it('should handle message with no replies', async () => { - const messageWithoutReplies = { - id: 1, - senderId: 2, - receiverId: 1, - subject: 'Test Message', - content: 'Hello there!', - isRead: false, - replies: [], - update: jest.fn() - }; - mockMessageFindOne.mockResolvedValue(messageWithoutReplies); - - const response = await request(app) - .get('/messages/1'); - - expect(response.status).toBe(200); - expect(response.body.replies).toEqual([]); - }); - - it('should handle missing optional fields in message creation', async () => { - const mockReceiver = { id: 2, firstName: 'Jane', lastName: 'Smith' }; - const mockCreatedMessage = { - id: 6, - senderId: 1, - receiverId: 2, - subject: undefined, - content: 'Just content', - parentMessageId: undefined - }; - const mockMessageWithSender = { - ...mockCreatedMessage, - sender: { id: 1, firstName: 'John', lastName: 'Doe' } - }; - - mockUserFindByPk.mockResolvedValue(mockReceiver); - mockMessageCreate.mockResolvedValue(mockCreatedMessage); - mockMessageFindByPk.mockResolvedValue(mockMessageWithSender); - - const response = await request(app) - .post('/messages') - .send({ - receiverId: 2, - content: 'Just content' - // subject and parentMessageId omitted - }); - - expect(response.status).toBe(201); - expect(mockMessageCreate).toHaveBeenCalledWith({ - senderId: 1, - receiverId: 2, - subject: undefined, - content: 'Just content', - parentMessageId: undefined - }); - }); }); }); \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 96c3660..8eaf995 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,7 +21,6 @@ import Owning from './pages/Owning'; import Profile from './pages/Profile'; import PublicProfile from './pages/PublicProfile'; import Messages from './pages/Messages'; -import MessageDetail from './pages/MessageDetail'; import ForumPosts from './pages/ForumPosts'; import ForumPostDetail from './pages/ForumPostDetail'; import CreateForumPost from './pages/CreateForumPost'; @@ -150,14 +149,6 @@ const AppContent: React.FC = () => { } /> - - - - } - /> } /> } /> = ({ show, onClose, recipient, onMes // Build FormData for message (with or without image) const formData = new FormData(); formData.append('receiverId', recipient.id); - formData.append('subject', `Message from ${currentUser?.firstName}`); formData.append('content', messageContent || ' '); // Send space if only image if (imageToSend) { formData.append('image', imageToSend); diff --git a/frontend/src/components/MessageModal.tsx b/frontend/src/components/MessageModal.tsx index 65f4471..1625401 100644 --- a/frontend/src/components/MessageModal.tsx +++ b/frontend/src/components/MessageModal.tsx @@ -10,7 +10,6 @@ interface MessageModalProps { } const MessageModal: React.FC = ({ show, onClose, recipient, onSuccess }) => { - const [subject, setSubject] = useState(''); const [content, setContent] = useState(''); const [sending, setSending] = useState(false); const [error, setError] = useState(null); @@ -23,12 +22,10 @@ const MessageModal: React.FC = ({ show, onClose, recipient, o try { const formData = new FormData(); formData.append('receiverId', recipient.id); - formData.append('subject', subject); formData.append('content', content); await messageAPI.sendMessage(formData); - setSubject(''); setContent(''); onClose(); if (onSuccess) { @@ -58,19 +55,6 @@ const MessageModal: React.FC = ({ show, onClose, recipient, o {error} )} - -
- - setSubject(e.target.value)} - required - disabled={sending} - /> -
@@ -95,10 +79,10 @@ const MessageModal: React.FC = ({ show, onClose, recipient, o > Cancel - - -
-
-
- {otherUser?.profileImage ? ( - {`${otherUser.firstName} - ) : ( -
- -
- )} -
-
{message.subject}
- - {isReceiver ? 'From' : 'To'}: {otherUser?.firstName} {otherUser?.lastName} • {formatDateTime(message.createdAt)} - -
-
-
-
-

{message.content}

-
-
- - {message.replies && message.replies.length > 0 && ( -
-
Replies
- {message.replies.map((reply) => ( -
-
-
- {reply.sender?.profileImage ? ( - {`${reply.sender.firstName} - ) : ( -
- -
- )} -
- {reply.sender?.firstName} {reply.sender?.lastName} - {formatDateTime(reply.createdAt)} -
-
-

{reply.content}

-
-
- ))} -
- )} - -
-
-
Send Reply
- {error && ( -
- {error} -
- )} -
-
- -
- -
-
-
-
- - - ); -}; - -export default MessageDetail; \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f672b59..6bf0734 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -39,14 +39,11 @@ export interface Message { id: string; senderId: string; receiverId: string; - subject: string; content: string; isRead: boolean; - parentMessageId?: string; imagePath?: string; sender?: User; receiver?: User; - replies?: Message[]; createdAt: string; updatedAt: string; }