I am building a chat app with React, Node/Express and socket.io. I have my sockets successfully set to my express server via http.createServer. I have a listener on client and server listening for new messages coming into the chat room. Ideally, I want each instance of the chat to be updated when there is an additional message, like any chat room that ever existed :)
Now I have a successful listen between client and server. I know because of a console.log server-side. However, I am not re-rendering the chat component when I submit a new message from a different instance.
So my code in my client-side (again React) component is as follows and I am using the socket CDN with script tags in my index.html (script tags not shown):
Socket CDN here
var socket = io('')
So that is the socket you see client side :
componentDidMount() {
return axios.get(`api/messages`)
.then((result) => {
if (result.data.length) {
this.setState({
messages: [ ...this.state.messages, ...result.data]
} , () => {
console.log("The state after messages are mounted : ", this.state)
})
}
})
.catch((err) => { throw err})
socket.on('new message', msg => {
this.newMessage(msg);
})
};
newMessage(msg) {
this.setState({
messages: [...this.state.messages, msg]
}, () => {
this.setState({ message: '' })
return this.scrollToBottom()
});
};
onSubmitMessage(event) {
event.preventDefault();
const content = this.state.message;
const msg = {
content,
createdAt : new Date(),
userId : "one",
chatRoomId : "two"
}
axios.post(`api/messages/`, msg)
.then(() => {
this.newMessage(msg);
socket.emit('new message', msg); //HERE'S THE SOCKETS IN ACTION
})
};
Here is the server-side code Node/Express:
//in server.js
const io = new socketIo(server)
require('./socketEvents')(io);
const connections = [];
Then a separate file for my socket events
//in socketEvents.js
module.exports = (io) => {
io.on('connection', (socket) => {
console.log("Beautiful sockets are connected")
socket.once('disconnect', () => {
console.log("socket is disconnected");
});
//DOESN'T DO ANYTHING YET
socket.on('join global', (username) => {
socket.join(username);
console.log("New user in the global chat : ", username)
});
socket.on('new message', (msg) => {
console.log("The new message from sockets : ", msg);
socket.emit('new message', msg.content);
});
});
}
My sockets server side are linked up with the client. I'm just not seeing new messages in different instances. Is it because I'm not re-rendering after the server receives the message?
Thanks in advance, please let me know if you need me to clarify anything.
Cheers!
I figured it out... I'm going to leave this post up with a walkthrough in an attempt to help others who are having trouble with sockets. I may post a blog about it. Will update if I do.
So the code listens on the client side for a message to be sent inside of my onSubmitMessage function.
onSubmitMessage(event) {
event.preventDefault(); //prevents HTML <form> from going on its own post
const content = this.state.message;
//Create message object
const msg = {
content,
createdAt : new Date(),
userId : "one",
chatRoomId : "two"
}
//HERE'S THE IMPORTANT PART!!!
axios.post(`api/messages/`, msg)
.then(() => {
// wrapped in a promise, send a handler to server called
// ('new message') with the message object
this.newMessage(msg);
socket.emit('new message', msg);
})
.then(() => {
//Another promise then waits for the handler to come back from server
//*****IMPORTANT*************
//Then invoke newMessage function to get the post on all sockets
socket.on('message', (msg) => {
this.newMessage(msg);
})
})
};
Now on the server side this is what's happening:
// This is where the listener is for the client side handle
socket.on('new message', (msg) => {
// broadcast.emit will send the msg object back to client side and
// post to every instance expcept for the creator of the message
socket.broadcast.emit('message', msg);
});
SO the data path is (C) for client, (S) for server:
receive message object from user and -------->
(C)socket.emit('new message') -----> (S) socket.on('new message') -------> (S) socket.broadcast.emit('message') --------> (C)socket.on('message')
Back in the client side, I can invoke my newMessage function, which will set the message to state so I can display it.
I hope someone finds this useful! Surprisingly, this seems to go relatively unanswered on Stack. If anyone has any questions feel free to ask!
Related
I have an app where I need the users to be able to send a message. Right now, users can send a message and socket IO broadcasts the message, but the sender also receives. I need messages sent to go to everyone but sender
Current behavior: User A calls handleButton function, User A and User B receive broadcastMessage and console log guess
Wanted behavior: User A calls handleButton function. Only user B receives broadcastMessage and logs guess
Server code:
io.on("connection", (socket) => {
socket.on("upGuess", (guess, room) => {
if (room) {
socket.to(room).emit("broadcastMessage", guess);
} else {
socket.broadcast.emit("broadcastMessage", guess);
}
});
});
Client side Code:
socket.on("broadcastMessage", (guess) => {
console.log(guess);
});
const handleButton = () => {
socket.emit("upGuess", "TestGuess");
};
Essentially, this is what I had in mind :
Client
const Messages = new Mongo.Collection("messages");
Meteor.call("message", { room: "foo", message: "Hello world");
Meteor.subsribe("messages", "foo");
// elsewhere
const messages = Messages.find({ room: "foo" });
Server
Meteor.methods({
message: ({ room, message }) => {
// 1. remove old messages in the room
// 2. add new message to the room
}
});
Meteor.publish("messages", function (room) {
// 1. return message collection for room
});
The client, I assume, uses minimongo, which is fine, but the server does not have access to a MongoDB instance whatsoever.
What would be the implementation on the server?
As mentioned in comments, I think this can be achieved using the manual publication methods described in the documentation. Something like this might work:
// Server:
const rooms = {};
Meteor.publish('manualMessages', function(roomId) {
check(roomId, String);
rooms[roomId] = this;
this.ready();
});
Meteor.methods({
message: ({ roomId, message }) => {
// 1. remove old messages in the room
const room = rooms[roomId];
if (!room) {
throw new Meteor.Error('no room', 'that room does not exist');
}
room.removed('messages', roomId);
// 2. add new message to the room
room.added('messages', roomId, { message });
}
});
// Client:
const Messages = new Mongo.Collection("messages");
Meteor.call("message", { room: "foo", message: "Hello world");
Meteor.subsribe("manualMessages", "foo");
// elsewhere
const messages = Messages.find({ room: "foo" });
One thing to verify is whether this in the publish function changes per client, in which case rooms should contain arrays, or whether it is the same object for all clients, as assumed here. But hopefully this point you in the right direction.
I have two applications. I get the same message several times.
For example:
app1 -> user_1 writes message: 'I need help' topic: demo
app2 -> show list of chats
app2 -> user_2 open chat from list and subscribe to topic:demo and now can talk with user_1
app2 user_2
I need help
I need help
I need help
I need help
I need help
It seems that connection is created several times.
It seems that subscribe and publish to different topic also not working. App1:
let client = mqtt.connect('http://127.0.0.1:9001', {
clientId: "front-client",
username: "admin",
password: "admin",
});
client.on('connect', (topic) => {
client.subscribe(this.topic, (err) => {
if (!err) {}
});
});
this.mqttClient = client;
//handle incoming messages
client.on('message', (topic, message, packet) => {
this.showMessageFromMqtt(message);
});
}
sendMsgByMqtt(message: string) {
this.mqttClient.publish(this.topic + '/MSG', message, {retain: true});
}
App2:
configureMqttChannel() {
let client = mqtt.connect('http://127.0.0.1:9001', {
clientId: "front-client-angular",
username: "admin",
password: "admin"
});
// console.log("connected flag: " + client.connected);
client.on('connect', (topic) => {
// console.log("connected " + client.connected);
client.subscribe(this.topic + '/MSG', (err) => {
if (!err) {
// console.log('message sent by mqtt')
// client.publish(this.topic, '', {retain: true})
}
});
});
this.mqttClient = client;
//handle incoming messages
client.on('message', (topic, message, packet) => {
// console.log("message is " + message);
// console.log("topic is " + topic);
this.showMessageFromMqtt(message);
});
}
sendMsgByMqtt(message: string) {
this.mqttClient.publish(this.topic, message, {retain: true});
}
In browser console:
connected true
connected true
connected true
connected true
connected true
connected true
connected true
......
If you are going to publish and subscribe to the same topic then your message must include an identifier that allows you to filter out messages that you've published when they invariably come back to you.
An example of this would be (pseudo code):
const MY_IDENTIFIER = generateSomeIdentifierUniqueToThisClient();
publish({
identifier:MY_IDENTIFIER,
data: // the actual content of your message
});
handleIncoming = data => {
// this was a message you sent
if(data.identifier === MY_IDENTIFIER) return;
// handle it
}
An alternative solution is to publish to say, demo/someUniqueClientIdentifier and subscribe to demo/+
When you receive a message, you can use the topic name to see which client it came from and filter it out that way - a variation on the exact same thing as above.
I'm developing a chat app and i have some trouble with socket.io.
Stuff works great and quick. But as soon as more people are logged in - everyone see's all the messages and rendering gets messy.
I'm sending a unique ID with each message, which i would love to create rooms with where people can chat with each other (1 on 1)
THis happens first:
const socketMessage = {
type: type,
body: body,
author: author,
date: Date.now(),
id: chatId
};
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('message-sent', socketMessage);
THis goes to the server where this happens:
socket.on('message-sent', data => {
socket.broadcast.emit('new-chat-message', data);
});
WHich then goes into a custom hook and adds to the messages-array.
export default function useGetChatMessages(_id) {
const [chatMessages, setChatMessages] = React.useState([]);
React.useEffect(() => {
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('send-chat-id', _id);
socket.on('chat-messages', data => {
setChatMessages(data.messages);
});
socket.on('new-chat-message', message => {
setChatMessages(messages => [...messages, message]);
notification.play();
});
}, [_id]);
return chatMessages;
}
I know the broadcast part is wrong on my server but i have tried lots of things:
Sending the chatId with the message and then do socket.join(chatId) at several stages.
Then socket.to(chatId).emit(...) and so on.
But i can not get it right.
Can someone help?
Thanks a lot in advance!! :-)
Actually, you could use the socket io feature called namespaces.
Look an pretty simple example:
In the client side you would do something like this:
const socket = io.connect(socket_url, { query: "chatRoomId=123&userId=983912" });
And in the server side something like this:
io.on('connection', (socket) => {
const chatRoomId = socket.handshake.query['chatRoomId'];
const userId = socket.handshake.query['userId'];
const user = find(userId) // Pretend :)
socket.join(chatRoomId);
socket.on('new_message', (id, msg) => {
io.to(chatRoomId).emit(`${user.name} says ${msg}`);
});
socket.on('disconnect', () => {
socket.leave(chatRoomId)
console.log('user disconnected');
});
});
TL;DR - How to prevent client from receiving its own messages?
So I'm playing with socket.io after my experience with apollo and graphql.
My simple server looks like this:
io.on('connection', (socket) => {
console.log('New connection established.');
socket.on('disconnect', () => {
console.log('User disconnected.');
});
// Projects:
socket.on('join project', (data) => {
console.log(`User (${data.user.email}) join project with ID ${data.project.id}`);
socket.join(data.project.id);
});
socket.on('leave project', (data) => {
socket.leave(data.project.id);
});
socket.on('change field', (data) => {
console.log('Field was changed:', data);
const { project } = data;
socket.to(project.id).broadcast.emit('field changed', data);
});
});
I'm emitting something like this inside my application:
socket.emit('change field', {
project: {
id: 1,
},
value: 'Hello world',
usersEmail: 'example#email.com',
fieldName: 'description',
});
socket.on('field changed', (data) => {
// if (data.usersEmail === 'example#email.com') return; // This would stop from receiving own messages
console.log('CLIENT: field was changed!', data);
});
What I thought would happen is (due to the broadcast flag that I set up in the on('change field', ...)):
Clients A emits the message
Clients other than A receive the message
What is happening is a log inside other clients and client A itself, saying that the field was changed. Am I missing something?
I had the exact same problem. Couldn't (or didn't try hard enough) to find a setting for it, so instead just added this to my clients on page load:
document.windowid = Math.round(Math.random() * 1000000000000);
Then, add this to the message you emit in your client:
windowid: document.windowid
Then, when you accept data on the client, only do the action when windowid is not the same:
if (message.windowid != document.windowid)
It's not great and socket.io should take care of this issue, but this is the solution I used in my app :)