Skip to content

Commit e4dbe7d

Browse files
authored
Merge pull request #3 from EcstaticFly/feature/message_seenAt
feat: Implement message seen status, time and update message model
2 parents a2f0ce7 + 4b4fdc6 commit e4dbe7d

5 files changed

Lines changed: 145 additions & 37 deletions

File tree

client/src/components/chat-container.jsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MessageInput } from "./messageInput.jsx";
55
import { MessageSkeleton } from "./skeletons/messageSkeleton.jsx";
66
import { authStore } from "../store/authStore.js";
77
import { formatMessageTime } from "../configs/utils.js";
8-
import { X, FileText, Download, ArrowUpRightFromSquare } from "lucide-react";
8+
import { X, FileText, ArrowUpRightFromSquare } from "lucide-react";
99
import ReactMarkdown from "react-markdown";
1010

1111
export default function ChatContainer() {
@@ -17,6 +17,7 @@ export default function ChatContainer() {
1717
isChatLoading,
1818
listenIncomingMessage,
1919
stopListenIncomingMessage,
20+
markMessagesAsSeen,
2021
} = chatStore();
2122
const { user } = authStore();
2223

@@ -25,12 +26,14 @@ export default function ChatContainer() {
2526
useEffect(() => {
2627
getMessages(selectedUser._id);
2728
listenIncomingMessage();
29+
markMessagesAsSeen(selectedUser._id);
2830
return () => stopListenIncomingMessage();
2931
}, [
3032
getMessages,
3133
selectedUser._id,
3234
listenIncomingMessage,
3335
stopListenIncomingMessage,
36+
markMessagesAsSeen,
3437
]);
3538

3639
useEffect(() => {
@@ -124,15 +127,34 @@ export default function ChatContainer() {
124127
<ReactMarkdown>{message.text}</ReactMarkdown>
125128
</div>
126129
)}
127-
<p
128-
className={`text-[10px] mt-1.5 ${
129-
message?.senderId === user._id
130-
? "text-primary-content/70"
131-
: "text-base-content/70"
132-
}`}
133-
>
134-
{formatMessageTime(message?.createdAt)}
135-
</p>
130+
131+
<div className="flex items-center justify-end gap-1">
132+
<p
133+
className={`text-[10px] ${
134+
message?.senderId === user._id
135+
? "text-primary-content/70"
136+
: "text-base-content/70"
137+
}`}
138+
>
139+
{formatMessageTime(message?.createdAt)}
140+
</p>
141+
142+
{/* Show seen status for sent messages */}
143+
{message?.senderId === user._id && (
144+
<span className="text-[10px]">
145+
{message.seenAt ? (
146+
<span
147+
className="text-blue-600"
148+
title={`Seen ${formatMessageTime(message.seenAt)}`}
149+
>
150+
✓✓
151+
</span>
152+
) : (
153+
<span className="text-primary-content/50"></span>
154+
)}
155+
</span>
156+
)}
157+
</div>
136158
</div>
137159
</div>
138160
))}

client/src/store/chatStore.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,36 @@ export const chatStore = create((set, get) => ({
8787
}
8888
},
8989

90+
markMessagesAsSeen: (senderId) => {
91+
const socket = authStore.getState().socket;
92+
if (socket) {
93+
socket.emit("markMessagesAsSeen", { senderId });
94+
}
95+
},
96+
97+
listenMessagesSeen: () => {
98+
const socket = authStore.getState().socket;
99+
100+
socket.on("messagesSeen", ({ seenBy, seenAt }) => {
101+
const { messages, selectedUser } = get();
102+
103+
if (selectedUser?._id === seenBy) {
104+
const updatedMessages = messages.map((msg) => {
105+
if (msg.senderId === authStore.getState().user._id && !msg.seenAt) {
106+
return { ...msg, seenAt };
107+
}
108+
return msg;
109+
});
110+
set({ messages: updatedMessages });
111+
}
112+
});
113+
},
114+
115+
stopListenMessagesSeen: () => {
116+
const socket = authStore.getState().socket;
117+
socket.off("messagesSeen");
118+
},
119+
90120
listenIncomingMessage: () => {
91121
const { selectedUser } = get();
92122
if (!selectedUser) return;
@@ -95,11 +125,14 @@ export const chatStore = create((set, get) => ({
95125
socket.on("newMessage", (message) => {
96126
if (message.senderId !== selectedUser._id) return;
97127
set({ messages: [...get().messages, message] });
128+
get().markMessagesAsSeen(selectedUser._id);
98129
});
130+
get().listenMessagesSeen();
99131
},
100132

101133
stopListenIncomingMessage: () => {
102134
const socket = authStore.getState().socket;
103135
socket.off("newMessage");
136+
get().stopListenMessagesSeen();
104137
},
105138
}));

server/configs/socket.js

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Server } from "socket.io";
22
import express from "express";
33
import http from "http";
4-
import {config} from 'dotenv';
4+
import { config } from "dotenv";
5+
import Message from "../models/message.js";
56

67
config();
78

@@ -16,28 +17,55 @@ const io = new Server(server, {
1617
},
1718
});
1819

19-
export function getReceiverSocketId(userId){
20+
export function getReceiverSocketId(userId) {
2021
return onlineUsersSocketMap[userId];
2122
}
2223

2324
const onlineUsersSocketMap = {};
2425

25-
io.on("connection",(socket)=>{
26-
console.log("a client connected", socket.id);
26+
io.on("connection", (socket) => {
27+
console.log("a client connected", socket.id);
2728

28-
const userId = socket.handshake.query.userId;
29+
const userId = socket.handshake.query.userId;
2930

30-
if(userId){
31-
onlineUsersSocketMap[userId] = socket.id;
32-
}
31+
if (userId) {
32+
onlineUsersSocketMap[userId] = socket.id;
33+
}
34+
35+
io.emit("getOnlineUsers", Object.keys(onlineUsersSocketMap));
36+
37+
socket.on("markMessagesAsSeen", async ({ senderId }) => {
38+
try {
39+
const receiverId = userId;
3340

34-
io.emit("getOnlineUsers",Object.keys(onlineUsersSocketMap));
41+
const result = await Message.updateMany(
42+
{
43+
senderId: senderId,
44+
receiverId: receiverId,
45+
seenAt: null,
46+
},
47+
{
48+
seenAt: new Date(),
49+
}
50+
);
3551

36-
socket.on("disconnect",()=>{
37-
console.log("a client disconnected", socket.id);
38-
delete onlineUsersSocketMap[userId];
39-
io.emit("getOnlineUsers",Object.keys(onlineUsersSocketMap));
40-
})
41-
})
52+
const senderSocketId = onlineUsersSocketMap[senderId];
53+
if (senderSocketId) {
54+
io.to(senderSocketId).emit("messagesSeen", {
55+
seenBy: receiverId,
56+
seenAt: new Date(),
57+
});
58+
}
59+
} catch (error) {
60+
console.error("Error marking messages as seen:", error);
61+
}
62+
});
63+
64+
socket.on("disconnect", () => {
65+
console.log("a client disconnected", socket.id);
66+
delete onlineUsersSocketMap[userId];
67+
io.emit("getOnlineUsers", Object.keys(onlineUsersSocketMap));
68+
});
69+
});
4270

43-
export {io,server,app};
71+
export { io, server, app };

server/controllers/messageController.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,39 @@ export const searchUser = async (req, res) => {
6868

6969
export const getMessages = async (req, res) => {
7070
try {
71-
const { id } = req.params;
72-
const currentUserId = req.user._id;
71+
const { id: userToChatId } = req.params;
72+
const myId = req.user._id;
73+
7374
const messages = await Message.find({
7475
$or: [
75-
{ senderId: currentUserId, receiverId: id },
76-
{ senderId: id, receiverId: currentUserId },
76+
{ senderId: myId, receiverId: userToChatId },
77+
{ senderId: userToChatId, receiverId: myId },
7778
],
7879
});
7980

81+
await Message.updateMany(
82+
{
83+
senderId: userToChatId,
84+
receiverId: myId,
85+
seenAt: null,
86+
},
87+
{
88+
seenAt: new Date(),
89+
}
90+
);
91+
92+
const senderSocketId = getReceiverSocketId(userToChatId);
93+
if (senderSocketId) {
94+
io.to(senderSocketId).emit("messagesSeen", {
95+
seenBy: myId,
96+
seenAt: new Date(),
97+
});
98+
}
99+
80100
res.status(200).json(messages);
81-
} catch (e) {
82-
console.log(e.message);
83-
res.status(500).json({ message: "Failed to fetch messages" });
101+
} catch (error) {
102+
console.log("Error in getMessages controller: ", error.message);
103+
res.status(500).json({ error: "Internal server error" });
84104
}
85105
};
86106

@@ -109,7 +129,7 @@ export const sendMessage = async (req, res) => {
109129
const docFile = req.files.document[0];
110130
const result = await uploadToCloudinary(
111131
docFile.buffer,
112-
"chatzy/messages/documents",
132+
"chatzy/messages/documents"
113133
);
114134

115135
// console.log("Uploaded document:", docFile.originalname);
@@ -128,6 +148,7 @@ export const sendMessage = async (req, res) => {
128148
receiverId,
129149
image: imageUrl,
130150
document: documentData,
151+
seenAt: null,
131152
});
132153
await message.save();
133154

@@ -153,13 +174,13 @@ export const sendMessage = async (req, res) => {
153174

154175
const sendChatBotMessage = async (data) => {
155176
try {
156-
const genAI = new GoogleGenAI({apiKey: process.env.CHATBOT_API_KEY});
177+
const genAI = new GoogleGenAI({ apiKey: process.env.CHATBOT_API_KEY });
157178

158179
const result = await genAI.models.generateContent({
159180
model: "gemini-2.5-flash",
160181
contents: data.prompt,
161182
});
162-
183+
163184
const message = await Message.create({
164185
text: result.text,
165186
senderId: chatBotId,
@@ -174,7 +195,7 @@ const sendChatBotMessage = async (data) => {
174195
}
175196
} catch (error) {
176197
console.error("ChatBot Error:", error.message);
177-
198+
178199
try {
179200
const errorMessage = await Message.create({
180201
text: "I'm currently unavailable due to high demand. Please try again in a few minutes.",

server/models/message.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ const messageSchema = new mongoose.Schema(
4444
type: documentSchema,
4545
required: false,
4646
},
47+
seenAt: {
48+
type: Date,
49+
default: null,
50+
},
4751
},
4852
{
4953
timestamps: true,

0 commit comments

Comments
 (0)