From f0e9385490e7ff4f3f5f3c86931e6e9b77c4bf21 Mon Sep 17 00:00:00 2001 From: surajanthwal Date: Fri, 17 Jan 2025 19:36:00 +0530 Subject: [PATCH 1/3] dummy commit --- constants.json | 1 - 1 file changed, 1 deletion(-) diff --git a/constants.json b/constants.json index 831af1f5..8ca6ef14 100644 --- a/constants.json +++ b/constants.json @@ -20,6 +20,5 @@ "NEW_EVENT_READ_MESSAGE": "read_message", "NEW_EVENT_ONLINE_STATUS": "online_status", "NEW_EVENT_REQUEST_PUBLIC_KEY": "requestPublicKey", - "FIFTEEN_MINUTES": 900000 } \ No newline at end of file From 379f51cbea9cd7bb36a6a6ebb6fe0132ca6b154e Mon Sep 17 00:00:00 2001 From: argturing Date: Thu, 10 Apr 2025 11:23:04 +0530 Subject: [PATCH 2/3] chnages --- server/models/MessageModel.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/models/MessageModel.js b/server/models/MessageModel.js index 5ebdfbb1..49a18ad2 100644 --- a/server/models/MessageModel.js +++ b/server/models/MessageModel.js @@ -37,6 +37,11 @@ const MessageSchema = new Schema( type: Schema.Types.ObjectId, ref: 'Message', }, + reactions: { + type: Map, + of: [String], // Array of user IDs who reacted with this emoji + default: new Map() + } }, { timestamps: true, @@ -54,11 +59,11 @@ const MessageSchema = new Schema( oldMessages: this.oldMessages, isRead: this.isRead, replyTo: this.replyTo?.toString() || null, + reactions: this.reactions ? Object.fromEntries(this.reactions) : {} }; }, }, }, } ); - module.exports = model('Message', MessageSchema); From 5d5b429ce3ca5f1da29e271dbb7e0324b2eff8c5 Mon Sep 17 00:00:00 2001 From: argturing Date: Thu, 10 Apr 2025 21:06:11 +0530 Subject: [PATCH 3/3] added changes --- constants.json | 2 + server/index.js | 2 + server/migrations/addReactionsField.js | 49 ++++++++++++++++ server/models/MessageModel.js | 2 +- server/package.json | 3 +- server/sockets/reactions.js | 76 ++++++++++++++++++++++++ server/utils/lib.js | 80 +++++++++++++++++++++++++- 7 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 server/migrations/addReactionsField.js create mode 100644 server/sockets/reactions.js diff --git a/constants.json b/constants.json index 8ca6ef14..c8d3528e 100644 --- a/constants.json +++ b/constants.json @@ -1,4 +1,6 @@ { + "NEW_EVENT_ADD_REACTION": "add_reaction", + "NEW_EVENT_REMOVE_REACTION": "remove_reaction", "NEW_EVENT_CLOSE": "close", "NEW_EVENT_CLOSED": "closed", "NEW_EVENT_INACTIVE": "inactive", diff --git a/server/index.js b/server/index.js index 668c3e0b..8d639319 100644 --- a/server/index.js +++ b/server/index.js @@ -50,6 +50,7 @@ const CloseChatHandler = require('./sockets/close'); const stopSearch = require('./sockets/stopSearch'); const onlineStatus = require('./sockets/onlineStatus'); const requestPublicKeyHandler = require('./sockets/requestPublicKey'); +const ReactionsHandler = require('./sockets/reactions'); app.use(express.json()); app.use(cors()); @@ -66,6 +67,7 @@ io.on('connection', (socket) => { * This event is emitted once the user clicks on the Start button or * navigates to the /founduser route */ + ReactionsHandler(socket); JoinHandler(io, socket); SendMessageHandler(socket); EditMessageHandler(socket); diff --git a/server/migrations/addReactionsField.js b/server/migrations/addReactionsField.js new file mode 100644 index 00000000..17a402f9 --- /dev/null +++ b/server/migrations/addReactionsField.js @@ -0,0 +1,49 @@ +require('dotenv').config(); +const mongoose = require('mongoose'); +const Message = require('../models/MessageModel'); + +async function migrateMessages() { + console.log('Starting migration: Adding reactions field to messages'); + + try { + // Connect to MongoDB + await mongoose.connect(process.env.MongoDB_URL, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + console.log('Connected to MongoDB'); + + // Find all messages that don't have the reactions field + const messages = await Message.find({ reactions: { $exists: false } }); + console.log(`Found ${messages.length} messages to update`); + + // Update messages in batches to avoid memory issues + const batchSize = 100; + let updated = 0; + + for (let i = 0; i < messages.length; i += batchSize) { + const batch = messages.slice(i, i + batchSize); + const operations = batch.map(message => ({ + updateOne: { + filter: { _id: message._id }, + update: { $set: { reactions: new Map() } } + } + })); + + await Message.bulkWrite(operations); + updated += batch.length; + console.log(`Updated ${updated}/${messages.length} messages`); + } + + console.log('Migration completed successfully'); + } catch (error) { + console.error('Migration failed:', error); + } finally { + await mongoose.disconnect(); + console.log('Disconnected from MongoDB'); + } +} + +// Run the migration +migrateMessages(); \ No newline at end of file diff --git a/server/models/MessageModel.js b/server/models/MessageModel.js index 49a18ad2..c8c4efc1 100644 --- a/server/models/MessageModel.js +++ b/server/models/MessageModel.js @@ -40,7 +40,7 @@ const MessageSchema = new Schema( reactions: { type: Map, of: [String], // Array of user IDs who reacted with this emoji - default: new Map() + default: () => new Map() // Using a function to return a new Map instance } }, { diff --git a/server/package.json b/server/package.json index f8a18053..02355ce7 100644 --- a/server/package.json +++ b/server/package.json @@ -11,7 +11,8 @@ "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint --ext .js,.ts --ignore-path .gitignore --fix ./", "format": "prettier --write .", - "prepare": "cd .. && husky install server/.husky" + "prepare": "cd .. && husky install server/.husky", + "migrate:reactions": "node migrations/addReactionsField.js" }, "repository": { "type": "git", diff --git a/server/sockets/reactions.js b/server/sockets/reactions.js new file mode 100644 index 00000000..06a6f309 --- /dev/null +++ b/server/sockets/reactions.js @@ -0,0 +1,76 @@ +const { + NEW_EVENT_ADD_REACTION, + NEW_EVENT_REMOVE_REACTION + } = require('../../constants.json'); + const { getActiveUser, addReaction, removeReaction } = require('../utils/lib'); + + module.exports = (socket) => { + // Handle adding a reaction + socket.on( + NEW_EVENT_ADD_REACTION, + async ({ messageId, chatId, emoji }, reactionAddedSuccessfully) => { + try { + const user = getActiveUser({ + socketId: socket.id, + }); + + if (!user || !messageId || !chatId || !emoji) { + reactionAddedSuccessfully(false); + return; + } + + const reactionAdded = await addReaction(chatId, messageId, emoji, user.id); + + if (reactionAdded) { + // Broadcast the reaction to all users in the chat + socket.broadcast.to(chatId).emit(NEW_EVENT_ADD_REACTION, { + messageId, + chatId, + emoji, + userId: user.id + }); + } + + reactionAddedSuccessfully(reactionAdded); + } catch (error) { + console.error('Error adding reaction:', error); + reactionAddedSuccessfully(false); + } + } + ); + + // Handle removing a reaction + socket.on( + NEW_EVENT_REMOVE_REACTION, + async ({ messageId, chatId, emoji }, reactionRemovedSuccessfully) => { + try { + const user = getActiveUser({ + socketId: socket.id, + }); + + if (!user || !messageId || !chatId || !emoji) { + reactionRemovedSuccessfully(false); + return; + } + + const reactionRemoved = await removeReaction(chatId, messageId, emoji, user.id); + + if (reactionRemoved) { + // Broadcast the reaction removal to all users in the chat + socket.broadcast.to(chatId).emit(NEW_EVENT_REMOVE_REACTION, { + messageId, + chatId, + emoji, + userId: user.id + }); + } + + reactionRemovedSuccessfully(reactionRemoved); + } catch (error) { + console.error('Error removing reaction:', error); + reactionRemovedSuccessfully(false); + } + } + ); + }; + \ No newline at end of file diff --git a/server/utils/lib.js b/server/utils/lib.js index 3a741205..03a0cadb 100644 --- a/server/utils/lib.js +++ b/server/utils/lib.js @@ -546,6 +546,81 @@ async function isUserBlocked(users) { return timeSinceCreated <= FIFTEEN_MINUTES; // Check if within 15 minutes } + +/** + * Add a reaction to a message + * @param {string} chatId - The chat ID + * @param {string} messageId - The message ID + * @param {string} emoji - The emoji to add + * @param {string} userId - The user ID who reacted + * @returns {Promise} - Whether the reaction was added + */ +async function addReaction(chatId, messageId, emoji, userId) { + if (!chats[chatId] || !chats[chatId].messages[messageId]) { + return false; + } + + // Get the message from the database + const messageDoc = await Message.findById(messageId); + if (!messageDoc) {return false;} + + // Add the reaction using the instance method + const reactionAdded = await messageDoc.addReaction(emoji, userId); + + if (reactionAdded) { + // Update in-memory cache + const message = chats[chatId].messages[messageId]; + if (!message.reactions) { + message.reactions = {}; + } + + if (!message.reactions[emoji]) { + message.reactions[emoji] = []; + } + + if (!message.reactions[emoji].includes(userId)) { + message.reactions[emoji].push(userId); + } + } + + return reactionAdded; +} + +/** + * Remove a reaction from a message + * @param {string} chatId - The chat ID + * @param {string} messageId - The message ID + * @param {string} emoji - The emoji to remove + * @param {string} userId - The user ID who reacted + * @returns {Promise} - Whether the reaction was removed + */ +async function removeReaction(chatId, messageId, emoji, userId) { + if (!chats[chatId] || !chats[chatId].messages[messageId]) { + return false; + } + + // Get the message from the database + const messageDoc = await Message.findById(messageId); + if (!messageDoc) {return false;} + + // Remove the reaction using the instance method + const reactionRemoved = await messageDoc.removeReaction(emoji, userId); + + if (reactionRemoved) { + // Update in-memory cache + const message = chats[chatId].messages[messageId]; + if (message.reactions && message.reactions[emoji]) { + message.reactions[emoji] = message.reactions[emoji].filter(id => id !== userId); + + if (message.reactions[emoji].length === 0) { + delete message.reactions[emoji]; + } + } + } + + return reactionRemoved; +} + module.exports = { init, createChat, @@ -567,5 +642,8 @@ module.exports = { seenMessage, blockUser, isUserBlocked, - isMessageEditableOrDeletable + isMessageEditableOrDeletable, + removeReaction, + addReaction, + addReaction, };