Private chat using MERN stack and socket.io - node.js

I'm developing a private chat application using MERN stack and socket.io.
I'm able to send a private message to a specific user successfully but not able to include the message sender to show to both the users.
Client
export default function Chat({ users }) {
const [activeUser, setActiveUser] = useState('');
const [currentUser, setCurrentUser] = useState(null);
const [filteredList, setFilteredList] = useState([]);
const [message, setMessage] = useState('');
const [chats, setChats] = useState([]);
useEffect(() => {
const jwt = isAuthenticated().user;
setCurrentUser(jwt);
config.socket;
config.socket.on('isOnline', (data) => {
console.log(data);
});
}, []);
useEffect(() => {
const list = users.filter(user => currentUser ? user._id !== currentUser._id : user._id);
setFilteredList(list);
}, [currentUser]);
useEffect(() => {
config.socket.on('chat', (data) => {
setChats([...chats, data]);
});
}, []);
const changeActiveUser = (e, user) => {
e.preventDefault();
console.log(`changeActiveUser ${JSON.stringify(user)}`);
setActiveUser(user);
};
const handleClick = e => {
e.preventDefault();
const msg = {
socketId: activeUser.socketId,
to: activeUser._id,
toUser: activeUser.username,
message: message,
from: currentUser._id
}
// config.socket.emit('join', {id: activeUser.socketId, username: activeUser.username})
console.log(`Private MSG ${JSON.stringify(msg)}`);
config.socket.emit('private', msg);
setMessage('');
}
return (
<>
<div className='container'>
<div className='row'>
<div className='col-3 leftSide'>
{filteredList.map((user) => (
<li key={user._id} className={`list ${user._id === activeUser._id ? 'active' : ''}`} onClick={e => changeActiveUser(e, user)}>
<img className='userImg' src={user.gender === 'Female' ? '/female.jpg' : '/male.jpg'} /> {user.name} <small> ({user.username}) <span>{user.online ? 'online' : 'offline'}</span></small>
</li>
))}
</div>
<div className='col-9 rightSide'>
<div className='row'>
<div className='col rightTop'>
<h1>{activeUser.username ? `${activeUser.username} (${activeUser._id})` : 'Start a chat!'}</h1>
<ul>
{chats && chats.map((chat, i) => (
<li key={i}>
<span>{activeUser._id === chat.from ? <span style={{ float: 'left' }}>{`${activeUser.username}: ${chat.message}`}</span> : <span style={{ float: 'right' }}>{`Me: ${chat.message}`}</span>}</span>
{/* <div>{activeUser._id === chat.to ? <div style={{ float: 'right' }}>{`Me: ${chat.message}`}</div> : ''}</div> */}
</li>
))}
</ul>
</div>
</div>
<hr />
{activeUser && (
<div className='row rightBottomContainer'>
<div className='rightBottomInput'>
<input value={message} type='text' onChange={(e) => setMessage(e.target.value)} placeholder=' Enter your chat...' />
</div>
<div className='rightBottomButton'>
<button onClick={(e) => handleClick(e)} type='submit'>Send</button>
</div>
</div>
)}
</div>
</div>
</div>
</>
);
}
Server
const users = {};
io.on('connection', (socket) => {
console.log(`${socket.id} connected`);
socket.on('signin', (data) => {
users[data.username] = data.socketId;
socket.emit('isOnline', { userId: data.userId, online: true });
});
console.log(`USERS: ${JSON.stringify(users)}`);
// socket.on('join', (data) => {
// console.log(`JOIN: ${JSON.stringify(data)}`);
// socket.join(data.id);
// });
socket.on('private', (msg) => {
console.log(`users[msg.toUser] ${users[msg.toUser]} === msg.socketId ${msg.socketId}`);
if (users[msg.toUser] === msg.socketId) {
console.log(msg);
io.to(msg.socketId).emit('chat', msg);
}
});
socket.on('disconnect', () => {
console.log(`${socket.id} disconnected`);
});
});
I need help to include the sender along with the message to show to both the users. And I also want to append the messages properly since the above code is overriding the previous message.

You can create a room with 2 users in it. every time you want to start a private chat. I just use something like the following code to implement this.
Let's assume that we have a user with id = 1 that wants to send a message to a user with id = 2. so we create a room names 1-2 so when the first user wants to send a message to the second user, you have to add 1-2 to the query when you initializing the socket.io in the client like this:
socket = socketIOClient(ENDPOINT, {
query: {
room: "1-2",
},
});
now you know on the server that a user wants to connect to which room. so you can add the room to your socket using this code:
io.use(async function(socket, next) {
socket.room = socket.handshake.query.room;
return next();
}
So now you can use room feature of socket.io to send the message to both of sender and receiver like this:
socket.on('private', (msg) => {
socket.broadcast
.to(socket.room)
.emit("chat", msg)
});
the whole server code will be something like this:
io.use(function(socket, next) {
socket.room = socket.handshake.query.room;
return next();
})
.on("connection", function(socket) {
socket.on('private', (msg) => {
socket.broadcast
.to(socket.room)
.emit("chat", msg)
});
}

Related

how to paginate through user notifications without mixing them up?

i'm working on a MERN app where the user can receive notifications on certain events, the user can then open the notification page where he can view his notifications, the issue that i have is the notifications have a "seen" state, the notifications that aren't seen will always be on top of the notification results..but say the user clicks on a notification which sets it as seen and then paginates further down the notification list by clicking on "show more" btn, he will find that earlier notification waiting for him in the bottom, basically it's duplicating..how can i fix this?
express controller for notifications
router.get("/", verifyUser, (req, res) => {
let userId = res.locals.user._id; //from the verify user MW, unrelated to question
let pageNum = req.query.page;
const options = {
page: pageNum,
limit: 8,
sort: { seen: 1, createdAt: -1 }
};
Notification.paginate({ receiver: userId }, options, (err, result) => {
if (err) {
console.log(err);
res.sendStatus(400);
} else {
res.status(200).json(result);
}
})
})
react
function Notifications(){
let { texts } = useContext(LangContext);
let [notificationsList, setNotificationsList] = useState([]);
let [isPending, setIsPending] = useState(true);
let [notiPage, setNotiPage] = useState(1);
let [hasNext, setHasNext] = useState(false);
function getNotifications() {
axios.get(`/api/notification?page=${notiPage}`).then(result => {
console.log(result);
setNotificationsList([...notificationsList, ...result.data.docs]);
if (result.data.hasNextPage) {
setNotiPage(result.data.nextPage);
setHasNext(true);
} else {
setHasNext(false);
}
setIsPending(false);
}).catch(err => {
console.log(err);
setIsPending(false);
})
}
//update notification seen status on the backend as well as on the front end
function toggleNotificationView(id, seen) {
let viewStatus = !seen;
axios.put(`/api/notification/${id}`, { seen: viewStatus }).then(result => {
console.log(result);
let notiArray = [...notificationsList];
let index = notiArray.findIndex(noti => {
return noti._id == id;
})
let editedNoti = notiArray[index];
editedNoti.seen = viewStatus;
notiArray.splice(index, 1, editedNoti);
setNotificationsList(notiArray);
}).catch(err => {
console.log(err);
})
}
return <div>
<div className="d-flex flex-column justify-content-center align-content-center my-3">
{notificationsList.map(notification => {
return (<div key={notification._id} id={notification._id} className="w-70 mx-auto">
<div className={`d-flex justify-content-between ${!notification.seen ? "new-notification" : ""}`}>
<a className="text-decoration-none text-dark" href={notification.srcInfo.link}>
<div className={`d-flex justify-content-start align-items-center`}>
<img src={notificationBell} width={100} height={100} />
<p className="text-start fs-4 mx-4" >{parseNotification(notification)}</p>
</div>
</a>
<div className="noti-dropdown noselect align-self-start">
<i onClick={toggleDropDown} className="bi bi-three-dots-vertical fs-4"></i>
<ul className="noti-dropdown-options px-0 fs-5">
<li onClick={() => { deleteNotification(notification._id) }}>{texts.deleteNotification}</li>
<li onClick={() => { toggleNotificationView(notification._id, notification.seen) }}> {notification.seen ? texts.markNotificationNotSeen : texts.markNotificationSeen}</li>
</ul>
</div>
</div>
<hr className="text-main" />
</div>)
})}
</div>
{hasNext && <h4 onClick={getNotifications} className="mx-auto text-center text-main text-decoration-underline"><span className="cursor-pointer">{texts.notificationShowMore}</span></h4>}
</div>
}
the problem is happening because i'm sorting by seen while paginating, what can i do about this?

socket io server connected = true, but client connected = false

iam new to the websocket, and i'm currently wanting to create a chat app using socket.io
i follow the documentation on the socket.io website and successfully connect the server to the client
but as i want to emit from the server to the client or vise versa i found out that my client is not connected to the server as the property disconnected = true
and my guess is that because of this i can't use the emit event
can someone explain whats going on?
note: both using version 2.2.0 of socket.io
here is the screenshot of whats happening
server
and the code of the server
const app = require('express')()
const http = require('http').createServer(app)
const io = require('socket.io')(http)
io.on('connection', socket => {
console.log(socket.connected)
socket.on('message', ({ name, message }) => {
console.log(name)
console.log(message)
io.emit('message', { name, message })
})
})
http.listen(4000, function () {
console.log('listening on port 4000')
})
and here is the screenshot of the client
client
and here is the code of the client
import React, { useEffect, useRef, useState } from "react"
import io from "socket.io-client"
import "./App.css"
function App() {
const [state, setState] = useState({ message: "", name: "" })
const [chat, setChat] = useState([])
const socketRef = useRef()
useEffect(
() => {
socketRef.current = io("http://127.0.0.1:4000")
socketRef.current.connect()
console.log(socketRef.current)
socketRef.current.on('connect', function () {
console.log("masuk")
});
socketRef.current.on("message", ({ name, message }) => {
setChat([...chat, { name, message }])
})
return () => socketRef.current.disconnect()
},
[chat]
)
const onTextChange = (e) => {
setState({ ...state, [e.target.name]: e.target.value })
}
const onMessageSubmit = (e) => {
const { name, message } = state
console.log(name)
console.log(message)
socketRef.current.emit("message", { name, message })
e.preventDefault()
setState({ message: "", name })
}
const renderChat = () => {
return chat.map(({ name, message }, index) => (
<div key={index}>
<h3>
{name}: <span>{message}</span>
</h3>
</div>
))
}
return (
<div className="card">
<form onSubmit={onMessageSubmit}>
<h1>Messenger</h1>
<div className="name-field">
<TextField name="name" onChange={(e) => onTextChange(e)} value={state.name} label="Name" />
</div>
<div>
<TextField
name="message"
onChange={(e) => onTextChange(e)}
value={state.message}
id="outlined-multiline-static"
variant="outlined"
label="Message"
/>
</div>
<button>Send Message</button>
</form>
<div className="render-chat">
<h1>Chat Log</h1>
{renderChat()}
</div>
</div>
)
}
export default App
i got it already, its because the version is not compatible between the server and the client

Stripe js: uncompleted payment, payment is not defined

So I am working on payment processing with stripe. When I go to the payments on stripe it says they are uncompleted, the customer did not define the payment method...
React component
useEffect(() => {
const getClientSecret = async () => {
const responce = await axios({
method: "post",
url: `/payments/create?total=${getBasketTotal(basket) * 100}`,
});
setClientSecret(responce.data.clientSecret);
};
getClientSecret();
}, [basket]);
console.log("THE SECRET IS >>> ", clientSecret);
const submitHandler = async (e) => {
//stripe magic
e.preventDefault();
setProcessing(true);
const payload = await stripe
.confirmCardPayment(clientSecret, {
payment_method: {
card: elements?.getElement(CardElement),
},
})
.then(({ paymentIntent }) => {
//paymentIntent = payment confirmation
console.log(paymentIntent);
setSucceeded(true);
setError(null);
setProcessing(false);
dispatch({
type: "EMPTY_BASKET",
});
history.replace("/orders");
});
};
const changeHandler = (e) => {
//stripe magic
setDisabled(e.empty);
setError(e.error ? e.error.message : "");
};
return (
<div className="payment">
<div className="payment__container">
<h1>
Checkout(<Link to="/checkout">{basket?.length} items</Link>)
</h1>
<div className="payment__section">
<div className="payment__title">
<h3>Delivery Address</h3>
</div>
<div className="payment__address">
<p>{user?.email}</p>
<p>123 React Lane</p>
<p>Los Angeles, CA</p>
</div>
</div>
<div className="payment__section">
<div className="payment__title">
<h3>Review items and delivery</h3>
</div>
<div className="payment__items">
<FlipMove>
{basket.map((item) => (
<div>
<CheckoutProduct
id={item.id}
title={item.title}
image={item.image}
price={item.price}
rating={item.rating}
/>
</div>
))}
</FlipMove>
</div>
</div>
<div className="payment__section">
<div className="payment__title">
<h3>Payment Method</h3>
</div>
<div className="payment__details">
<form onSubmit={submitHandler}>
<CardElement onChange={changeHandler} />
<div className="payment__priceContainer">
<CurrencyFormat
renderText={(value) => (
<>
<h3>Order Total: {value}</h3>
</>
)}
decimalScale={2}
value={getBasketTotal(basket)}
displayType={"text"}
thousandSeperator={true}
prefix={"$"}
/>
<button
disabled={
processing || disabled || succeeded || clientSecret === null
}
>
<span>{processing ? <p>Processing</p> : "Buy Now"}</span>
</button>
</div>
{error && <div>{error}</div>}
</form>
</div>
</div>
</div>
</div>
);
}
export default Payment;
Node JS
const app = express();
// - Middlewares
app.use(cors({ origin: true }));
app.use(express.json());
// - API routes
app.get("/", (request, responce) => responce.status(200).send("hello world"));
app.post("/payments/create", async (request, responce) => {
const total = request.query.total;
console.log("Payment Request Received >>> ", total);
const paymentIntent = await stripe.paymentIntents.create({
amount: total,
currency: "usd",
});
// OK - Created
responce.status(201).send({
clientSecret: paymentIntent.client_secret,
});
});
// - Listen command
exports.api = functions.https.onRequest(app);
I have two questions: 1st, is this going to b a problem when working on order history? an 2nd, how do I fix this?
Thank you in advance

Delete an item with ReactJS

I'm trying to delete an item after the user clicks on the Delete button. Through the handleDelete function, I am passing an id (idBooks) via axios when the user clicks a book. How do I withdraw it from the click?
Below you will find the React code and then the backend side code in node js.
Frontend
(class extends React.Component {
handleDelete = (e) => {
e.preventDefault();
const { book } = this.props;
axios.delete("http://localhost:8081/delete", book.idBooks )
.then(res => {
console.log(res.data);
}).catch(err => {
console.warn(err.warn);
});
};
render() {
const { book, classes } = this.props;
const token = localStorage.getItem('token');
return(
<Paper className= { classes.marginTopBottom }>
<h2 className={ classes.title }>
{ book.title }
</h2><hr />
<div className= { classes.scrollingDiv }>
<p>
{ book.plot }
</p>
</div>
<hr/>
<div className={ classes.pStyle }>
<p>Publish date:<br /> { new Date(book.publish_date).toLocaleDateString() }</p>
<p>Author:<br /> { book.author }
</p>
<p>Genre:<br /> { book.genre }</p>
</div>
<div>
{ token && (
<Button className={ classes.delete } size="small" onClick={this.handleDelete} type="button" variant="contained" color="primary"
component= {Link} to="delete">
Delete
<DeleteIcon className={ classes.rightIcon } />
</Button>
)}
</div>
</Paper>
)
}
});
Backend
const deleteBook = (req, res) => {
const connection = mysql.createConnection(connectionProperties);
connection.connect();
const query = `DELETE FROM Books WHERE idBooks = ${ req.body.idBooks }`;
connection.query(query, (err, res) => {
if (err) {
res.status(500).send(err);
} else {
res.status(200).send('Book deleted correctly.');
}
});
};
I'd add a prop onDeleteCallback, and on successful delete call that function with deleted book id. In parent component (with all the books are listed) update the state with filtered out books.
I guess passing the parameter might help you fix this issue.
On the delete Button add a parameter to the onClick={()=>this.handleDelete(e,book.idBooks)}
Change the handleDelete function a bit as below
handleDelete = (e,idBooks) => {
e.preventDefault();
axios.delete("http://localhost:8081/delete", idBooks )
.then(res => {
console.log(res.data);
}).catch(err => {
console.warn(err.warn);
});
};

how to show a Component in React and renew its lifecycle each time its showed?

I just started node.js and react, I was curious how to create a chat app with socket.io so I followed few tutorials on how to create rooms, joining/leaving and messaging in the room. I used React Context to pass one socket instance.
I can create rooms, join them, leave them. The problem I got is that when I join another room and try to send a message, I get this warning:
Can't perform a React state update on an unmounted component. when rendering a component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
Can you please give me some advice on about how I should rethink to rewrite React with componentDidMount and componentWillUnmount functions for this case?
I'd like to add these details to understand what I'm trying to do:
- homepage where users can create rooms
- rooms are spawned as 2 buttons, 1 to show the Chatroom (join the room) and the other to close the Chatroom (leave the room)
- users can join the Chatroom and send messages
client side (Home.js)
class Home extends Component {
constructor(props) {
super(props);
this.state = {
room: "",
rooms: [],
chat: false
};
this.creatingRoom = this.creatingRoom.bind(this);
this.joinRoom = this.joinRoom.bind(this);
this.leaveRoom = this.leaveRoom.bind(this);
this.props.socket.on("createRoom", function(room) {
addRoom(room);
});
const addRoom = room => {
this.setState({ rooms: [...this.state.rooms, room] });
};
};
creatingRoom(e) {
e.preventDefault();
this.props.socket.emit("creatingRoom", {
room: this.state.room
});
this.setState({ room: "" });
};
joinRoom(e) {
let room = e.target.value;
this.props.socket.emit("joinRoom", room);
this.setState({
chat: true
});
};
leaveRoom(e) {
let room = e.target.value;
this.props.socket.emit("leaveRoom", room);
this.setState({
chat: false
});
};
render() {
return (
<React.Fragment>
{this.state.chat === true ? (
<ChatroomWithSocket />
) : (
<h1> no chatroom </h1>
)}
<div>
<h1> Create your room </h1>
</div>
<div>
<form>
<textarea
name="room"
placeholder="Write.."
value={this.state.room}
onChange={ev => this.setState({ room: ev.target.value })}
/>
<button onClick={this.creatingRoom}>
Create
</button>
</form>
</div>
<div>
<h4> Rooms </h4>
<div>
<ul>
{this.state.rooms.map((room, index) => {
return (
<li key={index}>
<button href="#" onClick={this.joinRoom} value={room.room}>
Join {room.room}
</button>
<button href="#" onClick={this.leaveRoom} value=
{room.room}>
Leave {room.room}
</button>
</li>
);
})}
</ul>
</div>
</div>
</React.Fragment>
);
}
}
const HomeWithSocket = props => (
<SocketContext.Consumer>
{socket => <Home {...props} socket={socket} />}
</SocketContext.Consumer>
);
client side (Chatroom.js)
class Chatroom extends Component {
constructor(props) {
super(props);
this.state = {
message: "",
messages: []
};
this.sendMessage = this.sendMessage.bind(this);
this.props.socket.on("receiveMessage", function(data) {
addMessage(data);
});
const addMessage = data => {
this.setState({ messages: [...this.state.messages, data] });
};
}
sendMessage(e) {
e.preventDefault();
this.props.socket.emit("sendMessage", {
message: this.state.message
});
this.setState({ message: "" });
}
render() {
return (
<React.Fragment>
<div className="messages">
{this.state.messages.map((message, key) => {
return <li key={key}>{message.message}</li>;
})}
</div>
<div>
<form onSubmit={this.sendMessage}>
<input
type="text"
placeholder="Message"
value={this.state.message}
onChange={ev => this.setState({ message: ev.target.value })}
/>
<button type="submit">
Send
</button>
</form>
</div>
</React.Fragment>
);
}
}
const ChatroomWithSocket = props => (
<SocketContext.Consumer>
{socket => <Chatroom {...props} socket={socket} />}
</SocketContext.Consumer>
);
server side (index.js)
var rooms = [];
io.on("connection", function(socket) {
socket.on("creatingRoom", function(room) {
rooms.push(room);
io.emit("createRoom", room);
});
socket.on("joinRoom", function(newRoom) {
socket.join(newRoom);
socket.room = newRoom;
});
socket.on("leaveRoom", function() {
socket.leave(socket.room);
});
socket.on("sendMessage", function(data) {
io.emit("receiveMessage", data);
});
});
This error usually means what it says, a possible memory leak.
From my suspicions, it could be from the socket connection. See it this way..
In the componentDidMount, you set a socket connection. If you are leaving the component without closing the socket connection, you're bound to receive this error. I'm not sure what exactly you're doing, but try closing the socket in the componentWillUnmount to see if it works.
If it does, consider creating the socket in a component that doesn't get destroyed when navigating, or store the socket connection in Redux

Resources