DEV Community

Jiayi
Jiayi

Posted on

Private chat using Socket.io

Hey guys! I haven't been updating the series here but I have made quite a lot of progress on the Goodeed app. One of the features of this app includes a private chat and what better technology to use for real time communication than Socket.io.

The idea is that if the user wants to know more about the item they want to borrow or they're not comfortable leaving those details such as where and when to meet in the comments, they can choose to message the other privately. So, basically it is a 1-to-1 chat.

There are a few ways to go about creating socket connections and to make a private messaging feature work but after a few attempts, I've decided to do the Rooms route in which a chat will be held in a room where there can only be a maximum of 2 participants. Check out more about Socket.io rooms here.

Socket.io requires both server and client integration to work. The flow would be to create socket instance on the server and upon connection, the socket will have access to events to either listen or receive them. The client does pretty much the same thing, only that the client will connect to the server's port.


const { Server } = require('socket.io');

const io = new Server(res.socket.server);

io.on('connection', socket => {
  console.log(socket.id + ' ==== connected');

  // creating a room name that's unique using both user's unique username

  socket.on('join', roomName => {
  let split = roomName.split('--with--'); // ['username2', 'username1']

  let unique = [...new Set(split)].sort((a, b) => (a < b ? -1 : 1)); // ['username1', 'username2']

  let updatedRoomName = `${unique[0]}--with--${unique[1]}`; // 'username1--with--username2'

   Array.from(socket.rooms)
        .filter(it => it !== socket.id)
        .forEach(id => {
          socket.leave(id);
          socket.removeAllListeners(`emitMessage`);
        });

   socket.join(updatedRoomName);

   socket.on(`emitMessage`, message => {
      Array.from(socket.rooms)
           .filter(it => it !== socket.id)
           .forEach(id => {
              socket.to(id).emit('onMessage', message);
           });
        });
      });

    socket.on('disconnect', () => {
      console.log(socket.id + ' ==== diconnected');
      socket.removeAllListeners();
     });
   });
Enter fullscreen mode Exit fullscreen mode

So let's walkthrough this a little. I found that a single connection can indeed join multiple rooms which is not what I want. When the user clicks on a chat, we want to emit join from the client side with a string that has both participants' unique usernames like username1--with--username2. This shows that username1 and username2 has to join a unique room. Since there are two clients here, the roomName is sorted in a way that is currentUser--with--whoTheUserWantsToChatWith. So a sort needs to happen so we generate a unique room name that the two participants can join the same one.

Socket.io automatically creates a unique id for a connected client and that id itself is a room. For example, logging socket.rooms when a client (let's take username1) with a socket.id (123123) is connected will give us

Array.from(socket.rooms) // ['123123']
Enter fullscreen mode Exit fullscreen mode

Before joining a private chat room with username2, first we need to make sure there is only 1 room which is the socket.id 123123 (like the example above), and no other rooms. If there are other rooms though, we need to leave that room with socket.leave(roomName) and remove emitMessage listener.

Then we'll join the client

socket.join(roomName)

console.log(Array.from(socket.rooms) // ['123123', 'username1--with--username2'])
Enter fullscreen mode Exit fullscreen mode

Now it's time to emit a message from the server to the client.

socket.on(`emitMessage`, message => {
      Array.from(socket.rooms)
           .filter(it => it !== socket.id)
           .forEach(id => {
              socket.to(id).emit('onMessage', message);
           });
        });
      });
Enter fullscreen mode Exit fullscreen mode

Here I'm listening an emitMessage event and a message parameter that client sent over. Now I only want to send the message IN the only room that client is in but not its own socket.id. Once a filter is done, there I am pointing to the unique room and emit an onMessage event that the client is listening with the message.

Also, not forgetting to disconnect the socket and removing all listeners once the socket is no longer in use.

On the client side, we're gonna set up event listeners that match the server's. In order to join a room, we would do something like this

// user: username2, anotherUser: username1
socket.emit('join', `${user}--with--${anotherUser}`);
Enter fullscreen mode Exit fullscreen mode

To send a message to the server, the client will need to emit emitMessage event with the message.

// send message
 socket.emit(`emitMessage`, "Hello world!");
Enter fullscreen mode Exit fullscreen mode

To receive message from the server that's being emitted from onMessage event:

// receive message
socket.on('onMessage', msg => {
console.log(msg) // "Hello world!"
      });
Enter fullscreen mode Exit fullscreen mode

Have fun coding :)

Top comments (2)

Collapse
 
isaacpro01 profile image
ssemugenyi isaac

Thanks Jiayi for this awesome article, I am experiencing a challenge in my react native app, hope you can help out, in the event that I listen for an 'onMessage', which kind of state or condition do I need to pass down to my useEffect inorder for it to update the recvMessages state when a new message is sent.
useEffect(()=>{
// receive message
socket.on('onMessage', message => {
setRecvMessages(prevState => GiftedChat.append(prevState, message));
});
}, [ ]);

As of now i can receive these messages on the other users ends

Collapse
 
isaacpro01 profile image
ssemugenyi isaac

Sorry, I meant the other user can receive messages