If you're a regular reader of this blog, you will know that I have previouslywritten some posts about a node.js/socket.io chat application for which I have received a great feedback from you guys. I think what makes the article appealing and easily followable is the fact that I tried to document my learning curve with these technologies so it does help out people who are equally new to node.js/socket.io.
I constantly keep on updating the application and the code behind it and today it's time for another update. After a few hours of working on this I have not only added some new features but I have refactored some of my code as well as made the app look prettier (and I still can't call myself a designer)
Let's go through these new changes and enhancements now starting right off with the code changes to the backend.
I have started using the wonderful underscorejs.org library that provides functional programming support for JavaScript - it's especially useful when you do lot of operations on arrays and collections. Before, I was using code to manipulate collections that I either put together on my own or have copied from some other place. With underscore.js things got a lot simpler and neater. Consider the following:
Array.prototype.contains = function (k, callback) {
var self = this;
return (function check(i) {
if (i >= self.length) {
return callback(false);
}
if (self[i] === k) {
return callback(true);
}
return process.nextTick(check.bind(null, i + 1));
})(0);
};
The above contains
function determines whether a particular array/collection contains an element. This is how I was using it before:
room.people.contains(socket.id, function (found) {
if (found) {
//do something
}
});
Now, using underscore.js, I can remove the contains
function and utilise its built in contains function, which makes the whole code more clean and neat when searching for a particular socket ID in the room list:
if (_.contains(room.people, socket.id)) {
//do something
}
Also, consider the following - finding a particular element in the people's collection using my "old method" looked like the following:
for (var key in people) {
if (people[key].name === name) {
exists = true;
break;
}
}
With underscore.js the code looks much simpler:
_.find(people, function (key, value) {
if (key.name === name) return (exists = true);
});
I am utilising the find
, findWhere
, contains
, count
and without
functions mostly. My personal favourite is findWhere
which has helped me transform a complicated set of for
and if
statements into two lines only. When a client disconnects, I am removing the socket ID from a sockets
array using the following piece of code:
var o = _.findWhere(sockets, { id: socket.id });
sockets = _.without(sockets, o);
The code above first finds the right element inside the sockets
array by the id
as the parameter. Once found, using the without
function it updates itself by returns a copy of the array with all instances of the values removed.
Another cool feature that I have added to this release is the detection of the User Agent. This essentially means that every online user will now have either a mobile or a PC icon next to their username indicating what type of a device they are connected from. To achieve this functionality I am using the navigator.userAgent
value on the client side:
var device = 'desktop';
if (
navigator.userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i
)
) {
device = 'mobile';
}
This string is then sent to the backend and the device
value is stored as a key in the people
collection. To replace the string with an icon, I am using the amazing Font Awesome library:
$('#people').append(
'<li class="list-group-item"><span>' +
obj.name +
'</span> <i class="fa fa-' +
obj.device +
'"></i></li>'
);
where obj.name
is the user's username and obj.device
is the device identifier.
Of course this is not enough and I wanted to add more cool features. How nice would it be to see which location users are logged in from? Let's extend the application to have GeoLocation
capabilities as well. Using the standard HTML5 GeoLocation API and Yahoo's Geolookup I brewed the following together:
if (navigator.geolocation) { //get lat lon of user
navigator.geolocation.getCurrentPosition(positionSuccess, positionError, { enableHighAccuracy: true });
} else {
$("#errors").show();
$("#errors").append("Your browser is ancient and it doesn't support GeoLocation.");
}
function positionError(e) {
console.log(e);
}
function positionSuccess(position) {
var lat = position.coords.latitude;
var lon = position.coords.longitude;
//consult the yahoo service
$.ajax({
type: "GET",
url: "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20geo.placefinder%20where%20text%3D%22"+lat+"%2C"+lon+"%22%20and%20gflags%3D%22R%22&format=json",
dataType: "json",
success: function(data) {
socket.emit("countryUpdate", {country: data.query.results.Result.countrycode});
}
});
}
});
Once the Yahoo service successfully returns data, I emit a message out to my socket server and yet again, I add a new key to the people's collection, namely country
and I re-broadcast the people's list, now containing this extra key.
socket.on('countryUpdate', function (data) {
country = data.country.toLowerCase();
people[socket.id].country = country;
io.sockets.emit('update-people', { people: people, count: sizePeople });
});
Once this is done, I replace the country
string with a flag icon using the Flag Stripes library.
The final output of both the User Agent detection and the GeoLocation detection looks the following:
As you can see from the screencapture above, I have added the whisper function as well against each user, so now it's enough to click on that link and the chat message input box will automatically be updated with the right format in order to whisper to a particular user.
As always, all the code is up to date on GitHub and please see the setup instructions as well.