ChatBox: A Peer to Peer Chat Application.

Introduction

Welcome back JavaScript beginners! I hope you enjoyed my last tutorial to create a simple chat application and find it useful. I got many comments/question regarding the limited features of my previous tutorial. But all of those falls under the advanced level of creating a chat application, and I wanted to keep that as simple as possible for newbies to understand it easily.

Considering all the requests from users, I am trying to extend the existing simple chat application to the next level and trying to add/accommodate the different change requests or enhancements in this tutorial.

Here is the list of all new things that I am going to implement in this tutorial.

  1. Peer to peer chat.
  2. List of online users.
  3. Chat notification with the count of new messages.
  4. Notification sound when a new message comes in from selected and non-selected users.
  5. Assign a user-defined name to the user instead of random ID.
  6. Display username (My name) at top of online users list and as document title.
  7. Improved User experience by changing some CSS properties. Improved chatBox messages UI.

In this tutorial, you will learn how to create a Peer to Peer chat application using Node.js and Socket.IO. Let’s get started with the advanced level of the chat application, and I hope you will find it easy and useful to learn and implement.

Getting Started

I assume you all are now familiar with Node.js, Express.js, and Socket.IO and if not then refer my previous tutorial for more details. So I am skipping the details how to install and use Node Server, Node.js, NPM, Express.js, and Socket.IO.

Quick steps

Note: I am using Linux machine, so you may face difficulties following steps in case you are using a different operating system.

  1. Let’s create a folder/directory called chat-application to hold our application files.
  2. Now open NodeJS command prompt/terminal and navigate to your application directory i.e. chat-application.
    cd Desktop\projects\chat-application
  3. Create a jsonfile which is the manifest file that describes our project.
    {
        "name": "socket-chat-example",
        "version": "0.0.1",
        "description": "my first socket.io app",
        "dependencies": {
            "express": "^4.10.2",
            "http": "0.0.0",
            "path": "^0.12.7",
            "socket.io": "^2.1.1"
        }
    }
    
  4. Install Express, HTTP, socket.io and path npm modules in your working directory i.e. chat-application. To install all these npm modules/packages/dependencies execute below command.
    npm install
    
    Or simply 
    
    npm i
    

    The above command will result in below screen, you can see all the dependencies mentioned in package.json are installed in node_modules directory.

    chatbox

  5.  Now create a HTML file i.e. html which is used to serve the chat window in a browser.
    <!doctype html>
    <html>
     <head>
      <title>Chat Application</title>
      <link rel='stylesheet' href='style.css' type='text/css'/>
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
      <script src="chat.js"></script>
    </head>
    <body> 
      <div class="onlineUsersContainer">
          <div class="userInfo">Welcome <label id="myName">Guest</label> !!</div>
          <div><ul id="onlineUsers"></ul></div>
      </div>
      <div class="chatContainer">
        <ul id="messages"></ul>
        <span id="notifyTyping"></span>
        <form id="form" action="" onsubmit="return submitfunction();" > 
            <input type="hidden" id="user" value="" /><input id="m" autocomplete="off" onkeyup="notifyTyping();" placeholder="Type yor message here.." /><input type="submit" id="button" value="Send"/> 
        </form>
      </div>
    </body>
    </html>
    

    I have seen may people complaining about my previous application that it’s not working. Please make sure you reference to the correct/compatible versions of the JQuery and Socket.io (client) and server (node) versions.

    The whole page is divided into 2 parts i.e onlineUsersContainer and onlineUsersContainer. onlineUsersContainer holds the list of online users and onlineUsersContainer holds the chatBox, span to notify typing, textbox to type a message and a send button to send a message.

  6.  Now create a css file i.e. cssto have a better user experience. Put below css properties in style.css and save it.
    /*
     *  (C) 2018, All rights reserved. This software constitutes the trade secrets and confidential and proprietary information
     *  It is intended solely for use by Sandip Salunke. This code may not be copied or redistributed to third parties without 
     *  prior written authorization from Sandip Salunke
     */
    
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-size: 16px; font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif; }
    #form, #messages { display: none; }
    .userInfo { color: #FFF; font-weight: bold; font-size: 18px; width: 100%; border: 1px solid rgb(177, 113, 234); padding: 20px 8px; background: rgb(181, 125, 232); text-align: center; }
    .onlineUsersContainer { width: 20%; float: left; min-height: 869px; background-color: rgb(225, 203, 245); }
    .onlineUsersContainer ul li { list-style-type: none; cursor: pointer; margin: 0; padding: 12px; box-shadow: 0px 2px 8px #C28FF1; border-radius: 10px; }
    .onlineUsersContainer ul li::before { content: "• "; color: #2D9F0B; font-size: 20px; font-weight: bold; padding: 5px; }
    .onlineUsersContainer ul li.active { background: rgba(211, 176, 243, 0.92); }
    .onlineUsersContainer ul li:hover { background: rgb(214, 187, 239) }
    .onlineUsersContainer li a { color: #6C0DC3; text-decoration:none; }
    .chatContainer { width: 80%; float: left; min-height: 869px; border-left: 3px solid #B57DE8; }
    form { background: #2E8E12; padding: 3px; position: fixed; bottom: 0; width: 80%; }
    form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
    form #button { color:#FFF; background: #34BB0C; border: none; padding: 10px;  width: 9%; }
    #messages { list-style-type: none; margin: 0; padding: 0; position: absolute; bottom: 70px; float: left; width: 79%; }
    #messages li { padding: 5px 10px; }
    #notifyTyping { font-size: 12px; position: fixed; bottom: 42px; width: 100%; padding: 5px; }
    .chatNotificationCount { color: #FFF; display: none; float: right; border-radius: 12px; border: 1px solid #2E8E12; background: #34BB0C; padding: 2px 6px; }
    #messages li.chatMessageRight { border-radius: 25px; padding: 11px 18px; background: #56a8e3; color: #ffffff; position: relative; display: inline-block; max-width: 500px; float: right; clear: both; margin: 12px 0px; }
    #messages li.chatMessageRight:before { border-bottom-color: #56a8e3; border-radius: 20px / 10px; left: auto; right: -7px; content: ''; position: absolute; }
    #messages li.chatMessageRight:after { border: 8px solid transparent; border-bottom-color: #56a8e3; border-radius: 20px / 10px; left: auto; right: -4px; bottom: 5px; content: ''; position: absolute; }
    #messages li.chatMessageLeft { border-radius: 25px; padding: 11px 18px; background: #34BB0C; color: #ffffff; position: relative; display: inline-block; max-width: 500px; float: left; clear: both; margin: 12px; }
    #messages li.chatMessageLeft:before { border-bottom-color: #34BB0C; border-radius: 20px / 10px; right: auto; left: -7px; content: ''; position: absolute; }
    #messages li.chatMessageLeft:after { border: 8px solid transparent; border-bottom-color: #34BB0C; border-radius: 20px / 10px; right: auto; left: -4px; bottom: 5px; content: ''; position: absolute; }
    
  7. Now create index.js file to set up our application server. This will be your server-side node.js script file used to serve the request from all connected clients.
    /*
     *  (C) 2018, All rights reserved. This software constitutes the trade secrets and confidential and proprietary information
     *  It is intended solely for use by Sandip Salunke. This code may not be copied or redistributed to third parties without 
     *  prior written authorization from Sandip Salunke
     */
    
    var app = require('express')();
    var http = require('http').Server(app);
    var io = require('socket.io')(http);
    var path = require('path');
    
    var onlineUsers = [];
    
    // Initialize appication with route / (that means root of the application)
    app.get('/', function(req, res){
      var express=require('express');
      app.use(express.static(path.join(__dirname)));
      res.sendFile(path.join(__dirname, '../chat-application', 'index.html'));
    });
    
    // Register events on socket connection
    io.on('connection', function(socket){ 
    
      // Listen to chantMessage event sent by client and emit a chatMessage to the client
      socket.on('chatMessage', function(message){
        io.to(message.receiver).emit('chatMessage', message);
      });
    
      // Listen to notifyTyping event sent by client and emit a notifyTyping to the client
      socket.on('notifyTyping', function(sender, receiver){
        io.to(receiver.id).emit('notifyTyping', sender, receiver);
      });
    
      // Listen to newUser event sent by client and emit a newUser to the client with new list of online users
      socket.on('newUser', function(user){
        var newUser = {id: socket.id, name: user};
        onlineUsers.push(newUser);
        io.to(socket.id).emit('newUser', newUser);
        io.emit('onlineUsers', onlineUsers);
      });
    
      // Listen to disconnect event sent by client and emit userIsDisconnected and onlineUsers (with new list of online users) to the client 
      socket.on('disconnect', function(){
        onlineUsers.forEach(function(user, index){
          if(user.id === socket.id) {
            onlineUsers.splice(index, 1);
            io.emit('userIsDisconnected', socket.id);
            io.emit('onlineUsers', onlineUsers);
          }
        });
      });
    
    });
    
    // Listen application request on port 3000
    http.listen(3000, function(){
      console.log('listening on *:3000');
    });
    

    Above code translates into the following:

    • First block includes all the modules we are needing in our application
    • Second block initializes out application on server
    • Third block registers events on socket connection. Each even is described in detail below.
      • connection: Sent by all clients when they establish the connection with server.
      • chatMessage: Sent by a connected client on click of send button. This event is listened by server and emits another chatMessage event to the recipient client. This is peer to peer communication.
      • notifyTyping: Sent by a connected client onKeyup even of textbox and emits another notifyTyping event to the recipient client. This is peer to peer communication.
      • newUser: Sent by a connected client on page load to notify server that the client is connected with name say Sandip Salunke. Server then push this new user to the list of online users and emit another newUser to notify all other connected clients that the new user is connected. Server will also emit onlineUsers event to send the updated list of online users to all connected clients. This is broadcast communication.
      • disconnect: Sent by connected client when user disconnects/leaves the chatBox. Server then removes the user from the list of online users and emit onlineUsers event to send the updated list of online users to all connected clients. Server will also emit a userDisconnected event to all connected clients to notify that the user is disconnected. This is broadcast communication.
    • Last block make the HTTP server to listen application requests on port 3000
  8.  Create a chat.js file to hold our client side functionality. This is the simple javascript file and contains all client side functionalities and logic. This file is referenced in index.html.
    /*
     *  (C) 2018, All rights reserved. This software constitutes the trade secrets and confidential and proprietary information
     *  It is intended solely for use by Sandip Salunke. This code may not be copied or redistributed to third parties without 
     *  prior written authorization from Sandip Salunke
     */
    
    var socket = io();
    var allChatMessages = [];
    var chatNotificationCount = [];
    var myUser = {};
    var myFriend = {};
    
    // Document Ready function called automatically on page load
    $(document).ready(function(){
      loginMe();
    });
    
    // Function to ask user to supply his/her name before entering a chatbox
    function loginMe() {
      var person = prompt("Please enter your name:", "Sandip Salunke");
      if (/([^\s])/.test(person) && person != null && person != "") {
        //$('#user').val(person);
        socket.emit('newUser', person);
        document.title = person;
      } else {
        location.reload();
      }
    }
    
    // Function to be called when sent a message from chatbox
    function submitfunction() {
      var message = {};
      text = $('#m').val();
      
      if(text != '') {
        message.text = text;
        message.sender = myUser.id;
        message.receiver = myFriend.id;
    
        $('#messages').append('<li class="chatMessageRight">' + message.text + '</li>');
        
        if(allChatMessages[myFriend.id] != undefined) {
          allChatMessages[myFriend.id].push(message);
        } else {
          allChatMessages[myFriend.id] = new Array(message);
        }
        socket.emit('chatMessage', message);
      }
    
      $('#m').val('').focus();
      return false;
    }
    
    // function to emit an even to notify friend that I am typing a message 
    function notifyTyping() { 
      socket.emit('notifyTyping', myUser, myFriend);
    }
    
    // Load all messages for the selected user
    function loadChatBox(messages) {
      $('#messages').html('');
      messages.forEach(function(message){
        var cssClass = (message.sender == myUser.id) ? 'chatMessageRight' : 'chatMessageLeft';
        $('#messages').append('<li class="' + cssClass + '">' + message.text + '</li>');
      });
    }
    
    // Append a single chant message to the chatbox
    function appendChatMessage(message) {
      if(message.receiver == myUser.id && message.sender == myFriend.id) {
        playNewMessageAudio();
        var cssClass = (message.sender == myUser.id) ? 'chatMessageRight' : 'chatMessageLeft';
        $('#messages').append('<li class="' + cssClass + '">' + message.text + '</li>');
      } else {
        playNewMessageNotificationAudio();
        updateChatNotificationCount(message.sender);
      }
    
      if(allChatMessages[message.sender] != undefined) {
        allChatMessages[message.sender].push(message);
      } else {
        allChatMessages[message.sender] = new Array(message);
      }
    }
    
    // Function to play a audio when new message arrives on selected chatbox
    function playNewMessageAudio() {
      (new Audio('https://notificationsounds.com/soundfiles/8b16ebc056e613024c057be590b542eb/file-sounds-1113-unconvinced.mp3')).play();
    }
    
    // Function to play a audio when new message arrives on selected chatbox
    function playNewMessageNotificationAudio() {
      (new Audio('https://notificationsounds.com/soundfiles/dd458505749b2941217ddd59394240e8/file-sounds-1111-to-the-point.mp3')).play();
    }
    
    // Function to update chat notifocation count
    function updateChatNotificationCount(userId) {
      var count = (chatNotificationCount[userId] == undefined) ? 1 : chatNotificationCount[userId] + 1;
      chatNotificationCount[userId] = count;
      $('#' + userId + ' label.chatNotificationCount').html(count);
      $('#' + userId + ' label.chatNotificationCount').show();
    }
    
    // Function to clear chat notifocation count to 0
    function clearChatNotificationCount(userId) {
      chatNotificationCount[userId] = 0;
      $('#' + userId + ' label.chatNotificationCount').hide();
    } 
    
    
    // Function to be called when a friend is selected from the list of online users
    function selectUerChatBox(element, userId, userName) {
      myFriend.id = userId;
      myFriend.name = userName;
    
      $('#form').show();
      $('#messages').show();
      $('#onlineUsers li').removeClass('active');
      $(element).addClass('active');
      $('#notifyTyping').text('');
      $('#m').val('').focus();
    
      // Reset chat message count to 0
      clearChatNotificationCount(userId);
    
      // load all chat message for selected user 
      if(allChatMessages[userId] != undefined) {
        loadChatBox(allChatMessages[userId]);
      } else {
        $('#messages').html('');
      }
    }
    
    // ############# Event listeners and emitters ###############
    // Listen to newUser even to set client with the current user information
    socket.on('newUser', function(newUser){
      myUser = newUser;
      $('#myName').html(myUser.name);
    });
    
    // Listen to notifyTyping event to notify that the friend id typying a message
    socket.on('notifyTyping', function(sender, recipient){
      if(myFriend.id == sender.id) {
        $('#notifyTyping').text(sender.name + ' is typing ...');
      }
      setTimeout(function(){ $('#notifyTyping').text(''); }, 5000);
    });
    
    // Listen to onlineUsers event to update the list of online users
    socket.on('onlineUsers', function(onlineUsers){
      var usersList = '';
    
      if(onlineUsers.length == 2) {
        onlineUsers.forEach(function(user){
          if(myUser.id != user.id){
            myFriend.id = user.id;
            myFriend.name = user.name;
            $('#form').show();
            $('#messages').show();
          }
        });
      }
      
      onlineUsers.forEach(function(user){
        if(user.id != myUser.id) {
          var activeClass = (user.id == myFriend.id) ? 'active' : '';
          usersList += '<li id="' + user.id + '" class="' + activeClass + '" onclick="selectUerChatBox(this, \'' + user.id + '\', \'' + user.name + '\')"><a href="javascript:void(0)">' + user.name + '</a><label class="chatNotificationCount"></label></li>';
        }
      });
      $('#onlineUsers').html(usersList);
    });
    
    // Listen to chantMessage event to receive a message sent by my friend 
    socket.on('chatMessage', function(message){
      appendChatMessage(message);
    });
    
    // Listen to userIsDisconnected event to remove its chat history from chatbox
    socket.on('userIsDisconnected', function(userId){
      delete allChatMessages[userId];
      $('#form').hide();
      $('#messages').hide();
    });
    

    Above code translates into the following

    • Initialize socket Io using var socket = io();
    • allChatMessages[] array is used to hold all the messages sent/received to/from my friends.
      Here is the schema:

      {
          "FGongxDG0DkHov36AAAK": {[
              {
                  sender: "FGongxDG0DkHov36AAAK",
                  receiver: "KoiasKDa098asAklsn",
                  text: "Hi Sandip Salunke"
              },
              {
                  sender: "FGongxDG0DkHov36AAAK",
                  receiver: "KoiasKDa098asAklsn",
                  text: "Hi Sandip Salunke"
              }
          ]},
          "FGongxDG0DkHov36AAAK": {[
              {
                  sender: "KoiasKDa098asAklsn",
                  receiver: "FGongxDG0DkHov36AAAK",
                  text: "Hi Sandip Salunke"
              },
              {
                  sender: "KoiasKDa098asAklsn",
                  receiver: "FGongxDG0DkHov36AAAK",
                  text: "Hi Sandip Salunke"
              }
          ]}
      }
      
    • chatNotificationCount[] array is used to hold the count of unread messages from my friends.
    • myUser[] and myFriend[] arrays are used to hold users information and selected friend’s information respectively.
      Here is the schema:

      {
          name: "Sandip Salunke",
          id: "NhS_62uf0VtWTf4cAAAT"  
             // This is unique id assigned by socket.io 
             // and the same is used to send peer message.
      }
      
    • On page load i.e document.ready() call loginMe() function which will then ask user for his/her name and then on submission emits a newUser event to the server.
    • On form submit get the value of input box, push the chat message in allChatMessages[] array and then emits a chatMessage event along with message object.
      Here is the message schema:

      {
          sender: "FGongxDG0DkHov36AAAK",   // uniqueID assigned by socket.io
          receiver: "KoiasKDa098asAklsn",   // uniqueID assigned by socket.io
          text: "Hi Sandip Salunke"
      }
      
    • On keypress event of input box emit the notifyUser to the server along with name of user to display “Sandip Salunke is typing…” message to the user.
    • loadChatbox() function is called when user selects a friend from the list of online users. This function is used to load all chat message (chat history) sent/received to/from that selected friend.
    • appendCatMessage() function is called when a new message arrives from my friend. This function is called to append a new message to the selected chatBox. The new message received is also pushed into the allChatMessages[] array to maintain the history. An audio notification sound is also played on arrival of new message. This function also updates the chatNotification count if the user is inactive/not selected.
    • playNewMessageAudio() function is used to play notification sound when a new message received for active friend.
    • playNewMessageNotificationAudio() function is used to play notification sound when a new message received for inactive friend.
    • updateChatNotificationCount() function is used to maintain the count of unread messages for inactive friends and updates the same on UI when new message comes in.
    • clearChatNotificationCount() function is used to clear count of unread messages when user clicks on friend (or begins chat with friend) from the list of online users.
    • selectUserChatBox() function is called when user selects a friend from the list of online users. This function activates the chatBox, load chat history and clears the notification count.
    • newUser: This event listener receives the details (name, id) of user and assigns the same to the myUser array. The reason behind sending same events back and forth from client and server is that the name of the user is collected by client and the unique ID (socket Id) is assigned by server. In order to sync both the values at client as well as server and establish a initial connection we have to send these events back and forth.
    • notifyTyping: This event listener notifies the user that his/her friend is typing a message.
    • onlineUsers: This event listener build the list of online users and activates my friend’s chatBox by default if there is only one.
    • chatMessage: This event listener receives the new chat message from friends and append it to the chatbox.
  9.  All set, now it’s time to run the server to see how it works. Now go to your node command prompt and run the application with the following command.
    node index.js

    The above command will result in below screen, enter your name and hit ok then you are allow to enter the chatBox. Note: If you don’t provide a name it will not allow you to enter the chatBox.

    chatbox

  10.  Now go to your favorite web browser and open link (localhost:3000) in two different windows.chatbox
    chatbox
  11. Here we go, enjoy the chatBox.!!
  12. Here is Git repository and here is Demo

 

Quick References

I Hope you have enjoyed this tutorial and find it easy to learn, implement and use. I have written this tutorial based on my experience and knowledge. There might be some glitches present in this tutorial, please bear with me and let me know if you find any problem. Any suggestions/improvements in this tutorial are welcome. Thanks!

Special thanks to all readers and followers..!!

8 Comments ChatBox: A Peer to Peer Chat Application.

  1. Sushant

    Checked the demo and tried the application. Its nice to have such a simple chat application…
    Waiting for your next version!! All the best!

    Reply
  2. Harshal

    Hey Sandip , this one is awesome !! Scenario specific audio tunes are great!! Nice UI as well…
    Few suggestions. Can we make solution more modular, loosely coupled introducing some architecture? Could you add me as a contributor I can push some changes. See how it looks to you 🙂

    Reply
    1. Sandip Salunke

      Sure, Nice to know you are interested to make it better. Please do let me know what you are thinking with all possible details (Technical).

      Reply

Leave A Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.