diff --git a/client/src/App.jsx b/client/src/App.jsx index 53be66a..f4d5d71 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -37,7 +37,7 @@ function App() { // Render a loading state until authentication check is complete if (!authChecked) { - return
Loading...
; + return
Loading...
; } return ( diff --git a/client/src/pages/Home/DirectAccess.jsx b/client/src/pages/Home/DirectAccess.jsx index 056cbe7..76682cd 100644 --- a/client/src/pages/Home/DirectAccess.jsx +++ b/client/src/pages/Home/DirectAccess.jsx @@ -7,7 +7,7 @@ import { useContext } from 'react'; import IsSearchingContext from '../../Contexts/IsSearchingContext'; import PropTypes from 'prop-types'; -const DirectAccess = ({ setSelectedUser }) => { +const DirectAccess = ({ setSelectedUser, selectedUser, setSelectedConnection }) => { const [placeholderValue, setPlaceholderValue] = useState('Search Here'); const [inputValue, setInputValue] = useState(''); const [searchKey, setSearchKey] = useState(0); // Key to force remount of SearchedProfile component @@ -45,13 +45,15 @@ const DirectAccess = ({ setSelectedUser }) => { placeholder={placeholderValue} /> - {isSearching ? : } + {isSearching ? : } ); }; DirectAccess.propTypes = { setSelectedUser: PropTypes.func.isRequired, + selectedUser: PropTypes.object, + setSelectedConnection: PropTypes.func, }; export default DirectAccess; diff --git a/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx b/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx index 215fe4d..78d4e90 100644 --- a/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx +++ b/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { useContext, useEffect, useState } from 'react'; import UserContext from '../../../Contexts/userContext'; -const ChatContainerComponent = ({ selectedUser, socket }) => { +const ChatContainerComponent = ({ selectedUser, socket, selectedConnection }) => { // State to store the messages in the chat const [messages, setMessages] = useState([]); @@ -17,6 +17,7 @@ const ChatContainerComponent = ({ selectedUser, socket }) => { // Attach 'new_message' event listener when the component mounts useEffect(() => { socket.on('new_message', (message) => { + console.log('message received'); setMessages((prevMessages) => [...prevMessages, message]); }); @@ -26,21 +27,10 @@ const ChatContainerComponent = ({ selectedUser, socket }) => { }; }, [socket]); - // Connect or disconnect the socket based on selectedUser changes - useEffect(() => { - // Connect the socket if not already connected - socket.connect(); - - // Disconnect the socket when the component unmounts - return () => { - socket.disconnect(); - }; - }, [socket]); - // Fetch messages when the selected user changes or the socket changes useEffect(() => { searchMessages(); - }, [socket, selectedUser]); + }, [socket, selectedUser, selectedConnection]); // Function to send a message const sendMessage = async (e) => { @@ -59,7 +49,7 @@ const ChatContainerComponent = ({ selectedUser, socket }) => { }), }); const data = await response.json(); - socket.emit('private_message', data); + socket.emit('private_message', { data, selectedConnection }); setTextInputValue(''); } }; @@ -125,6 +115,7 @@ const ChatContainerComponent = ({ selectedUser, socket }) => { ChatContainerComponent.propTypes = { socket: PropTypes.object.isRequired, selectedUser: PropTypes.object.isRequired, + selectedConnection: PropTypes.object, }; export default ChatContainerComponent; diff --git a/client/src/pages/Home/MessageComponents/ChatMenu.jsx b/client/src/pages/Home/MessageComponents/ChatMenu.jsx index d0ace12..7bf74c3 100644 --- a/client/src/pages/Home/MessageComponents/ChatMenu.jsx +++ b/client/src/pages/Home/MessageComponents/ChatMenu.jsx @@ -6,8 +6,10 @@ import ChatContainerComponent from './ChatContainerComponent'; import ChatInfoButtons from './ChatInfoButtons'; import PropTypes from 'prop-types'; import socket from '../../../socket'; +import { useEffect, useState } from 'react'; -const ChatMenu = ({ selectedUser }) => { +const ChatMenu = ({ selectedUser, selectedConnection }) => { + const [selectedUserStatus, setSelectedUserStatus] = useState(selectedUser.isOnline); function removeHiddenClass() { const element = document.querySelector('.chat-info'); if (element) { @@ -22,15 +24,35 @@ const ChatMenu = ({ selectedUser }) => { document.querySelector('.chat-menu').style.width = '100%'; } } + + useEffect(() => { + socket.on('user_status_changed_chat_menu', ({ userId, isOnline }) => { + console.log(selectedUser); + if (userId === selectedUser._id) { + setSelectedUserStatus(isOnline); + } + }); + return () => { + socket.off('user_status_changed_chat_menu'); + }; + }, [selectedUser.isOnline]); + + useEffect(() => { + setSelectedUserStatus(selectedUser.isOnline); + }, [selectedUser]); return ( <>
- +
+ +
+
{selectedUser === undefined ? 'undefined' : selectedUser.displayName}

{selectedUser === undefined ? 'undefined' : selectedUser.id}

+

@@ -40,7 +62,7 @@ const ChatMenu = ({ selectedUser }) => {
- +
@@ -55,6 +77,8 @@ const ChatMenu = ({ selectedUser }) => { ChatMenu.propTypes = { selectedUser: PropTypes.object, + setSelectedUser: PropTypes.func, + selectedConnection: PropTypes.object, }; export default ChatMenu; diff --git a/client/src/pages/Home/MessageMenu.jsx b/client/src/pages/Home/MessageMenu.jsx index f78904a..efbf9cd 100644 --- a/client/src/pages/Home/MessageMenu.jsx +++ b/client/src/pages/Home/MessageMenu.jsx @@ -4,11 +4,12 @@ import ChatMenu from './MessageComponents/ChatMenu'; const MessageMenu = () => { const [selectedUser, setSelectedUser] = useState({}); + const [selectedConnection, setSelectedConnection] = useState({}); return (
- - + +
); }; diff --git a/client/src/pages/Home/index.jsx b/client/src/pages/Home/index.jsx index 3d50ea2..ad047f2 100644 --- a/client/src/pages/Home/index.jsx +++ b/client/src/pages/Home/index.jsx @@ -2,8 +2,19 @@ import LeftBar from './LeftBar'; import '../pages-styles.scss'; import MessageMenu from './MessageMenu'; import { IsSearchingProvider } from '../../Contexts/IsSearchingContext'; +import socket from '../../socket'; +import { useEffect, useContext } from 'react'; +import UserContext from '../../Contexts/userContext'; -const index = () => { +const Index = () => { + const user = useContext(UserContext); + + useEffect(() => { + socket.connect(); + socket.auth = { user }; + // Remove the event listener on component unmount + return () => socket.disconnect(); + }); return (
@@ -14,4 +25,4 @@ const index = () => { ); }; -export default index; +export default Index; diff --git a/client/src/pages/Home/normalChats.jsx b/client/src/pages/Home/normalChats.jsx index f31056e..f4e194d 100644 --- a/client/src/pages/Home/normalChats.jsx +++ b/client/src/pages/Home/normalChats.jsx @@ -4,7 +4,7 @@ import UserContext from '../../Contexts/userContext'; import PropTypes from 'prop-types'; import socket from '../../socket'; -const NormalChats = ({ setSelectedUser }) => { +const NormalChats = ({ setSelectedUser, setSelectedConnection }) => { // State to store the chat connections const [connectionsArr, setConnectionsArr] = useState([]); @@ -28,6 +28,15 @@ const NormalChats = ({ setSelectedUser }) => { return () => socket.off('connect_error'); }, []); + useEffect(() => { + socket.on('user_status_changed_normal_chats', ({ userId, isOnline }) => { + // Update the user status in your connectionsArr state + setConnectionsArr((prevConnectionsArr) => prevConnectionsArr.map((connection) => (connection.userOne._id === userId || connection.userTwo._id === userId ? { ...connection, userOne: { ...connection.userOne, isOnline }, userTwo: { ...connection.userTwo, isOnline } } : connection))); + }); + // Remove the event listener on component unmount + return () => socket.off('user_status_changed_normal_chats'); + }, []); + // Function to fetch the user's chat connections from the server const searchConnections = async function () { const response = await fetch('http://localhost:3000/connection/searchConnections', { @@ -50,15 +59,11 @@ const NormalChats = ({ setSelectedUser }) => { } return text; } - // Function to handle a click on a chat connection const handleClick = function ({ clickedOnUser, connection }) { - // Disconnect the socket before updating authentication data - socket.disconnect(); - socket.auth = { connection, clickedOnUser }; - // Reconnect the socket with updated authentication data - socket.connect(); // Update the selected user in the parent component + setSelectedConnection(connection); + socket.emit('connection_selected', { connection }); setSelectedUser(clickedOnUser); }; @@ -105,10 +110,14 @@ const NormalChats = ({ setSelectedUser }) => { }} key={i} > - Profile +
+ Profile +
+
+

{truncateText(tmpArr[i].displayName, 18)}

-

default text

+

Default Placeholder

); @@ -127,6 +136,8 @@ const NormalChats = ({ setSelectedUser }) => { // Define the PropTypes for the component NormalChats.propTypes = { setSelectedUser: PropTypes.func.isRequired, + selectedUser: PropTypes.object, + setSelectedConnection: PropTypes.func, }; export default NormalChats; diff --git a/client/src/pages/pages-styles.scss b/client/src/pages/pages-styles.scss index 1e73f84..cf83c13 100644 --- a/client/src/pages/pages-styles.scss +++ b/client/src/pages/pages-styles.scss @@ -96,10 +96,39 @@ .chat { @apply flex p-3 gap-3 border-y-2 border-slate-300 max-h-[5rem] items-center; - + img { @apply w-[3rem] h-[3rem] rounded-full; } + + .image_container { + position: relative; + } + .user-picture { + @apply w-[2.25rem] h-[2.25rem] rounded-full; + } + #status_offline { + position: absolute; + top: 2.1rem; + left: 2rem; + background-color: #828282; + height: 0.8rem; + width: 0.8rem; + border-radius: 50%; + // border-color: #000; + // border-width: 0.15rem; + } + #status_online { + position: absolute; + top: 2.1rem; + left: 2rem; + background-color: #5dfcc3; + height: 0.8rem; + width: 0.8rem; + border-radius: 50%; + // border-color: #000; + // border-width: 0.15rem; + } .contact-name { @apply text-base font-semibold; } @@ -115,9 +144,35 @@ .chat-left { @apply w-[50%] h-[100%] flex items-center px-5; + .image_container { + position: relative; + } .user-picture { @apply w-[2.25rem] h-[2.25rem] rounded-full; } + #status_offline { + position: absolute; + top: 1.5rem; + left: 1.5rem; + background-color: #828282; + height: 0.7rem; + width: 0.7rem; + border-radius: 50%; + // border-color: #000; + // border-width: 0.15rem; + } + #status_online { + position: absolute; + top: 1.5rem; + left: 1.5rem; + background-color: #5dfcc3; + height: 0.7rem; + width: 0.7rem; + border-radius: 50%; + // border-color: #000; + // border-width: 0.15rem; + } + .user-info { @apply px-3; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1c15f11 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "synapse", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "synapse", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/server/config/passport.js b/server/config/passport.js index d57b6e4..430f4c6 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -27,6 +27,7 @@ passport.use( displayName: profile.displayName, googleId: profile.id, provider: issuer, + isOnline: false, }); userCreate.save(); diff --git a/server/controllers/userCRUD.js b/server/controllers/userCRUD.js index 5ca6971..82c0b75 100644 --- a/server/controllers/userCRUD.js +++ b/server/controllers/userCRUD.js @@ -50,8 +50,6 @@ exports.findUser = async function findUser(reference) { * @throws {Error} - If failed to update user. */ exports.updateUser = async function updateUser(_id, updateKeys) { - let user = await findUser({ _id }); - try { const updatedUser = await User.findByIdAndUpdate(_id, updateKeys, { new: true, @@ -59,8 +57,7 @@ exports.updateUser = async function updateUser(_id, updateKeys) { if (!updatedUser) { throw new Error('User not found'); } - user = updatedUser; - return user; + return updatedUser; } catch (error) { throw new Error('Failed to update user'); } diff --git a/server/index.js b/server/index.js index 307eaff..f5eb8af 100644 --- a/server/index.js +++ b/server/index.js @@ -14,6 +14,8 @@ const authRoutes = require('./routes/authRoutes.js'); const messageRoutes = require('./routes/messageRoutes.js'); const googleRoutes = require('./routes/googleAuth.js'); +const { updateUser } = require('./controllers/userCRUD.js'); + // Connect to the MongoDB database connectDatabase(); @@ -94,35 +96,62 @@ app.use('/', googleRoutes); // Socket.IO middleware function for authentication and authorization. io.use((socket, next) => { // Extract authentication data from the handshake object sent by the client. - const { connection, clickedOnUser } = socket.handshake.auth; - - // Check if the 'clickedOnUser' flag exists in the authentication data. - if (!clickedOnUser) { - // If the flag is missing, send an error to the client and abort the connection. - return next(new Error('User Does Not Exist')); - } + const { user } = socket.handshake.auth; // If the user is authenticated, attach the 'connection' data to the socket for later use. - socket.connection = connection; + socket.curUser = user; next(); }); +const activeConnections = {}; // Event listener for a new socket connection. -io.on('connection', (socket) => { +io.on('connection', async (socket) => { // Log the ID of the connected socket. console.log(socket.id); + await updateUser(socket.curUser._id, { isOnline: true }); + + socket.broadcast.emit('user_status_changed_chat_menu', { userId: socket.curUser._id, isOnline: true }); + socket.broadcast.emit('user_status_changed_normal_chats', { userId: socket.curUser._id, isOnline: true }); + + // Function to leave the previous room and join a new one + const updateConnectionRoom = (newConnectionId) => { + const oldConnectionId = activeConnections[socket.id]; + if (oldConnectionId !== newConnectionId) { + if (oldConnectionId) { + socket.leave(oldConnectionId); + // console.log(`Socket ${socket.id} left room ${oldConnectionId}`); + } + socket.join(newConnectionId); + activeConnections[socket.id] = newConnectionId; + // console.log(`Socket ${socket.id} joined room ${newConnectionId}`); + } + }; + // Join a specific room based on the 'connection._id'. - socket.join(socket.connection._id); + + socket.on('connection_selected', (data) => { + updateConnectionRoom(data.connection._id); // Join the new room + }); // Event listener for 'private_message' events from the client. - socket.on('private_message', async (data) => { + socket.on('private_message', async ({ data, selectedConnection }) => { + console.log('message sent'); // Emit the 'new_message' event to all sockets in the same room. - io.to(socket.connection._id).emit('new_message', data); + io.to(selectedConnection._id).emit('new_message', data); }); - // Event listener for 'disconnect' events from the client. socket.on('disconnect', () => { + const connectionId = activeConnections[socket.id]; + if (connectionId) { + if (connectionId) { + socket.leave(connectionId); + delete activeConnections[socket.id]; + } + } + updateUser(socket.curUser._id, { isOnline: false }); + socket.broadcast.emit('user_status_changed_chat_menu', { userId: socket.curUser._id, isOnline: false }); + socket.broadcast.emit('user_status_changed_normal_chats', { userId: socket.curUser._id, isOnline: false }); // Log a message when a user disconnects. console.log('User has disconnected'); }); diff --git a/server/models/userModel.js b/server/models/userModel.js index de9d840..65cf8ed 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -10,7 +10,8 @@ const userSchema = new Schema({ email: { type: String, required: true, unique: true }, // User's email field displayName: { type: String, sparse: true }, googleId: { type: String, required: false, unique: true }, - provider: { type: String, required: false } + provider: { type: String, required: false }, + isOnline: { type: Boolean, required: true }, // imageUrl: String, // Uncomment this line to include an imageUrl field }); diff --git a/server/package.json b/server/package.json index 113e72c..34d7b0b 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "dev": "nodemon --watch '**/*.js' --exec node index.js", + "dev": "nodemon index.js", "test": "jest" }, "type": "commonjs", diff --git a/server/routes/messageRoutes.js b/server/routes/messageRoutes.js index 93c102a..8e2a0e7 100644 --- a/server/routes/messageRoutes.js +++ b/server/routes/messageRoutes.js @@ -17,7 +17,6 @@ messageRouter.post('/', async (req, res) => { // Endpoint to search for messages const messages = await addMessage(req.body); - console.log(messages); res.json(messages); }); diff --git a/server/seeders/userSeed.js b/server/seeders/userSeed.js index 7e76856..cb7cd47 100644 --- a/server/seeders/userSeed.js +++ b/server/seeders/userSeed.js @@ -37,6 +37,7 @@ async function seed() { username, email, displayName, + isOnline: false, }; users.push(newUser);