13. Realtime Chat using Socket IO

We raise this concern in the previous chapter, that you need to refresh the page to see new chat messages. In this chapter, we will fix that issue. We will be making our chat realtime using Socket IO.

Installing Socket IO in Vue JS

So go ahead and install the library socket.io-client by running the following command in CMD opened in the web folder:

> npm install socket.io-client

Then include it in your AppHeader.vue component:

import { io } from 'socket.io-client'

And inside your mounted method, create a global reference to the instance of Socket IO. Our API will serve as a socket.

global.socketIO = io(this.$apiURL)

Then in your getUser method, after the this.$user = response.data.user statement, you need to write the following code:

socketIO.emit("connected", this.$user.email)

socketIO.on("sendMessage", async function (data) {
    const Toast = swal.mixin({
        toast: true,
        position: 'bottom-right',
        customClass: {
            popup: 'colored-toast'
        },
        showConfirmButton: false,
            timer: 10000,
            timerProgressBar: true
        })
    
    await Toast.fire({
        title: data.title
    })
})

This will first connect the user with the socket. Then it will start listening to the sendMessage event. As soon as that event is received, it will create a toast instance using the sweet alert library and display it on the bottom right.

The socket data will contain a message object which we will use later to automatically append it to the chat screen. And the title which we will show in the toast.

We are doing this in AppHeader.vue because we want the receiver to receive the notification no matter on which page he is. Because the AppHeader component will be included on every page.

Installing Socket IO on Node JS

Run the following command in CMD opened in api folder:

> npm install socket.io

Then add the following lines before listening to HTTP in server.js:

// sockets are used for realtime communication
const socketIO = require("socket.io")(http, {
    cors: {
        origin: ["http://localhost:8080"]
    }
});

// array that holds all connected users socket ID
global.users = [];

This will create an socketIO instance from an HTTP server, and set the CORS origin to your client app URL. Also, it creates a global users array where we will store all users’ socket IDs along with their email addresses.

Note: Socket ID is used to send the event to individual users.

Then write the following code after the database is connected.

socketIO.on("connection", function (socket) {

    socket.on("connected", function (email) {
        users[email] = socket.id;

        console.log(users);
    });
});

This will first make the connection to the socket. Then it will start listening to the “connected” event from any of the clients. The client must send its email address while firing this event. It will store the socket ID in users the array by setting the key as the client’s email address. So this will become an associative array.

Refresh your client app and check your CMD from api folder. You will see the user’s array having the socket ID stored as value and email address as the key.

socket users
socket users

Emit Event to Receiver Only

Since Socket IO is installed on both sides, client and server, we can now easily make our chat realtime using Socket IO. You just need to emit the event when the message is sent from the server. Open your api/modules/chat.js and create an socketIO property in it.

module.exports = {
    socketIO: null,

    ...
}

Then in your “server.js”, add the following line after the socket IO connection is made:

chat.socketIO = socketIO;

This will set the socket to the chat module too. Then in your chat.js inside the “/send” POST route, write the following lines after the messageObject is created:

if (typeof global.users[receiver.email] !== "undefined") {
    self.socketIO.to(global.users[receiver.email]).emit("sendMessage", {
        title: "New message has been received.",
        data: messageObject
    });
}

This will first check if the receiver is connected to the socket. If the receiver has opened the website and logged in, then he will automatically be connected and this condition becomes true.

socketIO.to receives the receiver’s socket ID as a parameter to make chat realtime. We are already storing all socket IDs in our global user’s array. So we can simply get it from the user’s array. And finally, we are emitting the “sendMessage” event and sending the title and message object as data.

To test this, you need to open the sender and receiver in 2 different browsers side-by-side. Send the message to the other and you will notice the toast pop-up on the bottom right of the receiver without having to refresh the page.

realtime new message toast
new message toast

You can see that a new message notification has been displayed on the bottom right of the receiver (left window). Now we need to make it to append the message if the chat page is open. To make our chat page realtime too, we need to use Socket IO on our ChatComponent too.

Create a Vuex Store

Since our socket IO event listener is in AppHeader and we want to append in an array that is in ChatComponent, we need to use Vuex Store. First, create a folder named “vuex” in your “web/src” folder. Then inside it, create a file named “store.js”.

Then run the following command in CMD opened in the web folder to install vuex:

> npm install vuex

Following will be the code of the store.js file:

import { createStore } from "vuex"

export default createStore({
    state() {
        return {
            messages: []
        }
    },

    mutations: {
        appendMessage (state, newMessage) {
            state.messages.push(newMessage)
        },

        prependMessage (state, newMessage) {
            state.messages.unshift(newMessage)
        },

		setMessages (state, newMessages) {
            state.messages = newMessages
        },
    },

    getters: {
        getMessages (state) {
            return state.messages
        }
    }
})

It includes a method named createStore from the vuex library. It creates an object from it and initializes messages the array. mutations will set the value, and getters are used to get the values.

Back in your ChatComponent, first, include this store:

import store from "../vuex/store"

We have to move back one step because vuex is in the src folder and ChatComponent is in the “src/components” folder.

Then remove messages: [], from data() the object from ChatComponent. And add the following to your export default object:

computed: {
    messages() {
        return store.getters.getMessages
    }
},

And in getData when all the messages are fetched, do this:

for (let a = 0; a < response.data.messages.length; a++) {
    store.commit("prependMessage", response.data.messages[a])
}

Head back to AppHeader.vue and include the store there as well.

import store from "../../vuex/store"

We have to move back two steps because vuex is in the src folder and AppHeader is in the “src/components/layouts” folder.

Then simply write the following lines in the sendMessage event listener in AppHeader.vue:

socketIO.on("sendMessage", async function (data) {
    if (self.$route.path == "/chat/" + data.data.sender.email) {
        store.commit("appendMessage", data.data)
    }

    ...
}

Run the app now, and open both chats. As soon as you send a message, you will see that the receiver sees it appended at the end of the chat. But the sender still has to refresh the page.

Include the store in the ChatComponent at the top:

import store from "../vuex/store"

After that, simply add the following line after the response status is “success” in the sendMessage method in your ChatComponent:

store.commit("appendMessage", response.data.messageObject)

Now the sender will also start seeing his sent message.

Show Realtime Unread Counter

Right now, if you are on the home page and someone sends a new message to you, you will see an alert notification on the bottom right. But you do not know WHO sent the message?

It should increment the unread counter we created in previous chapters. To do that, we again going to use contacts from the store.

First, include vuex/store in ContactComponent the same way we included in ChatComponent. Then remove the contacts: [] from data() object. And add contacts as a computed property:

computed: {
	contacts() {
		return store.getters.getContacts
	}
},

Then open your store.js and add the contacts array in state() object:

state() {
    return {
        ...
        
        contacts: []
    }
},

And also create its mutations and getters as well:

mutations: {
    ...

    setContacts (state, newContacts) {
        state.contacts = newContacts
    }
},

getters: {
    ...
    
    getContacts (state) {
        return state.contacts
    }
}

Then in your getData method, change the following line:

this.contacts = response.data.contacts

With this:

store.commit("setContacts", response.data.contacts)

Then in the AppHeader component when the sendMessage event is listened, write the following code:

let tempContacts = self.$user.contacts
for (let a = 0; a < tempContacts.length; a++) {
    if (tempContacts[a]._id == data.data.sender._id) {
        tempContacts[a].unreadMessages++
    }
}
store.commit("setContacts", tempContacts)

It will get all the contacts and increment by the unread counter to 1 for the user who sent the message.

The only problem you will be having right now is when you re-open the chat again, you will notice that the messages are being repeated. That is because we are appending messages in the store array when the chat is opened.

Detect when User Leaves the Component – Vue JS

We can set the messages array to empty as soon as the user leaves the ChatComponent. So add the following to your ChatComponent export default object:

watch: {
    $route: function (to, from) {
        if (from.href.includes("/chat/")) {
        	store.commit("setMessages", [])
        }
    }
},

This will first check if the href of the current route has “/chat/” in it. Since all chat pages will have “/chat/” in the beginning, so this condition becomes true when you leave the chat page and move to another page.

If this condition becomes true, then it will call the setMessages function and pass the empty array. We have already created this function before. So you can free to test now, this problem will not happen again.

So that’s it, we just made our chat realtime using Socket IO in our single page Vue JS app.





Please disable your adblocker or whitelist this site!