From 66a6766834be796eeaa454d4e39ee5b47252cda1 Mon Sep 17 00:00:00 2001 From: Burak Orkmez Date: Wed, 31 Jan 2024 04:00:24 +0300 Subject: [PATCH] 4th part completed - socket --- backend/controllers/message.controller.js | 10 ++- backend/server.js | 4 +- backend/socket/socket.js | 38 ++++++++ frontend/package-lock.json | 84 +++++++++++++++++- frontend/package.json | 1 + frontend/src/assets/sounds/notification.mp3 | Bin 0 -> 24965 bytes frontend/src/components/messages/Message.jsx | 4 +- .../components/messages/MessageContainer.jsx | 4 +- frontend/src/components/messages/Messages.jsx | 2 + .../src/components/sidebar/Conversation.jsx | 5 +- frontend/src/context/SocketContext.jsx | 41 +++++++++ frontend/src/hooks/useListenMessages.js | 23 +++++ frontend/src/index.css | 31 +++++++ frontend/src/main.jsx | 5 +- 14 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 backend/socket/socket.js create mode 100644 frontend/src/assets/sounds/notification.mp3 create mode 100644 frontend/src/context/SocketContext.jsx create mode 100644 frontend/src/hooks/useListenMessages.js 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 0000000000000000000000000000000000000000..7f8301de73e89db128b3c3b6c704ec415371ec68 GIT binary patch literal 24965 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 uImPdE{UVdHpzL97WR&Qdb`So-l1ESr&B1ugo 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 I{7>HJUk;T-i~s-t literal 0 HcmV?d00001 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( - + + +