Further additions to the Node.js/socket.io chat app
Older Article
This article was published 13 years ago. Some information may be outdated or no longer applicable.
Merry Christmas and Happy Holidays! I’m using this quiet stretch to ship some projects I’ve been sitting on. And yes, that means another update to the node.js/socket.io chat application.
This project started as a learning exercise to get a grip on node.js and websockets. Eight months later, I’m still building on the same foundation. The project is available on GitHub and now has these features:
- People join the chat server after entering their names
- Usernames are unique. If one’s taken, a new suggestion gets generated
- User agent and geo location are both detected
- People can set up a room. Room names are unique. One person can create one 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 browser, users can record their messages
- Users can leave a room and/or disconnect from the server anytime
- New: People joining a room will see the past 10 messages (chat history)
- New: People will see an ‘is typing’ message when someone is typing
That list gives away what I’m covering here: chat history and a typing indicator.
Before getting into the details, a quick thank you to the people reading my articles. Last week someone reposted my article on Google+ and wrote: “A great article … Also, check his past posts and the source code. He’s writing a chat application, which is now able to detect GeoLocation and User Agents as well. Very nicely refactored using underscore.js.”
Let’s start with chat history. The mechanism is simple. I created a global variable var chatHistory = {}; (a plain JavaScript collection). When a room gets created, inside the createRoom function, I add an array keyed by the room name. This keeps historical messages encapsulated per room:
chatHistory[socket.room] = [];
Messages get pushed into this array inside the chat function. I’m using underscore.js to check the array size, and if it exceeds 10, old messages get sliced off with splice:
if (_.size(chatHistory[socket.room]) > 10) {
chatHistory[socket.room].splice(0, 1);
} else {
chatHistory[socket.room].push(people[socket.id].name + ': ' + msg);
}
One thing I nearly forgot: deleting the messages when a room gets destroyed. Easy enough:
delete chatHistory[room.name];
The ‘is typing’ feature was trickier. It required changes on both the frontend and the backend. Let’s tackle the frontend first, since it’s more involved.
Here’s the gist:
- A
typingvariable starts as false - When a user hits a key, a message fires to the backend, a timeout starts, and a visual indicator appears
- If the timeout hits its limit (5000 milliseconds), the backend gets contacted again, and we assume the user stopped typing
- If the user keeps pressing keys, we assume they’re still typing
The code:
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();
}
});
That’s not enough on its own, though. The timer doesn’t reset when the user hits enter. To fix that, the existing chat function needed extending (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);
});
I’m using connected users’ names as unique IDs for the li elements. Not best practice, but it works for now.
On the backend, I added a single short function. It re-emits the true and false values via the isTyping event and checks that a valid user is online. Without that first if statement, the function would fire for people who haven’t entered a username yet but are connected. Think of it 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’ve added one more tweak. Previously, a function triggered the message container to auto-scroll when someone sent a message (i.e. the chatForm was submitted). The problem? It only scrolled for the sender’s window, not other clients. I pulled that function out of the submit method and made it global by binding to the DOMSubtreeModified event. Now all divs scroll simultaneously for all connected clients. The bad news? No support before IE8:
$('#conversation').bind('DOMSubtreeModified', function () {
$('#conversation').animate({
scrollTop: $('#conversation')[0].scrollHeight,
});
});
Here are a few screenshots:
The latest codebase is on GitHub as always.