Skip to content
Merged
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
42 changes: 32 additions & 10 deletions client/src/components/chat-container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -17,6 +17,7 @@ export default function ChatContainer() {
isChatLoading,
listenIncomingMessage,
stopListenIncomingMessage,
markMessagesAsSeen,
} = chatStore();
const { user } = authStore();

Expand All @@ -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(() => {
Expand Down Expand Up @@ -124,15 +127,34 @@ export default function ChatContainer() {
<ReactMarkdown>{message.text}</ReactMarkdown>
</div>
)}
<p
className={`text-[10px] mt-1.5 ${
message?.senderId === user._id
? "text-primary-content/70"
: "text-base-content/70"
}`}
>
{formatMessageTime(message?.createdAt)}
</p>

<div className="flex items-center justify-end gap-1">
<p
className={`text-[10px] ${
message?.senderId === user._id
? "text-primary-content/70"
: "text-base-content/70"
}`}
>
{formatMessageTime(message?.createdAt)}
</p>

{/* Show seen status for sent messages */}
{message?.senderId === user._id && (
<span className="text-[10px]">
{message.seenAt ? (
<span
className="text-blue-600"
title={`Seen ${formatMessageTime(message.seenAt)}`}
>
✓✓
</span>
) : (
<span className="text-primary-content/50">✓</span>
)}
</span>
)}
</div>
</div>
</div>
))}
Expand Down
33 changes: 33 additions & 0 deletions client/src/store/chatStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
},
}));
60 changes: 44 additions & 16 deletions server/configs/socket.js
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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};
export { io, server, app };
43 changes: 32 additions & 11 deletions server/controllers/messageController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
}
};

Expand Down Expand Up @@ -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);
Expand All @@ -128,6 +148,7 @@ export const sendMessage = async (req, res) => {
receiverId,
image: imageUrl,
document: documentData,
seenAt: null,
});
await message.save();

Expand All @@ -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,
Expand All @@ -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.",
Expand Down
4 changes: 4 additions & 0 deletions server/models/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ const messageSchema = new mongoose.Schema(
type: documentSchema,
required: false,
},
seenAt: {
type: Date,
default: null,
},
},
{
timestamps: true,
Expand Down