Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion constants.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -20,6 +22,5 @@
"NEW_EVENT_READ_MESSAGE": "read_message",
"NEW_EVENT_ONLINE_STATUS": "online_status",
"NEW_EVENT_REQUEST_PUBLIC_KEY": "requestPublicKey",

"FIFTEEN_MINUTES": 900000
}
2 changes: 2 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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);
Expand Down
49 changes: 49 additions & 0 deletions server/migrations/addReactionsField.js
Original file line number Diff line number Diff line change
@@ -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();
7 changes: 6 additions & 1 deletion server/models/MessageModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() // Using a function to return a new Map instance
}
},
{
timestamps: true,
Expand All @@ -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);
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 76 additions & 0 deletions server/sockets/reactions.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
);
};

80 changes: 79 additions & 1 deletion server/utils/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>} - 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<boolean>} - 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,
Expand All @@ -567,5 +642,8 @@ module.exports = {
seenMessage,
blockUser,
isUserBlocked,
isMessageEditableOrDeletable
isMessageEditableOrDeletable,
removeReaction,
addReaction,
addReaction,
};
Loading