Skip to content

Commit

Permalink
4th part completed - socket
Browse files Browse the repository at this point in the history
  • Loading branch information
burakorkmez committed Jan 31, 2024
1 parent cecf505 commit 66a6766
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 11 deletions.
10 changes: 8 additions & 2 deletions backend/controllers/message.controller.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(<socket_id>).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);
Expand Down
4 changes: 2 additions & 2 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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}`);
});
38 changes: 38 additions & 0 deletions backend/socket/socket.js
Original file line number Diff line number Diff line change
@@ -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 };
84 changes: 81 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Binary file added frontend/src/assets/sounds/notification.mp3
Binary file not shown.
4 changes: 3 additions & 1 deletion frontend/src/components/messages/Message.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ const Message = ({ message }) => {
const profilePic = fromMe ? authUser.profilePic : selectedConversation?.profilePic;
const bubbleBgColor = fromMe ? "bg-blue-500" : "";

const shakeClass = message.shouldShake ? "shake" : "";

return (
<div className={`chat ${chatClassName}`}>
<div className='chat-image avatar'>
<div className='w-10 rounded-full'>
<img alt='Tailwind CSS chat bubble component' src={profilePic} />
</div>
</div>
<div className={`chat-bubble text-white ${bubbleBgColor} pb-2`}>{message.message}</div>
<div className={`chat-bubble text-white ${bubbleBgColor} ${shakeClass} pb-2`}>{message.message}</div>
<div className='chat-footer opacity-50 text-xs flex gap-1 items-center'>{formattedTime}</div>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/messages/MessageContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -33,10 +34,11 @@ const MessageContainer = () => {
export default MessageContainer;

const NoChatSelected = () => {
const { authUser } = useAuthContext();
return (
<div className='flex items-center justify-center w-full h-full'>
<div className='px-4 text-center sm:text-lg md:text-xl text-gray-200 font-semibold flex flex-col items-center gap-2'>
<p>Welcome 👋 John Doe</p>
<p>Welcome 👋 {authUser.fullName}</p>
<p>Select a chat to start messaging</p>
<TiMessages className='text-3xl md:text-6xl text-center' />
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/messages/Messages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/sidebar/Conversation.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
Expand All @@ -13,7 +16,7 @@ const Conversation = ({ conversation, lastIdx, emoji }) => {
`}
onClick={() => setSelectedConversation(conversation)}
>
<div className='avatar online'>
<div className={`avatar ${isOnline ? "online" : ""}`}>
<div className='w-12 rounded-full'>
<img src={conversation.profilePic} alt='user avatar' />
</div>
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/context/SocketContext.jsx
Original file line number Diff line number Diff line change
@@ -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 <SocketContext.Provider value={{ socket, onlineUsers }}>{children}</SocketContext.Provider>;
};
23 changes: 23 additions & 0 deletions frontend/src/hooks/useListenMessages.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 66a6766

Please sign in to comment.