diff --git a/backend/controllers/message.controller.js b/backend/controllers/message.controller.js index e5b13a3..144f4f3 100644 --- a/backend/controllers/message.controller.js +++ b/backend/controllers/message.controller.js @@ -1,5 +1,6 @@ import Conversation from "../models/conversation.model.js"; import Message from "../models/message.model.js"; +import { getReceiverSocketId, io } from "../socket/socket.js"; export const sendMessage = async (req, res) => { try { @@ -27,14 +28,19 @@ export const sendMessage = async (req, res) => { conversation.messages.push(newMessage._id); } - // SOCKET IO FUNCTIONALITY WILL GO HERE - // await conversation.save(); // await newMessage.save(); // this will run in parallel await Promise.all([conversation.save(), newMessage.save()]); + // SOCKET IO FUNCTIONALITY WILL GO HERE + const receiverSocketId = getReceiverSocketId(receiverId); + if (receiverSocketId) { + // io.to().emit() used to send events to specific client + io.to(receiverSocketId).emit("newMessage", newMessage); + } + res.status(201).json(newMessage); } catch (error) { console.log("Error in sendMessage controller: ", error.message); diff --git a/backend/server.js b/backend/server.js index 68d4fa0..53cf836 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,8 +7,8 @@ import messageRoutes from "./routes/message.routes.js"; import userRoutes from "./routes/user.routes.js"; import connectToMongoDB from "./db/connectToMongoDB.js"; +import { app, server } from "./socket/socket.js"; -const app = express(); const PORT = process.env.PORT || 5000; dotenv.config(); @@ -25,7 +25,7 @@ app.use("/api/users", userRoutes); // res.send("Hello World!!"); // }); -app.listen(PORT, () => { +server.listen(PORT, () => { connectToMongoDB(); console.log(`Server Running on port ${PORT}`); }); diff --git a/backend/socket/socket.js b/backend/socket/socket.js new file mode 100644 index 0000000..a2bb0b4 --- /dev/null +++ b/backend/socket/socket.js @@ -0,0 +1,38 @@ +import { Server } from "socket.io"; +import http from "http"; +import express from "express"; + +const app = express(); + +const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: ["http://localhost:3000"], + methods: ["GET", "POST"], + }, +}); + +export const getReceiverSocketId = (receiverId) => { + return userSocketMap[receiverId]; +}; + +const userSocketMap = {}; // {userId: socketId} + +io.on("connection", (socket) => { + console.log("a user connected", socket.id); + + const userId = socket.handshake.query.userId; + if (userId != "undefined") userSocketMap[userId] = socket.id; + + // io.emit() is used to send events to all the connected clients + io.emit("getOnlineUsers", Object.keys(userSocketMap)); + + // socket.on() is used to listen to the events. can be used both on client and server side + socket.on("disconnect", () => { + console.log("user disconnected", socket.id); + delete userSocketMap[userId]; + io.emit("getOnlineUsers", Object.keys(userSocketMap)); + }); +}); + +export { app, io, server }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba187e3..5393797 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "react-hot-toast": "^2.4.1", "react-icons": "^5.0.1", "react-router-dom": "^6.21.3", + "socket.io-client": "^4.7.4", "zustand": "^4.5.0" }, "devDependencies": { @@ -1177,6 +1178,11 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1813,7 +1819,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1905,6 +1910,26 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -3445,8 +3470,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -4380,6 +4404,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", + "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -5192,6 +5242,34 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e0d223f..9c4d8e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "react-hot-toast": "^2.4.1", "react-icons": "^5.0.1", "react-router-dom": "^6.21.3", + "socket.io-client": "^4.7.4", "zustand": "^4.5.0" }, "devDependencies": { diff --git a/frontend/src/assets/sounds/notification.mp3 b/frontend/src/assets/sounds/notification.mp3 new file mode 100644 index 0000000..7f8301d Binary files /dev/null and b/frontend/src/assets/sounds/notification.mp3 differ diff --git a/frontend/src/components/messages/Message.jsx b/frontend/src/components/messages/Message.jsx index dd5cbc3..09b2b58 100644 --- a/frontend/src/components/messages/Message.jsx +++ b/frontend/src/components/messages/Message.jsx @@ -11,6 +11,8 @@ const Message = ({ message }) => { const profilePic = fromMe ? authUser.profilePic : selectedConversation?.profilePic; const bubbleBgColor = fromMe ? "bg-blue-500" : ""; + const shakeClass = message.shouldShake ? "shake" : ""; + return (
@@ -18,7 +20,7 @@ const Message = ({ message }) => { Tailwind CSS chat bubble component
-
{message.message}
+
{message.message}
{formattedTime}
); diff --git a/frontend/src/components/messages/MessageContainer.jsx b/frontend/src/components/messages/MessageContainer.jsx index 41be7e0..c2a8160 100644 --- a/frontend/src/components/messages/MessageContainer.jsx +++ b/frontend/src/components/messages/MessageContainer.jsx @@ -3,6 +3,7 @@ import useConversation from "../../zustand/useConversation"; import MessageInput from "./MessageInput"; import Messages from "./Messages"; import { TiMessages } from "react-icons/ti"; +import { useAuthContext } from "../../context/AuthContext"; const MessageContainer = () => { const { selectedConversation, setSelectedConversation } = useConversation(); @@ -33,10 +34,11 @@ const MessageContainer = () => { export default MessageContainer; const NoChatSelected = () => { + const { authUser } = useAuthContext(); return (
-

Welcome 👋 John Doe ❄

+

Welcome 👋 {authUser.fullName} ❄

Select a chat to start messaging

diff --git a/frontend/src/components/messages/Messages.jsx b/frontend/src/components/messages/Messages.jsx index afa8765..f4c7fc8 100644 --- a/frontend/src/components/messages/Messages.jsx +++ b/frontend/src/components/messages/Messages.jsx @@ -2,9 +2,11 @@ import { useEffect, useRef } from "react"; import useGetMessages from "../../hooks/useGetMessages"; import MessageSkeleton from "../skeletons/MessageSkeleton"; import Message from "./Message"; +import useListenMessages from "../../hooks/useListenMessages"; const Messages = () => { const { messages, loading } = useGetMessages(); + useListenMessages(); const lastMessageRef = useRef(); useEffect(() => { diff --git a/frontend/src/components/sidebar/Conversation.jsx b/frontend/src/components/sidebar/Conversation.jsx index c8a2e1f..6991320 100644 --- a/frontend/src/components/sidebar/Conversation.jsx +++ b/frontend/src/components/sidebar/Conversation.jsx @@ -1,9 +1,12 @@ +import { useSocketContext } from "../../context/SocketContext"; import useConversation from "../../zustand/useConversation"; const Conversation = ({ conversation, lastIdx, emoji }) => { const { selectedConversation, setSelectedConversation } = useConversation(); const isSelected = selectedConversation?._id === conversation._id; + const { onlineUsers } = useSocketContext(); + const isOnline = onlineUsers.includes(conversation._id); return ( <> @@ -13,7 +16,7 @@ const Conversation = ({ conversation, lastIdx, emoji }) => { `} onClick={() => setSelectedConversation(conversation)} > -
+
user avatar
diff --git a/frontend/src/context/SocketContext.jsx b/frontend/src/context/SocketContext.jsx new file mode 100644 index 0000000..e0a6745 --- /dev/null +++ b/frontend/src/context/SocketContext.jsx @@ -0,0 +1,41 @@ +import { createContext, useState, useEffect, useContext } from "react"; +import { useAuthContext } from "./AuthContext"; +import io from "socket.io-client"; + +const SocketContext = createContext(); + +export const useSocketContext = () => { + return useContext(SocketContext); +}; + +export const SocketContextProvider = ({ children }) => { + const [socket, setSocket] = useState(null); + const [onlineUsers, setOnlineUsers] = useState([]); + const { authUser } = useAuthContext(); + + useEffect(() => { + if (authUser) { + const socket = io("http://localhost:5000", { + query: { + userId: authUser._id, + }, + }); + + setSocket(socket); + + // socket.on() is used to listen to the events. can be used both on client and server side + socket.on("getOnlineUsers", (users) => { + setOnlineUsers(users); + }); + + return () => socket.close(); + } else { + if (socket) { + socket.close(); + setSocket(null); + } + } + }, [authUser]); + + return {children}; +}; diff --git a/frontend/src/hooks/useListenMessages.js b/frontend/src/hooks/useListenMessages.js new file mode 100644 index 0000000..b85d35b --- /dev/null +++ b/frontend/src/hooks/useListenMessages.js @@ -0,0 +1,23 @@ +import { useEffect } from "react"; + +import { useSocketContext } from "../context/SocketContext"; +import useConversation from "../zustand/useConversation"; + +import notificationSound from "../assets/sounds/notification.mp3"; + +const useListenMessages = () => { + const { socket } = useSocketContext(); + const { messages, setMessages } = useConversation(); + + useEffect(() => { + socket?.on("newMessage", (newMessage) => { + newMessage.shouldShake = true; + const sound = new Audio(notificationSound); + sound.play(); + setMessages([...messages, newMessage]); + }); + + return () => socket?.off("newMessage"); + }, [socket, setMessages, messages]); +}; +export default useListenMessages; diff --git a/frontend/src/index.css b/frontend/src/index.css index 8bbb22f..1a8c1f1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -26,3 +26,34 @@ body { ::-webkit-scrollbar-thumb:hover { background: #242424; } + +/* SHAKE ANIMATION ON HORIZONTAL DIRECTION */ +.shake { + animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0.2s both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; +} + +@keyframes shake { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 6989ddb..6b81b55 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -4,12 +4,15 @@ import App from "./App.jsx"; import "./index.css"; import { BrowserRouter } from "react-router-dom"; import { AuthContextProvider } from "./context/AuthContext.jsx"; +import { SocketContextProvider } from "./context/SocketContext.jsx"; ReactDOM.createRoot(document.getElementById("root")).render( - + + +