Further additions to the node.js/socket.io chat app

First of all let me wish all of you a Merry Christmas & Happy Holidays! I am using this rather peaceful time of the year to roll out some projects that I wanted to work on but haven't had the time and in light of this, I have yet another update to the node.js/socket.io chat application. This project has started off as a learning curve for me to get a good grasp on node.js and websockets and voilĂ , here we are, 8 months later and I'm still using the base of the project to learn new things. At this moment, the project - which is available on GitHub - has the following functionality/features:

  • People are able to join the chat server after entering their names
  • Usernames are unique - if a username is taken, a new suggestion is generated
  • User agent and geo location are both detected
  • People can setup a room. Room names are unique. One person can create on room and join one room
  • Users have to join a room to chat, except for the whisper feature.
  • Whisper messages are private messages sent between two users
  • With a WebSpeech enabled browsers, users can record their messages
  • Users can leave a room and/or disconnect from the server anytime
  • New: People joining the room will see the past 10 messages (chat history).
  • New: People will see an 'is typing' message when someone is typing a message.

The list above gives away the content of my post for today - I'm going to be discussing how I have added a chat history as well as a message indicating if a particular connected user is typing something.

Before getting into the details, I would like to thank the people who are reading my articles. Last week someone has reposted my article on Google+ and wrote the following about my previous post: "A great article ... Also, check his past posts and the source code. He's writing a chat application, which is now in able to detect GeoLocation and User Agents as well. Very nicely refactored using underscore.js."

Let's start discussing the chat history functionality first. In essence, what's happening in the background is relatively simple. I have create a global variable var chatHistory = {}; which is a simple JavaScript collection. Once a room is created - inside the createRoom function, I add an array to this collection with a key where the key is the name of the room - this means that the historical message will be stored against a particular room, it's a good way of encapsulating the messages.

chatHistory[socket.room] = [];  

Later on messages are pusehd into this array, and I do this inside the chat function. Once again, I am utilising underscore.js to see the size of a given array inside the collection and if it's greater then 10, old messages are removed via a simple splice:

if (_.size(chatHistory[socket.room]) > 10) {  
  chatHistory[socket.room].splice(0,1);
} else {
  chatHistory[socket.room].push(people[socket.id].name + ": " + msg);
}

Once thing that I nearly forgot was to delete the messages if a room is destroyed - it can easily be done using the following piece of code:

delete chatHistory[room.name];
Now the tricky bit was to introduce the 'is typing' feature - that is - displaying a message indiciating that a particular user is typing in a message. To achieve this feature I had to both add code to the frontend as well as the backend. Let's discuss the frontend portion first as that's a bit complex. In a nutshell, the following is happening:
  • a typing variable is created and it's set to false by default
  • if a user hits a key, a message is sent to the backend and, a timeout starts and also a visual indicator is shown on the GUI
  • if the timeout reaches its limit (5000 milliseconds) the backend is contacted again, and we also assume that the user is no longer typing
  • if the user keeps on hitting a key, the code assumes that he's typing
The code representation of the above is the following:
var typing = false;  
var timeout = undefined;

function timeoutFunction() {  
  typing = false;
  socket.emit("typing", false);
}

$("#msg").keypress(function(e){
  if (e.which !== 13) {
    if (typing === false && myRoomID !== null && $("#msg").is(":focus")) {
      typing = true;
      socket.emit("typing", true);
    } else {
      clearTimeout(timeout);
      timeout = setTimeout(timeoutFunction, 5000);
    }
  }
});

socket.on("isTyping", function(data) {  
  if (data.isTyping) {
    if ($("#"+data.person+"").length === 0) {
      $("#updates").append("<li id='"+ data.person +"'><span class='text-muted'><small><i class='fa fa-keyboard-o'></i>" + data.person + " is typing.</small></li>");
      timeout = setTimeout(timeoutFunction, 5000);
    }
  } else {
    $("#"+data.person+"").remove();
  }
});

The above code is unfortunately not enough. If you test this out, the time will not be reset if the user hits the enter key. In order to reset the timer the already existing chat function had to be extended (still on the frontend):

socket.on("chat", function(person, msg) {  
  $("#msgs").append("<li><strong><span class='text-success'>" + person.name + "</span></strong>: " + msg + "</li>");
  //clear typing field
   $("#"+person.name+"").remove();
   clearTimeout(timeout);
   timeout = setTimeout(timeoutFunction, 0);
});

As you can see from the above, I am also using the connected people's names as a unique identifier as an ID for the li elements. This may not be a best practice but it works for the time being.

On the backend, I have only added one function which is only a few lines long. It makes sure that the true and false values are re-emmited using the isTyping function, and it also makes sure that there is a valid user online. Without that first if statement this function would be executed for people who have not yet entered a username but are online - think about that as a safety net:

socket.on("typing", function(data) {  
  if (typeof people[socket.id] !== "undefined")
    io.sockets.in(socket.room).emit("isTyping", {isTyping: data, person: people[socket.id].name});
});

I have added one more enhancement. Previously I had a function that was triggered when someone has sent a message (i.e. the chatForm was submitted) that triggered the div container for the messages to auto-scroll. The problem with this was that the scroll has only worked for a given window, but not for the other clients. I have moved this function out from the submit method and placed it as a global function so all divs now simultanously update for all connected clients and this is achieved by binding to the DOMSubtreeModified event. The bad news? No support for this before IE8:

$("#conversation").bind("DOMSubtreeModified",function() {  
  $("#conversation").animate({
      scrollTop: $("#conversation")[0].scrollHeight
    });
});

As per usual, here are a few screenshots.

I am planning on setting up a demo for the chat app and I have made the necessary steps - I have setup an account with AppFog however unfortunately they do not support socket.io just yet. Once they do I will deploy this demo online. Until that, the latest codebase is on GitHub as always.

Show Comments