How to send a notification instead of a message when user is offline using socket.io - reactjs

So whenever another user is offline and an online user tries to send a message, it returns an error. The error is caused because it didn't find a socket where the user is in, but I can console.log something from the error response, since I put it in if/else block. So i was wondering how could I send a response to the client and push a notification for the user who is offline. The explanation is probably confusing, so here is the code:
index.js(socket):
const io = require('socket.io')(8900, {
cors: {
origin: 'http://localhost:3000',
},
});
let users = [];
const addUser = (userId, socketId) => {
!users.some((user) => user.userId === userId) &&
users.push({ userId, socketId });
};
const removeUser = (socketId) => {
users = users.filter((user) => user.socketId !== socketId);
};
const getUser = (userId) => {
return foundUser = users.find((user) => user.userId === userId)
};
io.on('connection', (socket) => {
//when ceonnect
console.log('a user connected.');
//take userId and socketId from user
socket.on('addUser', (userId) => {
addUser(userId, socket.id);
io.emit('getUsers', users);
});
//send and get message
socket.on('sendMessage', ({ senderId, recieverId, text }) => {
const user = getUser(recieverId);
console.log('RECIEVERRRR', user);
if (user) {
io.to(user.socketId).emit('getMessage', {
senderId,
text,
});
} else {
// IMPORTANT: This is where I can console.log the error, but can't figure out how to send the response to the client
}
});
//when disconnect
socket.on('disconnect', () => {
console.log('a user disconnected!');
removeUser(socket.id);
io.emit('getUsers', users);
});
});
dashboard.js (I cut out most of the code, but this is where i'm sending request to socket):
const handleSubmit = async (e) => {
e.preventDefault();
const message = {
sender: user._id,
text: newMessage,
conversationId: currentChat._id,
};
const recieverId = currentChat.members.find(
(member) => member !== user._id
);
console.log('REC from FRONT END', recieverId);
socket.current.emit('sendMessage', {
senderId: user._id,
recieverId,
text: newMessage,
});
try {
const { data } = await axios.post('/api/messages/addMessage', message);
setMessages([...messages, data]);
setNewMessage('');
} catch (err) {
console.log(err);
}
};
Any help would be appreciated. Thanks.

You can emit a new event sendMessageFailed:
//send and get message
socket.on('sendMessage', ({ senderId, recieverId, text }) => {
const user = getUser(recieverId);
console.log('RECIEVERRRR', user);
if (user) {
io.to(user.socketId).emit('getMessage', {
senderId,
text,
});
} else {
io.to(user.socketId).emit('sendMessageFailed', "Your friend is offline");
}
});
At the client side listen to the event:
socket.current.on('sendMessageFailed', (err) => {
//Sending message has failed!
});

Related

Socket works the for a few seconds then fails

I've managed to establish a connection using socket. It works great for the first few seconds after that it runs super slow takes like almost 2-3 mins to complete a request. And then it produces this error continuously. The app doesn't crash it just runs slowly with the error displaying countless times.
Firefox can’t establish a connection to the server at ws://localhost:5000/socket.io/?EIO=4&transport=websocket&sid=9S6kqHJdHHXQgrobAAHp..
Error on console.
Main.js
function Home(props) {
const [Username , setUsername] = useState("")
const [SearchedData, setSearchedData] = useState()
const [Data, setData] = useState()
const socket = io('http://localhost:5000')
React.useEffect(() => {
// socket.current = io('http://localhost:5000')
socket.emit("content","yada")
socket.on("get-data", data => {
setData(data)
})
})
function NavBar(props){
const handleClick = (e) => {
const {id} = e.target
if(id === "Post-btn"){
if(Content.length > 0){
let data = {
Username: "yada", Content
}
props.socket.emit("store-data", data)
}
}
return(
Tags....
)}
function Content (props) {
const onLike = (e) => {
const { id } = e.target.dataset
const data = {
username: "yada",
id : id
}
// console.log(data)
props.socket.emit("like", data)
}
return(
Tags.....
)
}
server.js
mongoose.connect(process.env.MongoDB,
{ useNewUrlParser: true, useUnifiedTopology: true }).then(() => {
console.log("Database Connected")
}).catch(err => {
console.log(err)
});
const server = app.listen(process.env.Port, () => {
console.log("Connected on " + process.env.Port)
})
const io = socket(server, {
cors:{
origin: "http://localhost:3000",
credential: true,
}
})
let cuid;
io.on("connection", (socket) => {
socket.on("content", username => {
Comments.find({},(err, data) => {
if(!err)
socket.emit("get-data", data)
})
})
socket.on("store-data", data => {
const {Username, Content} = data
const newdata = new Comments({
userName: Username,
content: Content,
createdAt: new Date().toDateString(),
replies: []
})
newdata.save().then(data => {
for(const d in data)
if(d === "_id"){
Users.findOneAndUpdate({username: Username}, {$push: {UserContent: data[d]}}, {new: true}, (err, save) => {
if(err)
console.log(err)
else
console.log(save)
})
}
})
})
socket.on("like", data => {
const {username, id} = data
Users.findOne({username:username}, (err, data) => {
if(!err){
cuid = data['id']
console.log(cuid)
Comments.findByIdAndUpdate(id, {$set: {score: data['_id']}}, {upsert: true}, (err, d) => {
if(!err){
console.log(d)
}
})
}
})
})
})
Looking at the code provided, I noticed there is an useEffect without params. This may be causing a loop until the application crashes.
React.useEffect(() => {
// socket.current = io('http://localhost:5000')
socket.emit("content","yada")
socket.on("get-data", data => {
setData(data)
})
socket.on("Updated", data => {
setData(data)
})
}, []); <- this is missing
This empty array indicates that the content inside the useEffect will only run once.
More about this https://reactjs.org/docs/hooks-intro.html

How to wait for cookie before setting authContext and login state. React, Firebase

I have a createUser function that sends an axios req to express and takes some time to finish creating a firebase authenticated user, create a token and send token back to user as a cookie.
I also have a getLoggedIn function that takes the cookie and sends it to express to get authenticated and then adjust my authContext accordingly.
My function looks like this: await createUser(registerData ).then(() => getLoggedIn())
My problem is that getLoggedIn() is being called early, before the createUser is done sending back the cookie. Any idea how to fix this? Is there a way to listen for a cookie change? I dont see other people doing that, do I have the wrong approach in general?
Thanks for any help.
New User Form
const { getLoggedIn, loggedIn } = useContext(AuthContext);
const register = async (e) => {
e.preventDefault();
const registerData = {
email,
password,
passwordVerify,
};
try {
await createUser(registerData).then(() => getLoggedIn());
} catch (err) {
console.log(err);
}
}};
CreateUser
export const createUser = async (props) => {
console.log("New User Creation Initiated", props);
const { email, password, passwordVerify } = props;
let userObject = { tokenId: null };
try {
await firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(({ user }) => {
console.log("createUserWithEmailAndPassword", user);
user
.getIdToken()
.then((tokenId) => {
userObject = { tokenId: tokenId };
axios.post(`${domain}/auth`, userObject);
})
.catch((err) => console.log("new user error", err));
})
.catch((err) => {
console.log("createUser Error", err);
});
return;
} catch (err) {
console.log("New User Creation Error: ", err);
}
};
AuthContext
const AuthContextProvider = (props) => {
console.log("AuthContextProvider Initiated");
const [loggedIn, setLoggedIn] = useState(null);
const getLoggedIn = async () => {
console.log("getLoggedIn Initiated");
let validatedUser = await axios.get(`${domain}/auth/loggedIn`);
console.log("AuthContextProvider validatedUser", validatedUser);
setLoggedIn(validatedUser.data);
};
useEffect(() => {
getLoggedIn();
}, []);
return (
<AuthContext.Provider value={{ loggedIn, getLoggedIn }}>
{props.children}
</AuthContext.Provider>
);
};
Express
router.post("/", async (req, res) => {
console.log("---User signup initiated---");
try {
const { tokenId } = req.body;
console.log("tokenId passed from frontend: ", tokenId);
admin
.auth()
.verifyIdToken(tokenId)
.then((decodedToken) => {
const { email, uid } = decodedToken;
console.log("Fetched UID: ", uid);
console.log("Fetched email: ", email);
const Users = db.collection("users");
Users.doc(`${uid}`).set({
email: email,
posts: [],
});
console.log("---jwt signing initiated---");
const token = jwt.sign(
{
user: { email, uid },
},
process.env.JWT_SECRET
);
console.log("token log: ", token);
return res
.cookie("token", token, {
httpOnly: true,
sameSite: "none",
secure: true,
})
.send();
});
} catch (err) {
console.log(err);
}
});
router.get("/loggedIn", (req, res) => {
console.log("login validation initiated");
try {
const token = req.cookies.token;
if (!token) {
console.log("no token cookie");
return res.json(null);
}
const validatedUser = jwt.verify(token, process.env.JWT_SECRET);
console.log("Token cookie: ", validatedUser);
res.json(validatedUser);
} catch (err) {
console.log("loggedIn", err);
}
});
You can use onAuthStateChanged like this in your AuthContext.
const AuthContextProvider = (props) => {
console.log("AuthContextProvider Initiated");
const [loggedIn, setLoggedIn] = useState(null);
const getLoggedIn = async () => {
console.log("getLoggedIn Initiated");
let validatedUser = await axios.get(`${domain}/auth/loggedIn`);
console.log("AuthContextProvider validatedUser", validatedUser);
setLoggedIn(validatedUser.data);
};
const handleAuthStateChanged = user => {
if(user) { //user login
getLoggedIn()
} else { //user logout
setLoggedIn(null)
}
}
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
handleAuthStateChanged(user)
})
return unsubscribe
}, []);
return (
<AuthContext.Provider value={{ loggedIn, getLoggedIn }}>
{props.children}
</AuthContext.Provider>
);
};
In my createUser function I moved my axios call and returned it outside of the firebase.auth() sequence of promises. Seems to work now.
export const createUser = async (props) => {
console.log("New User Creation Initiated", props);
const { email, password, passwordVerify } = props;
let userObject = { tokenId: null };
try {
await firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(({ user }) => {
console.log("createUserWithEmailAndPassword", user);
user
.getIdToken()
.then((tokenId) => {
userObject = { tokenId: tokenId };
})
.catch((err) => console.log("new user error", err));
})
.catch((err) => {
console.log("createUser Error", err);
});
return axios.post(`${domain}/auth`, userObject);
} catch (err) {
console.log("New User Creation Error: ", err);
}
};

Socket.io connected property is always false

I am trying to create a private messaging app. The socket connects at first but then when I try to emit any event from the client side, it shows that socket.connected property is false.
Please help me out.
Here's is my client side code, Please note that socket.on("users") part works correctly because all of it happens when the socket it connected. It means the connection part is happening correctly. After that whenever I try to call a function that emits a socket event, it shows that socket.connected property is false and doesnt do anything.
Any help would be appreciated.
var connectionOptions = {
transports: ["websocket"],
autoConnect: false,
};
socket = io("http://localhost:3001", connectionOptions);
socket.on("connection _error", (err) => {
if (err.message === "invalid username") {
console.log("ERROR");
}
});
socket.on("users", (users) => {
users.forEach((user) => {
user.self = user.userID === socket.id;
//initReactiveProperties(user);
});
socket.on("user connected", (user) => {
// TODO
setUsers((existingusers) => [...existingusers, user]);
console.log(user);
});
// put the current user first, and then sort by username
users = users.sort((a, b) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
//console.log(users);
});
socket.on("private message", ({ content, from }) => {
console.log(content);
});
useEffect(() => {
const username = localStorage.getItem("username");
console.log(username);
socket.auth = { username };
socket.connect();
}, []);
function SendMessage() {
socket.emit("test", "hello");
// selectedChatUser
console.log(socket.connected);
if (selectChatUser) {
socket.emit("private message", {
content: "hello there",
to: selectChatUser.userID,
});
console.log("Message Sent");
}
}
And here is my server side code:
const app = require("express")();
const httpServer = require("http").createServer(app);
const cors = require("cors");
app.use(cors());
const options = {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
};
const io = require("socket.io")(httpServer, options);
io.use((socket, next) => {
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid usernmae"));
}
socket.username = username;
next();
});
io.on("connect", (socket) => {
console.log("New connection");
const users = [];
for (let [id, socket] of io.of("/").sockets) {
users.push({
userID: id,
username: socket.username,
});
}
socket.broadcast.emit("user connected", {
userID: socket.id,
username: socket.username,
});
socket.emit("users", users);
socket.on("test", () => {
console.log("test");
});
socket.on("private message", ({ content, to }) => {
console.log(content);
console.log("hello there");
socket.to(to).emit("private message", {
content,
from: socket.id,
});
});
});
httpServer.listen(3001, () => {
console.log("Server has started");
});
// https://socket.io/
Following line will re-run every time your component renders, losing reference to the socket that was actually connected:
socket = io("http://localhost:3001", connectionOptions);
You can use a ref to persist it between renders:
const socketRef = useRef();
socketRef.current = socket;
// use socketRef.current everywhere else in your code

the problem: the code below work flawlessly in dev but after uploaded to Heroku it's working sometimes,

I am trying to get socket.io to work with Heroku but it doesn't play well.
the problem: the code below work flawlessly in dev but after uploaded to Heroku it's working sometimes,
1.don't see anything weird on logs.
2.the data saved to DB and will appear after refresh
3.refresh helps to sockets sometimes.
4.there is no pattern to it, sometime it will work ok for an hour and some time won't last a minute
5.heroku features:enable http-session-affinity also done
server:
const mongoose = require("mongoose");
const Rooms = mongoose.model("Rooms");
const Chat = mongoose.model("Chats");
const jwt = require("jwt-then");
const socketChat = (app, io) => {
io.use(async (socket, next) => {
try {
const token = socket.handshake.query.token;
const payload = await jwt.verify(token, process.env.SECRET_KEY);
socket.userId = payload.id;
socket.id = payload.id;
socket.name = payload.username;
console.log({ socketisd: socket.userId, name: socket.name });
next();
} catch (err) { }
});
io.on("connection", (socket) => {
console.log("Connected: " + socket.name);
socket.on("disconnect", () => {
console.log("Disconnected: " + socket.name);
});
socket.on("joinRoom", async ({ roomId },callback) => {
socket.join(roomId);
console.log(` ${socket.name} joined room: ` + roomId);
socket.to(roomId).emit("live", { name: socket.name, live: true, roomId });
callback({
status: "ok"
});
});
socket.on("leaveRoom", async ({ roomId },callback) => {
socket.leave(roomId);
console.log(` ${socket.name} left room: ` + roomId);
socket.to(roomId).emit("live", { name: socket.name, live: false, roomId });
callback({
status: "ok"
});
});
socket.on("typing", async ({ msg, roomId }) => {
let name = "";
if (msg.text && msg.text.trim().length > 0) {
let length = msg.text.length;
name = length > 0 ? socket.name : "";
}
socket.to(roomId).emit("typingclient", { name });
});
socket.on(
"chatroomMessage",
async ({ roomId, message, name, profileImg, timestamp, type, date }) => {
if (message.trim().length > 0) {
io.to(roomId).emit("newMessage", {
roomId,
user: socket.userId,
message,
name,
type,
date,
profileImg,
timestamp,
recived: true,
});
let room = await Rooms.findById(roomId).populate("messages");
if (type === "reject") {
await Chat.findOneAndUpdate(
{ roomId, type: "dateConfirm" },
{ type: "reject", message },
{ new: true }
);
}
else {
const newMessage = new Chat({
roomId,
date,
type,
user: socket.userId,
message,
name,
profileImg,
timestamp,
recived: true,
});
await newMessage.save();
room.messages.push(newMessage);
await room.save();
}
let theOtherGuy =await room.users.find((user) => user != socket.userId);
io.to(theOtherGuy).emit("room", room);
}
}
);
});
};
module.exports = socketChat;
client:
/**
* Sends message with emit socket to server
* #param {Object} event Default Browser Event Object
* #param {String} text content of message
* #param {String} date Date for schedualing
* #param {String} type type of the message (reject,request etc...)
*/
const sendMessage = (event, text, date = null, type = null) => {
event && event.preventDefault();
if (socket) {
socket.emit("chatroomMessage", {
roomId,
date,
type,
name: currentUser.user.username,
profileImg: currentUser.user.profileImageUrl,
timestamp: new Date(),
recived: false,
message: text,
});
setText("");
socket.emit("typing", {
msg: "",
roomId,
});
}
};
React.useEffect(() => {
if (socket) {
socket.emit("joinRoom", {roomId},(answer)=>
console.log("joinRoom",roomId,answer)
);
socket.on("newMessage", (message) => {
console.log({message})
if (message.type === "reject")
setMessages((prevMessages) => [...prevMessages.filter(m => m.type !== 'dateConfirm'), message]);
else
setMessages((prevMessages) => [...prevMessages, message]);
});
socket.on("live", (message) => {
console.log(message)
message.live ? setSucess(`user ${message.name} has connected`) : setErr(`user ${message.name} has left`)
});
socket.on("typingclient", (name) => {
setTyping(name);
});
}
return () => {
if (socket) {
socket.emit("leaveRoom", {roomId},(answer)=>
console.log("leaveRoom",roomId,answer)
);
}
//Component Unmount
};
//eslint-disable-next-line
}, [socket]);
and main where i define my socket:
const [socket, setSocket] = React.useState(null);
const setupSocket = () => {
console.log("socket4")
const token = sessionStorage.getItem("jwtToken");
if (token && !socket) {
const newSocket = io("/", {
query: {
token: sessionStorage.getItem("jwtToken"),
},
path: '/socket'
});
newSocket.on("disconnect", () => {
// setSocket(null);
// makeToast("error", "Socket Disconnected!");
});
newSocket.on("connect", () => {
// makeToast("success", "Socket Connected!");
console.log("Socket Connected");
});
setSocket(newSocket);
}
};
React.useEffect(() => {
if (currentUser && !socket) setupSocket();
//eslint-disable-next-line
}, [currentUser, socket]);
ststic.json:
{
"root":"build/",
"routes":{
"/**":"index.html"
},
"proxies":{
"/api/":{"origin":"${API_URL}"},
"/socket/":{"origin":"${SOCKET_URL}"}
}
}
it looks like the Io object did not like
socket.id = payload.id;
I have removed it and everything is working now.
I think it may have resulted from different keys in the Io object resulting in unexpected behavior.

How to post with Axios in React?

This my first time in React and Axios. I have a login form and sign up form and don't have any database. I want any mock API to simulate a login and sign up which provides a token in the response, in order to save it in a local storage in order to keep the user logged in. Also how do I prevent the user to go the home page (login/logout screen). When they type for example www.blabla.com, I want, if the token exists they still in the app, otherwise the token will be erased.
I tried to fetch data from mock API by axios.get(), it worked but it still static
componentDidMount() { // this For Testing Until Now
axios.get('https://jsonplaceholder.typicode.com/users')
.then(res => {
console.log(res);
this.setState({
users: res.data
}, () => {
console.log('state', this.state.users)
})
});
}
I want to communicate with API that allows my to fetch data and post data to it. This is my login function
handleLogin(e) {
e.preventDefault();
const email = e.target.elements.email.value;
const password = e.target.elements.password.value;
let userData = {};
if(validator.isEmpty(email) || validator.isEmpty(password) || !validator.isEmail(email)) {
this.setState({
error: 'You Have To Complete Your Data Correctly'
}, () => {
console.log('failed');
});
} else {
userData = {email, password};
const { users } = this.state;
if(users.find(item => item.email === userData.email)) {
const index = users.findIndex(item => item.email === userData.email);
this.props.history.push(`./create/${users[index].username}`);
}
}
}
and this is my signup function
handleAddNewUser(e) {
e.preventDefault();
const name = e.target.elements.userName.value.toLowerCase().trim();
const email = e.target.elements.userEmail.value.toLowerCase().trim();
const password = e.target.elements.pass.value;
const repassword = e.target.elements.re_pass.value;
let userInfo = {};
const { users } = this.state;
console.log(name, email);
if (validator.isEmpty(name) || validator.isEmpty(email) ||
validator.isEmpty(password) || validator.isEmpty(repassword) ||
!validator.isEmail(email) || !validator.equals(password, repassword)) {
this.setState({
error: 'You Have to enter valid data, Make Sure That The Fields are Complete',
open: true
});
} else {
userInfo = { name, email, password };
if (
users.find(item => item.name === userInfo.name) ||
users.find(item => item.email === userInfo.email)
) {
this.setState({
error: 'This username or email is used',
open: true
});
} else {
this.setState({
users: this.state.users.concat(userInfo),
success: true
}, () => {
// this.props.history.push(`./create/${userInfo.name}`);
// console.log(users)
});
console.log(users)
}
}
}
You can use axios.post() to send post request.
// POST
const userData = {
email: 'demouser#gmail.com',
username: 'demouser',
password: '1a2b3c4d5e' //This should be encoded
}
axios.post('https://example.com/createUser', userData)
.then(res => {
responseData = res.data
if (responseData.status == 'success') {
const user = responseData.user
...
} else {
alert('Something went wrong while creating account')
}
})

Resources