Node.js & Socket.io chat app with GeoLocation and User Agent support

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

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.