From 4b4fdc6d7e14dedf021835190c1baffbfcebbfc8 Mon Sep 17 00:00:00 2001 From: Suyash Pandey Date: Fri, 2 Jan 2026 19:11:00 +0530 Subject: [PATCH] feat: Implement message seen status, time and update message model --- client/src/components/chat-container.jsx | 42 +++++++++++++---- client/src/store/chatStore.js | 33 +++++++++++++ server/configs/socket.js | 60 +++++++++++++++++------- server/controllers/messageController.js | 43 ++++++++++++----- server/models/message.js | 4 ++ 5 files changed, 145 insertions(+), 37 deletions(-) diff --git a/client/src/components/chat-container.jsx b/client/src/components/chat-container.jsx index ad5d669..86eab7f 100644 --- a/client/src/components/chat-container.jsx +++ b/client/src/components/chat-container.jsx @@ -5,7 +5,7 @@ import { MessageInput } from "./messageInput.jsx"; import { MessageSkeleton } from "./skeletons/messageSkeleton.jsx"; import { authStore } from "../store/authStore.js"; import { formatMessageTime } from "../configs/utils.js"; -import { X, FileText, Download, ArrowUpRightFromSquare } from "lucide-react"; +import { X, FileText, ArrowUpRightFromSquare } from "lucide-react"; import ReactMarkdown from "react-markdown"; export default function ChatContainer() { @@ -17,6 +17,7 @@ export default function ChatContainer() { isChatLoading, listenIncomingMessage, stopListenIncomingMessage, + markMessagesAsSeen, } = chatStore(); const { user } = authStore(); @@ -25,12 +26,14 @@ export default function ChatContainer() { useEffect(() => { getMessages(selectedUser._id); listenIncomingMessage(); + markMessagesAsSeen(selectedUser._id); return () => stopListenIncomingMessage(); }, [ getMessages, selectedUser._id, listenIncomingMessage, stopListenIncomingMessage, + markMessagesAsSeen, ]); useEffect(() => { @@ -124,15 +127,34 @@ export default function ChatContainer() { {message.text} )} -

- {formatMessageTime(message?.createdAt)} -

+ +
+

+ {formatMessageTime(message?.createdAt)} +

+ + {/* Show seen status for sent messages */} + {message?.senderId === user._id && ( + + {message.seenAt ? ( + + ✓✓ + + ) : ( + + )} + + )} +
))} diff --git a/client/src/store/chatStore.js b/client/src/store/chatStore.js index 8979bf9..cb3ff35 100644 --- a/client/src/store/chatStore.js +++ b/client/src/store/chatStore.js @@ -87,6 +87,36 @@ export const chatStore = create((set, get) => ({ } }, + markMessagesAsSeen: (senderId) => { + const socket = authStore.getState().socket; + if (socket) { + socket.emit("markMessagesAsSeen", { senderId }); + } + }, + + listenMessagesSeen: () => { + const socket = authStore.getState().socket; + + socket.on("messagesSeen", ({ seenBy, seenAt }) => { + const { messages, selectedUser } = get(); + + if (selectedUser?._id === seenBy) { + const updatedMessages = messages.map((msg) => { + if (msg.senderId === authStore.getState().user._id && !msg.seenAt) { + return { ...msg, seenAt }; + } + return msg; + }); + set({ messages: updatedMessages }); + } + }); + }, + + stopListenMessagesSeen: () => { + const socket = authStore.getState().socket; + socket.off("messagesSeen"); + }, + listenIncomingMessage: () => { const { selectedUser } = get(); if (!selectedUser) return; @@ -95,11 +125,14 @@ export const chatStore = create((set, get) => ({ socket.on("newMessage", (message) => { if (message.senderId !== selectedUser._id) return; set({ messages: [...get().messages, message] }); + get().markMessagesAsSeen(selectedUser._id); }); + get().listenMessagesSeen(); }, stopListenIncomingMessage: () => { const socket = authStore.getState().socket; socket.off("newMessage"); + get().stopListenMessagesSeen(); }, })); diff --git a/server/configs/socket.js b/server/configs/socket.js index 4e338b5..5c78e21 100644 --- a/server/configs/socket.js +++ b/server/configs/socket.js @@ -1,7 +1,8 @@ import { Server } from "socket.io"; import express from "express"; import http from "http"; -import {config} from 'dotenv'; +import { config } from "dotenv"; +import Message from "../models/message.js"; config(); @@ -16,28 +17,55 @@ const io = new Server(server, { }, }); -export function getReceiverSocketId(userId){ +export function getReceiverSocketId(userId) { return onlineUsersSocketMap[userId]; } const onlineUsersSocketMap = {}; -io.on("connection",(socket)=>{ - console.log("a client connected", socket.id); +io.on("connection", (socket) => { + console.log("a client connected", socket.id); - const userId = socket.handshake.query.userId; + const userId = socket.handshake.query.userId; - if(userId){ - onlineUsersSocketMap[userId] = socket.id; - } + if (userId) { + onlineUsersSocketMap[userId] = socket.id; + } + + io.emit("getOnlineUsers", Object.keys(onlineUsersSocketMap)); + + socket.on("markMessagesAsSeen", async ({ senderId }) => { + try { + const receiverId = userId; - io.emit("getOnlineUsers",Object.keys(onlineUsersSocketMap)); + const result = await Message.updateMany( + { + senderId: senderId, + receiverId: receiverId, + seenAt: null, + }, + { + seenAt: new Date(), + } + ); - socket.on("disconnect",()=>{ - console.log("a client disconnected", socket.id); - delete onlineUsersSocketMap[userId]; - io.emit("getOnlineUsers",Object.keys(onlineUsersSocketMap)); - }) -}) + const senderSocketId = onlineUsersSocketMap[senderId]; + if (senderSocketId) { + io.to(senderSocketId).emit("messagesSeen", { + seenBy: receiverId, + seenAt: new Date(), + }); + } + } catch (error) { + console.error("Error marking messages as seen:", error); + } + }); + + socket.on("disconnect", () => { + console.log("a client disconnected", socket.id); + delete onlineUsersSocketMap[userId]; + io.emit("getOnlineUsers", Object.keys(onlineUsersSocketMap)); + }); +}); -export {io,server,app}; \ No newline at end of file +export { io, server, app }; diff --git a/server/controllers/messageController.js b/server/controllers/messageController.js index 5ec7da4..eff6ffb 100644 --- a/server/controllers/messageController.js +++ b/server/controllers/messageController.js @@ -68,19 +68,39 @@ export const searchUser = async (req, res) => { export const getMessages = async (req, res) => { try { - const { id } = req.params; - const currentUserId = req.user._id; + const { id: userToChatId } = req.params; + const myId = req.user._id; + const messages = await Message.find({ $or: [ - { senderId: currentUserId, receiverId: id }, - { senderId: id, receiverId: currentUserId }, + { senderId: myId, receiverId: userToChatId }, + { senderId: userToChatId, receiverId: myId }, ], }); + await Message.updateMany( + { + senderId: userToChatId, + receiverId: myId, + seenAt: null, + }, + { + seenAt: new Date(), + } + ); + + const senderSocketId = getReceiverSocketId(userToChatId); + if (senderSocketId) { + io.to(senderSocketId).emit("messagesSeen", { + seenBy: myId, + seenAt: new Date(), + }); + } + res.status(200).json(messages); - } catch (e) { - console.log(e.message); - res.status(500).json({ message: "Failed to fetch messages" }); + } catch (error) { + console.log("Error in getMessages controller: ", error.message); + res.status(500).json({ error: "Internal server error" }); } }; @@ -109,7 +129,7 @@ export const sendMessage = async (req, res) => { const docFile = req.files.document[0]; const result = await uploadToCloudinary( docFile.buffer, - "chatzy/messages/documents", + "chatzy/messages/documents" ); // console.log("Uploaded document:", docFile.originalname); @@ -128,6 +148,7 @@ export const sendMessage = async (req, res) => { receiverId, image: imageUrl, document: documentData, + seenAt: null, }); await message.save(); @@ -153,13 +174,13 @@ export const sendMessage = async (req, res) => { const sendChatBotMessage = async (data) => { try { - const genAI = new GoogleGenAI({apiKey: process.env.CHATBOT_API_KEY}); + const genAI = new GoogleGenAI({ apiKey: process.env.CHATBOT_API_KEY }); const result = await genAI.models.generateContent({ model: "gemini-2.5-flash", contents: data.prompt, }); - + const message = await Message.create({ text: result.text, senderId: chatBotId, @@ -174,7 +195,7 @@ const sendChatBotMessage = async (data) => { } } catch (error) { console.error("ChatBot Error:", error.message); - + try { const errorMessage = await Message.create({ text: "I'm currently unavailable due to high demand. Please try again in a few minutes.", diff --git a/server/models/message.js b/server/models/message.js index 014fe3e..2e4ceda 100644 --- a/server/models/message.js +++ b/server/models/message.js @@ -44,6 +44,10 @@ const messageSchema = new mongoose.Schema( type: documentSchema, required: false, }, + seenAt: { + type: Date, + default: null, + }, }, { timestamps: true,