From 9201e997f633a916770970fddf3219ecbe080a9c Mon Sep 17 00:00:00 2001 From: Ank Date: Wed, 9 Aug 2023 20:58:06 +0600 Subject: [PATCH 1/6] implemented dynamic online status for users --- client/src/pages/Home/DirectAccess.jsx | 5 ++-- .../ChatContainerComponent.jsx | 1 + .../pages/Home/MessageComponents/ChatMenu.jsx | 1 + client/src/pages/Home/normalChats.jsx | 27 ++++++++++++++----- package-lock.json | 13 +++++++++ server/config/passport.js | 1 + server/controllers/userCRUD.js | 5 +--- server/index.js | 27 ++++++++++++++----- server/models/userModel.js | 3 ++- server/routes/messageRoutes.js | 1 - server/seeders/userSeed.js | 1 + 11 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 package-lock.json diff --git a/client/src/pages/Home/DirectAccess.jsx b/client/src/pages/Home/DirectAccess.jsx index 056cbe7..db42cbf 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 }) => { 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,14 @@ const DirectAccess = ({ setSelectedUser }) => { placeholder={placeholderValue} /> - {isSearching ? : } + {isSearching ? : } ); }; DirectAccess.propTypes = { setSelectedUser: PropTypes.func.isRequired, + selectedUser: PropTypes.object, }; export default DirectAccess; diff --git a/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx b/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx index 215fe4d..7efca6c 100644 --- a/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx +++ b/client/src/pages/Home/MessageComponents/ChatContainerComponent.jsx @@ -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]); }); diff --git a/client/src/pages/Home/MessageComponents/ChatMenu.jsx b/client/src/pages/Home/MessageComponents/ChatMenu.jsx index d0ace12..dec65b3 100644 --- a/client/src/pages/Home/MessageComponents/ChatMenu.jsx +++ b/client/src/pages/Home/MessageComponents/ChatMenu.jsx @@ -31,6 +31,7 @@ const ChatMenu = ({ selectedUser }) => {
{selectedUser === undefined ? 'undefined' : selectedUser.displayName}

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

+

{selectedUser?.isOnline ? 'Online' : 'Offline'}

diff --git a/client/src/pages/Home/normalChats.jsx b/client/src/pages/Home/normalChats.jsx index f31056e..43bbc31 100644 --- a/client/src/pages/Home/normalChats.jsx +++ b/client/src/pages/Home/normalChats.jsx @@ -4,9 +4,11 @@ import UserContext from '../../Contexts/userContext'; import PropTypes from 'prop-types'; import socket from '../../socket'; -const NormalChats = ({ setSelectedUser }) => { +const NormalChats = ({ setSelectedUser, selectedUser }) => { // State to store the chat connections const [connectionsArr, setConnectionsArr] = useState([]); + const [selectedConnection, setSelectedConnection] = useState({}); + // const [friendOnlineRerenderTriggeer, setFriendOnlineRerenderTriggeer] = useState(false); // Access the user data from the UserContext const user = useContext(UserContext); @@ -51,14 +53,24 @@ const NormalChats = ({ setSelectedUser }) => { return text; } + useEffect(() => { + socket.connect(); + socket.emit('user_connected'); + // setFriendOnlineRerenderTriggeer(!friendOnlineRerenderTriggeer); + socket.auth = { selectedUser, user, selectedConnection }; + // Remove the event listener on component unmount + return () => socket.disconnect(); + }); + // useEffect(() => { + // socket.emit('user_connected'); + // // Remove the event listener on component unmount + // return () => socket.off('user_connected'); + // }, [friendOnlineRerenderTriggeer]); + // 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); setSelectedUser(clickedOnUser); }; @@ -108,7 +120,7 @@ const NormalChats = ({ setSelectedUser }) => { Profile

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

-

default text

+

{tmpArr[i].isOnline ? 'Online' : 'Offline'}

); @@ -127,6 +139,7 @@ const NormalChats = ({ setSelectedUser }) => { // Define the PropTypes for the component NormalChats.propTypes = { setSelectedUser: PropTypes.func.isRequired, + selectedUser: PropTypes.object, }; export default NormalChats; 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..a5aa849 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,46 @@ 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; + const { selectedUser, user, selectedConnection } = socket.handshake.auth; - // Check if the 'clickedOnUser' flag exists in the authentication data. - if (!clickedOnUser) { + // Check if the 'selectedUser' flag exists in the authentication data. + if (!selectedUser) { // If the flag is missing, send an error to the client and abort the connection. return next(new Error('User Does Not Exist')); } // If the user is authenticated, attach the 'connection' data to the socket for later use. - socket.connection = connection; + socket.curUser = user; + socket.selectedConnection = selectedConnection; next(); }); // 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); + console.log(socket.curUser); + + const updatedUser = await updateUser(socket.curUser._id, { isOnline: true }); + console.log(updatedUser); // Join a specific room based on the 'connection._id'. - socket.join(socket.connection._id); + + socket.join(socket.selectedConnection?._id); // Event listener for 'private_message' events from the client. socket.on('private_message', async (data) => { + 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(socket.selectedConnection._id).emit('new_message', data); + }); + socket.on('user_connected', async (data) => { + io.emit('user_connected'); }); // Event listener for 'disconnect' events from the client. socket.on('disconnect', () => { + updateUser(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/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); From 7c5109390ce0492e705a8c90e9d3ff1741b11791 Mon Sep 17 00:00:00 2001 From: Ank Date: Sat, 12 Aug 2023 23:52:19 +0600 Subject: [PATCH 2/6] removed some unnecessary code --- client/src/pages/Home/normalChats.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/pages/Home/normalChats.jsx b/client/src/pages/Home/normalChats.jsx index 43bbc31..72d9633 100644 --- a/client/src/pages/Home/normalChats.jsx +++ b/client/src/pages/Home/normalChats.jsx @@ -8,7 +8,6 @@ const NormalChats = ({ setSelectedUser, selectedUser }) => { // State to store the chat connections const [connectionsArr, setConnectionsArr] = useState([]); const [selectedConnection, setSelectedConnection] = useState({}); - // const [friendOnlineRerenderTriggeer, setFriendOnlineRerenderTriggeer] = useState(false); // Access the user data from the UserContext const user = useContext(UserContext); @@ -56,7 +55,6 @@ const NormalChats = ({ setSelectedUser, selectedUser }) => { useEffect(() => { socket.connect(); socket.emit('user_connected'); - // setFriendOnlineRerenderTriggeer(!friendOnlineRerenderTriggeer); socket.auth = { selectedUser, user, selectedConnection }; // Remove the event listener on component unmount return () => socket.disconnect(); From 5c492b7ede7bdd3396bb166a68b79dcb09f9fa80 Mon Sep 17 00:00:00 2001 From: Ank Date: Sat, 12 Aug 2023 23:53:20 +0600 Subject: [PATCH 3/6] more unnecessary code removal --- client/src/pages/Home/normalChats.jsx | 7 ------- server/index.js | 7 +------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/client/src/pages/Home/normalChats.jsx b/client/src/pages/Home/normalChats.jsx index 72d9633..0b90f72 100644 --- a/client/src/pages/Home/normalChats.jsx +++ b/client/src/pages/Home/normalChats.jsx @@ -54,17 +54,10 @@ const NormalChats = ({ setSelectedUser, selectedUser }) => { useEffect(() => { socket.connect(); - socket.emit('user_connected'); socket.auth = { selectedUser, user, selectedConnection }; // Remove the event listener on component unmount return () => socket.disconnect(); }); - // useEffect(() => { - // socket.emit('user_connected'); - // // Remove the event listener on component unmount - // return () => socket.off('user_connected'); - // }, [friendOnlineRerenderTriggeer]); - // Function to handle a click on a chat connection const handleClick = function ({ clickedOnUser, connection }) { // Update the selected user in the parent component diff --git a/server/index.js b/server/index.js index a5aa849..024e7fa 100644 --- a/server/index.js +++ b/server/index.js @@ -114,10 +114,8 @@ io.use((socket, next) => { io.on('connection', async (socket) => { // Log the ID of the connected socket. console.log(socket.id); - console.log(socket.curUser); - const updatedUser = await updateUser(socket.curUser._id, { isOnline: true }); - console.log(updatedUser); + await updateUser(socket.curUser._id, { isOnline: true }); // Join a specific room based on the 'connection._id'. @@ -129,9 +127,6 @@ io.on('connection', async (socket) => { // Emit the 'new_message' event to all sockets in the same room. io.to(socket.selectedConnection._id).emit('new_message', data); }); - socket.on('user_connected', async (data) => { - io.emit('user_connected'); - }); // Event listener for 'disconnect' events from the client. socket.on('disconnect', () => { From e0ce54fe23de8a1e297cf9d20df223732b5fc12e Mon Sep 17 00:00:00 2001 From: Ank Date: Sun, 13 Aug 2023 11:55:36 +0600 Subject: [PATCH 4/6] Fixed nodemon and users not joining room properly --- client/src/App.jsx | 2 +- client/src/pages/Home/DirectAccess.jsx | 5 +-- .../ChatContainerComponent.jsx | 18 +++-------- .../pages/Home/MessageComponents/ChatMenu.jsx | 18 +++++++++-- client/src/pages/Home/MessageMenu.jsx | 5 +-- client/src/pages/Home/index.jsx | 15 +++++++-- client/src/pages/Home/normalChats.jsx | 22 +++++++------ server/index.js | 32 ++++++++++++------- server/package.json | 2 +- 9 files changed, 74 insertions(+), 45 deletions(-) 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 db42cbf..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, selectedUser }) => { +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,7 +45,7 @@ const DirectAccess = ({ setSelectedUser, selectedUser }) => { placeholder={placeholderValue} /> - {isSearching ? : } + {isSearching ? : } ); }; @@ -53,6 +53,7 @@ const DirectAccess = ({ setSelectedUser, selectedUser }) => { 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 7efca6c..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([]); @@ -27,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) => { @@ -60,7 +49,7 @@ const ChatContainerComponent = ({ selectedUser, socket }) => { }), }); const data = await response.json(); - socket.emit('private_message', data); + socket.emit('private_message', { data, selectedConnection }); setTextInputValue(''); } }; @@ -126,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 dec65b3..ef1f8ee 100644 --- a/client/src/pages/Home/MessageComponents/ChatMenu.jsx +++ b/client/src/pages/Home/MessageComponents/ChatMenu.jsx @@ -6,8 +6,9 @@ import ChatContainerComponent from './ChatContainerComponent'; import ChatInfoButtons from './ChatInfoButtons'; import PropTypes from 'prop-types'; import socket from '../../../socket'; +import { useEffect } from 'react'; -const ChatMenu = ({ selectedUser }) => { +const ChatMenu = ({ selectedUser, setSelectedUser, selectedConnection }) => { function removeHiddenClass() { const element = document.querySelector('.chat-info'); if (element) { @@ -22,6 +23,17 @@ const ChatMenu = ({ selectedUser }) => { document.querySelector('.chat-menu').style.width = '100%'; } } + useEffect(() => { + socket.on('user_status_changed', ({ isOnline }) => { + setSelectedUser((prevUser) => ({ + ...prevUser, // Copy all existing properties + isOnline: isOnline, // Update isOnline property + })); + // Update the user status in your connectionsArr state + }); + // Remove the event listener on component unmount + return () => socket.off('user_status_changed'); + }, []); return ( <>
@@ -41,7 +53,7 @@ const ChatMenu = ({ selectedUser }) => {
- +
@@ -56,6 +68,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 0b90f72..35b1262 100644 --- a/client/src/pages/Home/normalChats.jsx +++ b/client/src/pages/Home/normalChats.jsx @@ -4,10 +4,9 @@ import UserContext from '../../Contexts/userContext'; import PropTypes from 'prop-types'; import socket from '../../socket'; -const NormalChats = ({ setSelectedUser, selectedUser }) => { +const NormalChats = ({ setSelectedUser, setSelectedConnection }) => { // State to store the chat connections const [connectionsArr, setConnectionsArr] = useState([]); - const [selectedConnection, setSelectedConnection] = useState({}); // Access the user data from the UserContext const user = useContext(UserContext); @@ -29,6 +28,15 @@ const NormalChats = ({ setSelectedUser, selectedUser }) => { return () => socket.off('connect_error'); }, []); + useEffect(() => { + socket.on('user_status_changed', ({ 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'); + }, []); + // Function to fetch the user's chat connections from the server const searchConnections = async function () { const response = await fetch('http://localhost:3000/connection/searchConnections', { @@ -51,17 +59,12 @@ const NormalChats = ({ setSelectedUser, selectedUser }) => { } return text; } - - useEffect(() => { - socket.connect(); - socket.auth = { selectedUser, user, selectedConnection }; - // Remove the event listener on component unmount - return () => socket.disconnect(); - }); // Function to handle a click on a chat connection const handleClick = function ({ clickedOnUser, connection }) { // Update the selected user in the parent component setSelectedConnection(connection); + console.log(connection); + socket.emit('connection_selected', { connection }); setSelectedUser(clickedOnUser); }; @@ -131,6 +134,7 @@ const NormalChats = ({ setSelectedUser, selectedUser }) => { NormalChats.propTypes = { setSelectedUser: PropTypes.func.isRequired, selectedUser: PropTypes.object, + setSelectedConnection: PropTypes.func, }; export default NormalChats; diff --git a/server/index.js b/server/index.js index 024e7fa..dce985c 100644 --- a/server/index.js +++ b/server/index.js @@ -96,20 +96,14 @@ 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 { selectedUser, user, selectedConnection } = socket.handshake.auth; - - // Check if the 'selectedUser' flag exists in the authentication data. - if (!selectedUser) { - // 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.curUser = user; - socket.selectedConnection = selectedConnection; next(); }); +const activeConnections = {}; // Event listener for a new socket connection. io.on('connection', async (socket) => { // Log the ID of the connected socket. @@ -117,20 +111,34 @@ io.on('connection', async (socket) => { await updateUser(socket.curUser._id, { isOnline: true }); + socket.broadcast.emit('user_status_changed', { userId: socket.curUser._id, isOnline: true }); + // Join a specific room based on the 'connection._id'. - socket.join(socket.selectedConnection?._id); + socket.on('connection_selected', (data) => { + activeConnections[socket.id] = data.connection._id; + socket.join(data.connection._id); + }); - // 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.selectedConnection._id).emit('new_message', data); + io.to(selectedConnection._id).emit('new_message', data); }); + // Event listener for 'private_message' events from the client. + // 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', { userId: socket.curUser._id, isOnline: false }); // Log a message when a user disconnects. console.log('User has disconnected'); }); 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", From 9da271f9458ed3af3cd0d470ae6a761decdb0a94 Mon Sep 17 00:00:00 2001 From: Ank Date: Mon, 14 Aug 2023 00:02:28 +0600 Subject: [PATCH 5/6] Implemented Online status functionality, last seen, fixed nodemon --- .../pages/Home/MessageComponents/ChatMenu.jsx | 40 +++++++++++++++++-- client/src/pages/Home/normalChats.jsx | 1 - server/index.js | 25 ++++++++---- server/models/userModel.js | 1 + server/seeders/userSeed.js | 1 + 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/client/src/pages/Home/MessageComponents/ChatMenu.jsx b/client/src/pages/Home/MessageComponents/ChatMenu.jsx index ef1f8ee..11bddcb 100644 --- a/client/src/pages/Home/MessageComponents/ChatMenu.jsx +++ b/client/src/pages/Home/MessageComponents/ChatMenu.jsx @@ -6,9 +6,10 @@ import ChatContainerComponent from './ChatContainerComponent'; import ChatInfoButtons from './ChatInfoButtons'; import PropTypes from 'prop-types'; import socket from '../../../socket'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; const ChatMenu = ({ selectedUser, setSelectedUser, selectedConnection }) => { + const [lastSeenString, setLastSeenString] = useState(''); function removeHiddenClass() { const element = document.querySelector('.chat-info'); if (element) { @@ -23,17 +24,48 @@ const ChatMenu = ({ selectedUser, setSelectedUser, selectedConnection }) => { document.querySelector('.chat-menu').style.width = '100%'; } } + function getOfflineDuration() { + const lastOnlineTime = new Date(selectedUser.lastOnlineTimestamp); + const currentTime = new Date(); + const offlineDuration = currentTime - lastOnlineTime; + // Calculate duration in a human-readable format (e.g., hours, minutes) + if (offlineDuration < 60000) { + // Less than 1 minute + setLastSeenString('Last seen just now'); + } else if (offlineDuration < 3600000) { + // Less than 1 hour + const minutesAgo = Math.floor(offlineDuration / 60000); + setLastSeenString(`Last seen ${minutesAgo} minutes ago`); + } else if (lastOnlineTime.toDateString() === currentTime.toDateString()) { + // Same day + const hoursAgo = Math.floor(offlineDuration / 3600000); + setLastSeenString(`Last seen ${hoursAgo} hours ago`); + } else if (lastOnlineTime.getDate() === currentTime.getDate() - 1) { + // Yesterday + setLastSeenString(`Last seen yesterday at ${lastOnlineTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`); + } else if (lastOnlineTime.getFullYear() === currentTime.getFullYear()) { + // Same year + setLastSeenString(`Last seen ${lastOnlineTime.toLocaleDateString([], { weekday: 'long' })} at ${lastOnlineTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`); + } else { + setLastSeenString(lastOnlineTime.toLocaleString()); // Default to full date and time + } + } useEffect(() => { socket.on('user_status_changed', ({ isOnline }) => { setSelectedUser((prevUser) => ({ - ...prevUser, // Copy all existing properties - isOnline: isOnline, // Update isOnline property + ...prevUser, + isOnline, + lastOnlineTimestamp: Date.now(), // Set to null when online, and actual timestamp when offline })); // Update the user status in your connectionsArr state }); // Remove the event listener on component unmount return () => socket.off('user_status_changed'); }, []); + useEffect(() => { + getOfflineDuration(); + // Remove the event listener on component unmount + }, [selectedUser]); return ( <>
@@ -43,7 +75,7 @@ const ChatMenu = ({ selectedUser, setSelectedUser, selectedConnection }) => {
{selectedUser === undefined ? 'undefined' : selectedUser.displayName}

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

-

{selectedUser?.isOnline ? 'Online' : 'Offline'}

+

{selectedUser.isOnline ? 'Online' : `${lastSeenString}`}

{' '}
diff --git a/client/src/pages/Home/normalChats.jsx b/client/src/pages/Home/normalChats.jsx index 35b1262..ebe7fe1 100644 --- a/client/src/pages/Home/normalChats.jsx +++ b/client/src/pages/Home/normalChats.jsx @@ -63,7 +63,6 @@ const NormalChats = ({ setSelectedUser, setSelectedConnection }) => { const handleClick = function ({ clickedOnUser, connection }) { // Update the selected user in the parent component setSelectedConnection(connection); - console.log(connection); socket.emit('connection_selected', { connection }); setSelectedUser(clickedOnUser); }; diff --git a/server/index.js b/server/index.js index dce985c..6668371 100644 --- a/server/index.js +++ b/server/index.js @@ -109,25 +109,36 @@ io.on('connection', async (socket) => { // Log the ID of the connected socket. console.log(socket.id); - await updateUser(socket.curUser._id, { isOnline: true }); + await updateUser(socket.curUser._id, { isOnline: true, lastOnlineTimestamp: Date.now() }); socket.broadcast.emit('user_status_changed', { 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.on('connection_selected', (data) => { - activeConnections[socket.id] = data.connection._id; - socket.join(data.connection._id); + updateConnectionRoom(data.connection._id); // Join the new room }); + // Event listener for 'private_message' events from the client. 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(selectedConnection._id).emit('new_message', data); }); - - // Event listener for 'private_message' events from the client. - // Event listener for 'disconnect' events from the client. socket.on('disconnect', () => { const connectionId = activeConnections[socket.id]; @@ -137,7 +148,7 @@ io.on('connection', async (socket) => { delete activeConnections[socket.id]; } } - updateUser(socket.curUser._id, { isOnline: false }); + updateUser(socket.curUser._id, { isOnline: false, lastOnlineTimestamp: Date.now() }); socket.broadcast.emit('user_status_changed', { 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 65cf8ed..6f3b2de 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -12,6 +12,7 @@ const userSchema = new Schema({ googleId: { type: String, required: false, unique: true }, provider: { type: String, required: false }, isOnline: { type: Boolean, required: true }, + lastOnlineTimestamp: { type: Date }, // imageUrl: String, // Uncomment this line to include an imageUrl field }); diff --git a/server/seeders/userSeed.js b/server/seeders/userSeed.js index cb7cd47..e7607f1 100644 --- a/server/seeders/userSeed.js +++ b/server/seeders/userSeed.js @@ -38,6 +38,7 @@ async function seed() { email, displayName, isOnline: false, + lastOnlineTimestamp: 0, }; users.push(newUser); From ad164cfbaa15bc561ecef25f9ce1dc00122c5fe3 Mon Sep 17 00:00:00 2001 From: Ank Date: Mon, 14 Aug 2023 22:31:09 +0600 Subject: [PATCH 6/6] removed last seen. Added styles to online status indicator --- .../pages/Home/MessageComponents/ChatMenu.jsx | 61 ++++++------------- client/src/pages/Home/normalChats.jsx | 12 ++-- client/src/pages/pages-styles.scss | 57 ++++++++++++++++- server/index.js | 10 +-- server/models/userModel.js | 1 - server/seeders/userSeed.js | 1 - 6 files changed, 89 insertions(+), 53 deletions(-) diff --git a/client/src/pages/Home/MessageComponents/ChatMenu.jsx b/client/src/pages/Home/MessageComponents/ChatMenu.jsx index 11bddcb..7bf74c3 100644 --- a/client/src/pages/Home/MessageComponents/ChatMenu.jsx +++ b/client/src/pages/Home/MessageComponents/ChatMenu.jsx @@ -8,8 +8,8 @@ import PropTypes from 'prop-types'; import socket from '../../../socket'; import { useEffect, useState } from 'react'; -const ChatMenu = ({ selectedUser, setSelectedUser, selectedConnection }) => { - const [lastSeenString, setLastSeenString] = useState(''); +const ChatMenu = ({ selectedUser, selectedConnection }) => { + const [selectedUserStatus, setSelectedUserStatus] = useState(selectedUser.isOnline); function removeHiddenClass() { const element = document.querySelector('.chat-info'); if (element) { @@ -24,58 +24,35 @@ const ChatMenu = ({ selectedUser, setSelectedUser, selectedConnection }) => { document.querySelector('.chat-menu').style.width = '100%'; } } - function getOfflineDuration() { - const lastOnlineTime = new Date(selectedUser.lastOnlineTimestamp); - const currentTime = new Date(); - const offlineDuration = currentTime - lastOnlineTime; - // Calculate duration in a human-readable format (e.g., hours, minutes) - if (offlineDuration < 60000) { - // Less than 1 minute - setLastSeenString('Last seen just now'); - } else if (offlineDuration < 3600000) { - // Less than 1 hour - const minutesAgo = Math.floor(offlineDuration / 60000); - setLastSeenString(`Last seen ${minutesAgo} minutes ago`); - } else if (lastOnlineTime.toDateString() === currentTime.toDateString()) { - // Same day - const hoursAgo = Math.floor(offlineDuration / 3600000); - setLastSeenString(`Last seen ${hoursAgo} hours ago`); - } else if (lastOnlineTime.getDate() === currentTime.getDate() - 1) { - // Yesterday - setLastSeenString(`Last seen yesterday at ${lastOnlineTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`); - } else if (lastOnlineTime.getFullYear() === currentTime.getFullYear()) { - // Same year - setLastSeenString(`Last seen ${lastOnlineTime.toLocaleDateString([], { weekday: 'long' })} at ${lastOnlineTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`); - } else { - setLastSeenString(lastOnlineTime.toLocaleString()); // Default to full date and time - } - } + useEffect(() => { - socket.on('user_status_changed', ({ isOnline }) => { - setSelectedUser((prevUser) => ({ - ...prevUser, - isOnline, - lastOnlineTimestamp: Date.now(), // Set to null when online, and actual timestamp when offline - })); - // Update the user status in your connectionsArr state + socket.on('user_status_changed_chat_menu', ({ userId, isOnline }) => { + console.log(selectedUser); + if (userId === selectedUser._id) { + setSelectedUserStatus(isOnline); + } }); - // Remove the event listener on component unmount - return () => socket.off('user_status_changed'); - }, []); + return () => { + socket.off('user_status_changed_chat_menu'); + }; + }, [selectedUser.isOnline]); + useEffect(() => { - getOfflineDuration(); - // Remove the event listener on component unmount + setSelectedUserStatus(selectedUser.isOnline); }, [selectedUser]); return ( <>
- +
+ +
+
{selectedUser === undefined ? 'undefined' : selectedUser.displayName}

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

-

{selectedUser.isOnline ? 'Online' : `${lastSeenString}`}

{' '} +

diff --git a/client/src/pages/Home/normalChats.jsx b/client/src/pages/Home/normalChats.jsx index ebe7fe1..f4e194d 100644 --- a/client/src/pages/Home/normalChats.jsx +++ b/client/src/pages/Home/normalChats.jsx @@ -29,12 +29,12 @@ const NormalChats = ({ setSelectedUser, setSelectedConnection }) => { }, []); useEffect(() => { - socket.on('user_status_changed', ({ userId, isOnline }) => { + 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'); + return () => socket.off('user_status_changed_normal_chats'); }, []); // Function to fetch the user's chat connections from the server @@ -110,10 +110,14 @@ const NormalChats = ({ setSelectedUser, setSelectedConnection }) => { }} key={i} > - Profile +
+ Profile +
+
+

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

-

{tmpArr[i].isOnline ? 'Online' : 'Offline'}

+

Default Placeholder

); 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/server/index.js b/server/index.js index 6668371..f5eb8af 100644 --- a/server/index.js +++ b/server/index.js @@ -109,9 +109,10 @@ io.on('connection', async (socket) => { // Log the ID of the connected socket. console.log(socket.id); - await updateUser(socket.curUser._id, { isOnline: true, lastOnlineTimestamp: Date.now() }); + await updateUser(socket.curUser._id, { isOnline: true }); - socket.broadcast.emit('user_status_changed', { userId: 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) => { @@ -148,8 +149,9 @@ io.on('connection', async (socket) => { delete activeConnections[socket.id]; } } - updateUser(socket.curUser._id, { isOnline: false, lastOnlineTimestamp: Date.now() }); - socket.broadcast.emit('user_status_changed', { userId: socket.curUser._id, isOnline: false }); + 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 6f3b2de..65cf8ed 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -12,7 +12,6 @@ const userSchema = new Schema({ googleId: { type: String, required: false, unique: true }, provider: { type: String, required: false }, isOnline: { type: Boolean, required: true }, - lastOnlineTimestamp: { type: Date }, // imageUrl: String, // Uncomment this line to include an imageUrl field }); diff --git a/server/seeders/userSeed.js b/server/seeders/userSeed.js index e7607f1..cb7cd47 100644 --- a/server/seeders/userSeed.js +++ b/server/seeders/userSeed.js @@ -38,7 +38,6 @@ async function seed() { email, displayName, isOnline: false, - lastOnlineTimestamp: 0, }; users.push(newUser);