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,