diff --git a/backend/src/Database/connection.js b/backend/src/Database/connection.js index 811a091..86cdc32 100644 --- a/backend/src/Database/connection.js +++ b/backend/src/Database/connection.js @@ -6,7 +6,7 @@ const connectDB = async () => { if (!mongoUrl) { throw new Error("MONGODB_URI or MONGO_URL not found in environment variables"); } - + await mongoose.connect(mongoUrl); console.log(`MongoDB connected successfully ${mongoose.connection.host}`); } catch (err) { diff --git a/backend/src/routes/message.route.js b/backend/src/routes/message.route.js index 69cfa05..c229cba 100644 --- a/backend/src/routes/message.route.js +++ b/backend/src/routes/message.route.js @@ -6,8 +6,10 @@ const { getUnreadCount } = require('../controllers/message.controller'); +const { protectRoute } = require('../middleware/auth.middleware'); + // Send message -router.post('/send', sendMessage); +router.post('/send', protectRoute, sendMessage); // Get messages with a user // router.get('/:userId', getMessages); diff --git a/frontend/src/pages/chat/components/MessageInput.tsx b/frontend/src/pages/chat/components/MessageInput.tsx index 8cb4b34..2569b2a 100644 --- a/frontend/src/pages/chat/components/MessageInput.tsx +++ b/frontend/src/pages/chat/components/MessageInput.tsx @@ -8,11 +8,11 @@ import { useState } from "react"; const MessageInput = () => { const [newMessage, setNewMessage] = useState(""); const { user } = useUser(); - const { selectedUser, sendMessge } = useChatStore(); + const { selectedUser, sendMessage } = useChatStore(); - const handleSend = () => { - if (!selectedUser || !user || !newMessage) return; - sendMessge(selectedUser.clerkId, user.id, newMessage.trim()); + const handleSend = async () => { + if (!selectedUser || !user || !newMessage.trim()) return; + await sendMessage(selectedUser.clerkId, newMessage.trim()); setNewMessage(""); }; diff --git a/frontend/src/pages/chat/page/ChatPage.tsx b/frontend/src/pages/chat/page/ChatPage.tsx index 082cd7f..9f667da 100644 --- a/frontend/src/pages/chat/page/ChatPage.tsx +++ b/frontend/src/pages/chat/page/ChatPage.tsx @@ -1,13 +1,11 @@ - import { useChatStore } from "@/store/useChatStore"; import { useUser } from "@clerk/clerk-react"; -import { useEffect } from "react"; -import UsersList from "../components/UsersList.jsx"; +import { useEffect, useRef } from "react"; +import UsersList from "../components/UsersList"; import ChatHeader from "../components/ChatHeader"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; import MessageInput from "../components/MessageInput"; -// import TopHeader from "../../../components/layout/components/TopHeader"; const formatTime = (date: string | number | Date) => { return new Date(date).toLocaleTimeString("en-US", { @@ -19,32 +17,38 @@ const formatTime = (date: string | number | Date) => { const ChatPage = () => { const { user } = useUser(); - const { messages, selectedUser, fetchUsers, fetchMessages } = useChatStore(); + const { messages, selectedUser, fetchUsers, fetchMessages, startPolling, stopPolling } = useChatStore(); + const scrollRef = useRef(null); useEffect(() => { if (user) fetchUsers(); }, [fetchUsers, user]); useEffect(() => { - if (selectedUser) fetchMessages(selectedUser.clerkId); - }, [selectedUser, fetchMessages]); + if (selectedUser) { + fetchMessages(selectedUser.clerkId); + startPolling(selectedUser.clerkId); + } + + return () => stopPolling(); + }, [selectedUser, fetchMessages, startPolling, stopPolling]); - // console.log({ messages }); + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages]); return (
- {/* */} -
- {/* chat message */}
{selectedUser ? ( <> - {/* Messages */}
{messages?.map((message) => ( @@ -76,6 +80,7 @@ const ChatPage = () => {
))} +
@@ -89,7 +94,6 @@ const ChatPage = () => {
); }; -export default ChatPage; const NoConversationPlaceholder = () => (
@@ -99,4 +103,6 @@ const NoConversationPlaceholder = () => (

Choose a friend to start chatting

-); \ No newline at end of file +); + +export default ChatPage; \ No newline at end of file diff --git a/frontend/src/store/useChatStore.tsx b/frontend/src/store/useChatStore.tsx index 657dc49..a7e4ef9 100644 --- a/frontend/src/store/useChatStore.tsx +++ b/frontend/src/store/useChatStore.tsx @@ -2,6 +2,7 @@ import { create } from "zustand"; import { axiosInstance } from "../lib/axios"; import type { Message, User } from "@/types"; import { io, type Socket } from "socket.io-client"; +import toast from "react-hot-toast"; interface chatStore { users: User[]; @@ -13,20 +14,23 @@ interface chatStore { userActivities: Map; messages: Message[]; selectedUser: User | null; + pollingInterval: number | null; fetchUsers: () => Promise; initSocket: (userId: string) => void; disconnectSocket: () => void; - sendMessge: (receiverId: string, senderId: string, content: string) => void; + sendMessage: (receiverId: string, content: string) => Promise; fetchMessages: (userId: string) => Promise; setSelectedUser: (user: User | null) => void; + startPolling: (userId: string) => void; + stopPolling: () => void; } const baseURL = import.meta.env.MODE === "development" ? "http://localhost:5000" : "https://music-bd.vercel.app"; -// const baseURL = "https://music-bd.vercel.app"; + const socket = io(baseURL, { autoConnect: false, withCredentials: true, @@ -42,49 +46,34 @@ export const useChatStore = create((set, get) => ({ userActivities: new Map(), messages: [], selectedUser: null, + pollingInterval: null, setSelectedUser: (user) => set({ selectedUser: user }), fetchUsers: async () => { set({ isLoading: true, error: null }); try { - await axiosInstance.get("/users").then((res) => { - const response = res.data.user; - set({ users: response }); - // console.log(response); - }); - } catch (error) { - let message = "Error fetching users"; - try { - const e = JSON.parse(JSON.stringify(error)); - if (e?.response?.data?.message) message = e.response.data.message; - else if (e?.message) message = e.message; - } catch { - // keep generic - } - set({ error: message }); + const res = await axiosInstance.get("/users"); + set({ users: res.data.user }); + } catch (error: unknown) { + const errorMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || "Error fetching users"; + set({ error: errorMessage }); } finally { set({ isLoading: false }); } }, + initSocket: (userId) => { if (!get().isConnected) { socket.auth = { userId }; socket.connect(); - // Clear existing listeners to prevent duplicates - socket.off("user_online"); - socket.off("activities"); - socket.off("user_connected"); - socket.off("user_disconnected"); - socket.off("receive_message"); - socket.off("message_sent"); - socket.off("activity_updated"); + // Clear existing listeners + const events = ["user_online", "activities", "user_connected", "user_disconnected", "receive_message", "message_sent", "activity_updated"]; + events.forEach(event => socket.off(event)); socket.emit("user_connected", userId); - - // store socket instance in state so other actions can use it - set({ socket }); + set({ socket, isConnected: true }); socket.on("user_online", (users: string[]) => { set({ onlineUsers: new Set(users) }); @@ -94,89 +83,123 @@ export const useChatStore = create((set, get) => ({ set({ userActivities: new Map(activities) }); }); - socket.on("user_connected", (userId: string) => { + socket.on("user_connected", (id: string) => { set((state) => ({ - onlineUsers: new Set([...state.onlineUsers, userId]), + onlineUsers: new Set([...state.onlineUsers, id]), })); }); - socket.on("user_disconnected", (userId: string) => { + socket.on("user_disconnected", (id: string) => { set((state) => { const newOnlineUsers = new Set(state.onlineUsers); - newOnlineUsers.delete(userId); + newOnlineUsers.delete(id); return { onlineUsers: newOnlineUsers }; }); }); - socket.on("receive_message", (message: Message) => { - set((state) => ({ - messages: [...state.messages, message], - })); - }); + const handleNewMessage = (message: Message) => { + set((state) => { + if (state.messages.some(m => m._id === message._id)) return state; + const newMessages = [...state.messages, message].sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + return { messages: newMessages }; + }); + }; - socket.on("message_sent", (message: Message) => { - set((state) => ({ - messages: [...state.messages, message], - })); - }); + socket.on("receive_message", handleNewMessage); + socket.on("message_sent", handleNewMessage); - socket.on("activity_updated", ({ userId, activity }) => { - // console.log('Activity updated:', { userId, activity }); + socket.on("activity_updated", ({ userId: id, activity }) => { set((state) => { const newActivities = new Map(state.userActivities); - newActivities.set(userId, activity); - // console.log('New activities map:', Array.from(newActivities.entries())); + newActivities.set(id, activity); return { userActivities: newActivities }; }); }); - - set({ isConnected: true }); } }, + disconnectSocket: () => { if (get().isConnected) { - socket.off("user_online"); - socket.off("activities"); - socket.off("user_connected"); - socket.off("user_disconnected"); - socket.off("receive_message"); - socket.off("message_sent"); - socket.off("activity_updated"); - socket.disconnect(); set({ isConnected: false, socket: null }); } + get().stopPolling(); }, - sendMessge: (receiverId, senderId, content) => { - // prefer using the stored socket instance - const sock = get().socket || socket; - if (!sock) return; - sock.emit("send_message", { receiverId, senderId, content }); + sendMessage: async (receiverId, content) => { + if (!content.trim()) return; + + try { + const response = await axiosInstance.post("/messages/send", { + receiverId, + content: content.trim(), + }); + + if (response.data.success) { + const newMessage = response.data.data; + set((state) => { + if (state.messages.some(m => m._id === newMessage._id)) return state; + const newMessages = [...state.messages, newMessage].sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + return { messages: newMessages }; + }); + + const sock = get().socket; + if (sock && sock.connected) { + sock.emit("send_message", { receiverId, content }); + } + } + } catch (error: unknown) { + const errorMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to send message"; + toast.error(errorMessage); + } }, fetchMessages: async (userId: string) => { - set({ isLoading: true, error: null }); + const isFirstFetch = get().messages.length === 0; + if (isFirstFetch) set({ isLoading: true, error: null }); + try { const response = await axiosInstance.get(`/users/message/${userId}`); - // backend returns an array directly (res.json(messages)), - // but some endpoints may return { messages: [...] }. const messagesPayload = Array.isArray(response.data) ? response.data : response.data?.messages ?? []; - set({ messages: messagesPayload }); - } catch (error) { - let message = "Error fetching messages"; - try { - const e = JSON.parse(JSON.stringify(error)); - if (e?.response?.data?.message) message = e.response.data.message; - else if (e?.message) message = e.message; - } catch { - // keep generic message + + const sortedMessages = messagesPayload.sort( + (a: Message, b: Message) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + + if (JSON.stringify(sortedMessages) !== JSON.stringify(get().messages)) { + set({ messages: sortedMessages }); + } + } catch (error: unknown) { + if (isFirstFetch) { + const errorMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || "Error fetching messages"; + set({ error: errorMessage }); } - set({ error: message }); } finally { - set({ isLoading: false }); + if (isFirstFetch) set({ isLoading: false }); + } + }, + + startPolling: (userId) => { + get().stopPolling(); + const interval = window.setInterval(() => { + if (!get().isConnected || !get().socket?.connected) { + get().fetchMessages(userId); + } + }, 4000); + set({ pollingInterval: interval }); + }, + + stopPolling: () => { + const interval = get().pollingInterval; + if (interval !== null) { + window.clearInterval(interval); + set({ pollingInterval: null }); } }, }));