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:
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:
typing
variable is created and it's set to false by defaultThe 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.