Skip to main content

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

4 min read

Older Article

This article was published 13 years ago. Some information may be outdated or no longer applicable.

If you’ve been reading this blog, you’ll know I’ve previously written some posts about a Node.js/Socket.io chat application. I’ve had great feedback on those. I think what makes them work is that I documented my learning curve with these technologies, which helps people who are equally new to Node.js/Socket.io.

I keep updating the application and the code behind it. Today it’s time for another round. After a few hours of work, I’ve added new features, refactored some code, and made the app look better (I still can’t call myself a designer).

Let’s walk through these changes, starting with the backend code.

I’ve started using the wonderful underscorejs.org library for functional programming support in JavaScript. It’s especially useful for operations on arrays and collections. Before, I was using hand-rolled code (or code grabbed from other places) to manipulate collections. With Underscore.js, things got simpler and cleaner. Consider this:

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);
};

That contains function checks whether an array holds a particular element. Here’s how I used it before:

room.people.contains(socket.id, function (found) {
  if (found) {
    //do something
  }
});

With Underscore.js, I can scrap the custom contains function and use its built-in version. The whole thing becomes cleaner when searching for a particular socket ID in the room list:

if (_.contains(room.people, socket.id)) {
  //do something
}

Finding a particular element in the people collection using my old method looked like this:

for (var key in people) {
  if (people[key].name === name) {
    exists = true;
    break;
  }
}

With Underscore.js, much simpler:

_.find(people, function (key, value) {
  if (key.name === name) return (exists = true);
});

I’m mostly using find, findWhere, contains, count, and without. My personal favourite is findWhere, which collapsed a complicated set of for and if statements into two lines. When a client disconnects, I remove the socket ID from a sockets array like this:

var o = _.findWhere(sockets, { id: socket.id });
sockets = _.without(sockets, o);

The code first finds the right element inside the sockets array using id as the parameter. Then without returns a copy of the array with that element removed.

Another feature I’ve added to this release: User Agent detection. Every online user now has either a mobile or PC icon next to their username, showing what type of device they’re connected from. I’m using navigator.userAgent on the client side:

var device = 'desktop';
if (
  navigator.userAgent.match(
    /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i
  )
) {
  device = 'mobile';
}

This string gets sent to the backend, and the device value is stored as a key in the people collection. To swap the string for an icon, I’m using the Font Awesome library:

$('#people').append(
  '<li class="list-group-item"><span>' +
    obj.name +
    '</span> <i class="fa fa-' +
    obj.device +
    '"></i></li>'
);

Here obj.name is the username and obj.device is the device identifier.

But that wasn’t enough. How nice would it be to see where users are logging in from? Let’s bolt on GeoLocation. Using the standard HTML5 GeoLocation API and Yahoo’s Geolookup, I put together the following:

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 Yahoo’s service returns data, I emit a message to my socket server. I add a new key to the people collection (country) and re-broadcast the people list with 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 });
});

Then I replace the country string with a flag icon using the Flag Stripes library.

I’ve also added the whisper function against each user. Click the link and the chat message input box automatically fills with the right format for whispering to that particular user.

All the code is up to date on GitHub. Check the setup instructions as well.