REAL-TIME MESSAGING WEB CHAT APPLICATION

chat realtime messaging back-end tutorial Kuzzle

 

This tutorial builds a Real-time messaging web chat application based on Kuzzle's Javascript SDK.

You will start implementing a simple single chat room based on Kuzzle real-time messaging and progressively add some additional features such as counting the users in the room or adding a multi-room support thanks to Kuzzle's persistence layer.

The tutorial uses AngularJs that handles all the presentation logic for us and let us focus on the business code but it should be pretty straight-forward to adapt to another framework if needed.

Table of Content

1 - Real-time messaging

The first step of this tutorial will create a single chat room using Kuzzle's Javascript SDK. Any user connected to the chat can send and receive the messages in real time.

2 - Adding the users count

In this second part of our chat demo tutorial, we will add a small new functionality displaying the number of users currently connected to the room.

3 - Adding some multi-room support

In this last part of our chat tutorial, we will extend our application to support some multiple rooms. The list of rooms will be persisted in Kuzzle.

The user can join several rooms at the same time and receive some messages on all of them at the same time. When a user is the last one to exit a room, it is automatically deleted, both from Kuzzle and from all users chat room lists.

 

Kuzzle chat demo 101 - Real-time messaging

The first step of this tutorial will create a single chat room using Kuzzle's Javascript SDK

Any user connected to the chat can send and receive the messages in real time.

Table of Contents

Sources

The complete source files can be found in our Github repository.

Include Kuzzle SDK's library

In order to use Kuzzle, you first need to include its Javascript SDK and its dependencies.

cd path/to/chat/demo
bower install

In your html, you can then include it.

101.html --
  [..]
  <script src="bower_components/kuzzle-sdk/dist/kuzzle.js"></script>
  <script src="config.js"></script>
  <script src="js/app.101.js"></script>
  <script>
    $(document).foundation();
  </script>
</body>

Connecting to Kuzzle

Connecting to Kuzzle is just a matter of instantiating a Kuzzle object.

We expose it as an Angular service.

js/app.101.js --
angular.module('KuzzleChatDemo', [])
  // setup kuzzle as an Angular service
  .factory('kuzzle', function () {
    return new Kuzzle(config.kuzzleUrl, {
      defaultIndex: config.appIndex,
      ioPort: 7512,
      wsPort: 7513
    });
  })
  [..]

Where config.kuzzleUrl is set to your Kuzzle server's end point, i.e. http://localhost:7512.
The defaultIndex is the index to use.

Preparing our Chat room and linking it to Kuzzle

We create a ChatRoom model object that exposes methods needed to our current functionalities: subscribe to a room and send a message.

Once again, we use Angular services and we expose the constructor as a service.

js/app.101.js --
angular.module('KuzzleChatDemo', [])
  [..]
  // Our chatroom demo object
  .factory('ChatRoom', ['$rootScope', 'kuzzleMessagesCollection', function ($rootScope, kuzzleMessagesCollection) {

    /** Constructor. Will be returned as an Angular service ...*/
    function ChatRoom (options) {...}

    /** Subscribe to the Kuzzle Room. ...*/
    ChatRoom.prototype.subscribe = function () {...}

    /** Sends a new message to Kuzzle ...*/
    ChatRoom.prototype.sendMessage = function (message, me) {...}

    return ChatRoom;
  }])
  [..]

The ChatRoom model object details

Constructor
/**
 * Constructor. Will be returned as an Angular service
 * @param {Object} options
 * @constructor
 */
function ChatRoom (options) {
  var opts = options || {};

  this.id = opts.id || null;
  this.messages = [];
  this.subscribed = false;

  this.kuzzleSubscription = null;

  this.subscribe();
}

The constructor takes an options object as parameter, in which the chat room id can be defined.

It also defines the chat room properties we will need.

  • messages: An array containing the messages we get on the chat.
  • subscribed: A boolean value that indicates if our chat room object has subscribed to Kuzzle.
  • kuzzleSubscription: Once the subscription to Kuzzle is established, this attribute is used to store the corresponding KuzzleRoom object.

⚠️ The vocabulary can be confusing between our application chat room and Kuzzle internal rooms on which it relies. We will use the term chat room to designate our application room (with the term "chat").

Finally, our constructor automatically subscribes to Kuzzle.

Subscribe method
ChatRoom.prototype.subscribe = function () {
  var self = this;

  this.subscribed = true;

  kuzzleMessagesCollection
    .subscribe(
      {term: {chatRoom: self.id}},
      function (err, response) {
        self.messages.push({
          color: response.result._source.color,
          nickName: response.result._source.nickName,
          content: response.result._source.content
        });
        $rootScope.$apply();
      }
    )
    .onDone(function (err, room) {
      if (err) {
        return console.error(err);
      }
      self.kuzzleSubscription = room;
    });
};

The subscribe method just registers the application to Kuzzle to receive the incoming messages using the KuzzleDataCollection subscribe method.

Kuzzle's subscribe method expects to receive three parameters:

  1. filters to apply to incoming documents before noticing the user back. Only documents matching these filters will be received by our application.
  2. an optional option object, not given here.
  3. a callback function which will be triggered when a document matching the filter is published or modified.

In our case, we define as a filter all the documents whose chatRoom id property matches our current room one ('Main room').

The callback function appends a new simple message object to the ChatRoom messages array.

Finally, we ask Kuzzle to be notified on messages the application sends by passing the option subscribeToSelf to true.

sendMessage method
/**
 * Sends a new message to Kuzzle
 * @param {String} message The message to send
 * @param {Object} me. An object representing the current user.
 */
ChatRoom.prototype.sendMessage = function (message, me) {
  kuzzleMessagesCollection
    .publish({
      content: message,
      color: me.color,
      nickName: me.nickName,
      chatRoom: this.id
    });
};

The sendMessage method is just a wrapper for the KuzzleDataCollection publish method.

? Note the chatRoom property in the submitted object that will match our subscription filter.

Handling the presentation logic

Now that our ChatRoom object is ready to use, we just need to prepare an instance to be used and reflect its changes on the web application.

Thanks to AngularJs bidirectional binding, we just need to build a ChatRoom model instance in our controller and add a couple of helper functions to let the user change his nickname and messages sent.

.controller('KuzzleChatController', ['$scope', 'ChatRoom', function ($scope, ChatRoom) {
  var chat = this;

  this.me = {
    nickName: 'Anonymous',
    color: '#' + Math.floor(Math.random() * 16777215).toString(16)
  };

  this.chatRoom = new ChatRoom({id: 'Main room'});

  this.sendMessage = function () {
    chat.chatRoom.sendMessage(chat.messageText, chat.me);
    chat.messageText = '';
  };

  $scope.updateNickName = function () {
    var newNickName = prompt('Please enter your new nickname:');

    if ($.trim(newNickName) !== '') {
      chat.me.nickName = newNickName;
    }
  };

 

See it in action

 

 

 

 

Kuzzle chat demo 102 - Adding the user count

In this second part of our chat demo tutorial, we will add a small new functionality displaying the number of users currently connected to the room.

Table of Contents

Sources

The complete source files can be found in our Github repository.

Extending the ChatRoom model object

We add a new refreshUserCount method to our ChatRoom constructor.

js/app.102.js --
/**
 * Get the number of simultaneous users connected to the room
 */
ChatRoom.prototype.refreshUserCount = function () {
  var self = this;

  if (!this.kuzzleSubscription) {
    return;
  }
  this.kuzzleSubscription.count(function (err, result) {
    self.userCount = result.count;
    $rootScope.$apply();
  });
};

This method calls the KuzzleRoom count method to get the number of subscribers.

We then need to slightly modify our constructor to add the new userCount property and call the newly created method.

function ChatRoom (options) {
  var
    opts = options || {},
    self = this;

  this.id = opts.id || null;
  this.messages = [];
  this.userCount = 0;
  this.subscribed = false;

  this.kuzzleSubscription = null;

  this.subscribe();

  this.refreshUserCount();
  setInterval(function () {
    self.refreshUserCount();
  }, 2000);
}

NB: The subscribers count method is not implemented as a real-time event in Kuzzle. We need to call it on a regular basis, here using a setInterval to periodically refresh the counter.

Adding the front-end logic

As we extend the business ChatRoom object bound to the front-end, no additional action is needed on the javascript side.

We can just use the newly defined property directly in our html template:

102.html --
<div class="row collapse statusbar">
  <div class="small-12 columns">Main Room:
    <ng-pluralize count="chat.chatRoom.userCount"
                  when="{'0': 'No user',
                    '1': '1 user',
                    'other': '{} users'}">
    </ng-pluralize>
  </div>
</div>

 

See it in action

 

 

 

Kuzzle chat demo 103 - Adding some multi-room support

In this last part of our chat tutorial, we will extend our application to support some multiple rooms.

The list of rooms will be persisted in Kuzzle.

The user can join several rooms at the same time and receive some messages on all of them at the same time.

When a user is the last one to exit a room, it is automatically deleted, both from Kuzzle and from all users chat room lists.

Table of Contents

Sources

The complete source files can be found in our Github repository.

Modifying our ChatRoom model

In this new step, we need to handle a list of ChatRooms. Among these chat rooms, only the ones the user has joined are considered active.

On our application level, this also means that only the active rooms have a Kuzzle subscription.

We need to slightly modify our ChatRoom model object to reflect this change.

Implementation details

Constructor
function ChatRoom (options) {
  var
    opts = options || {},
    self = this;

  this.id = opts.id || null;
  this.messages = [];
  this.userCount = 0;
  this.subscribed = false;

  this.kuzzleSubscription = null;

  if (opts.subscribe) {
    this.subscribe();
  }

  this.refreshUserCount();
  setInterval(function () {
    self.refreshUserCount();
  }, 2000);
}

We use a new subscribe property to our options object. We subscribe to the room notifications on Kuzzle side only if it set to true.

unsubscribe method

We also need a new method to allow the user to quit a chat room.

ChatRoom.prototype.unsubscribe = function () {
  this.kuzzleSubscription.unsubscribe();

  this.kuzzleSubscription = null;
  this.subscribed = false;
};

This method just calls the KuzzleRoom unsubscribe method and updates the current object state.

Adding a new model for the room list

.factory('kuzzleChatRoomListCollection', ['kuzzle', function (kuzzle) {
  return kuzzle.dataCollectionFactory('KuzzleChatDemoRoomList');
}])

.factory('ChatRoomList', ['$rootScope', 'kuzzleChatRoomListCollection', 'ChatRoom', function ($rootScope, kuzzleChatRoomListCollection, ChatRoom) {
  function ChatRoomList () {...}

  /** Real-time subscription to the room list collection. ...*/
  ChatRoomList.prototype.subscribe = function () {...}

  /** Filters the list of rooms to get only the active ones. ...*/
  ChatRoomList.prototype.refreshActive = function () {...}

  /** Gets the list of rooms persisted in Kuzzle database. ...*/
  ChatRoomList.prototype.getAll = function () {...}

  ChatRoomList.prototype.add = function (chatRoom) {...}

  ChatRoomList.prototype.del = function (roomId) {...}

  ChatRoomList.prototype.addNewRoom = function (name) {...}

  ChatRoomList.prototype.activeRoom = function (roomId) {...}

  ChatRoomList.prototype.unactiveRoom = function (roomId) {...}

  return ChatRoomList;
}])

We first declare a new Angular service that returns a KuzzleDataCollection object.

We will use this new collection to store and persist the list of available rooms.

We then declare a new constructor for our chatrooms list.

Implementation details

Constructor
function ChatRoomList () {
  this.all = {};
  this.active = [];
  this.current = null;

  this.getAll();
  this.subscribe();
}

The constructor defines the available properties:

  • all: An object containing all the available rooms. It is a key => value set where the key is the room id and the value is a ChatRoom model object instance.
  • active: An array containing only the active rooms (the ones the current user is listening).
  • current: A reference to the currently highlighted chat room.

The constructor then retrieves all the available rooms from Kuzzle by calling its getAll method and subscribes to the room list collection notifications.

subscribe method
/**
 * Real-time subscription to the room list collection.
 * Allows to be informed when a new room is created by another user.
 */
ChatRoomList.prototype.subscribe = function () {
  var self = this;

  kuzzleChatRoomListCollection
    .subscribe(
      {},
      function (err, response) {
        var result = response.result;

        if (response.action === 'delete') {
          self.del(result._id);
          return;
        }

        var chatRoom = new ChatRoom({
          id: result._id,
          name: result._source.name,
          subscribe: false
        });
        self.add(chatRoom);
      },
      {subscribeToSelf: true}
    )
};

The subscribe method allows to receive the real-time notifications updates from the room list collection.

? Note the empty object given as a filter. This is equivalent to "receive all" notifications.

? Contrary to our previous ChatRoom object where no document could be deleted, we here need to perform a different action where a room is removed. This can be achieved checking on the action property from the response object we get from Kuzzle.

getAll method
/**
 * Gets the list of rooms persisted in Kuzzle database.
 * Called during init to populate the initial list.
 */
ChatRoomList.prototype.getAll = function () {
  var self = this;

  if (!self.all['Main room']) {
    self.all['Main room'] = new ChatRoom({
      id: 'Main room',
      subscribe: true
    });
    self.refreshActive();
  }

  kuzzleChatRoomListCollection
    .fetchAllDocuments(function (err, result) {
      $.each(result.documents, function (k, doc) {
        if (!self.all[doc.id]) {
          self.all[doc.id] = new ChatRoom({
            id: doc.id,
            subscribe: false
          })
        }
      });

      self.refreshActive();

      $rootScope.$apply();
    });
};

This method is called only once, by the constructor. It uses the KuzzleDataCollection fetchAllDocuments method to retrieve the list of persisted documents for the collection.

From the returned list, it fills the ChatRoomList all property.

We also add a default "Main room" that will always be available.

refreshActive method

This internal method just filters all the available rooms to put in the active array only the ones that the user is listening to.

add method

This internal method adds a new ChatRoom object to the all collection and triggers the refreshActive filter.

This method applies to the ChatRoomList model object only and does not perform any action on Kuzzle.

del method

This internal method removes a ChatRoom object to the current object list.

This method applies to the ChatRoomList model object only and does not perform any action on Kuzzle.

addNewRoom method
ChatRoomList.prototype.addNewRoom = function (name) {
  var self = this;

  kuzzleChatRoomListCollection
    .documentFactory({_id: name})
    .save({}, function (err, result) {
      var chatRoom = new ChatRoom({
        id: result.id,
        subscribe: true
      });

      self.add(chatRoom);
      self.activeRoom(chatRoom.id);
    });
};

Given a room name, this method will create a new KuzzleDocument and persist it in Kuzzle.

Once the object is persisted, on the callback, it adds a new matching ChatRoom object to the internal list.

activeRoom and unactiveRoom methods

These methods allow to toggle a given chat room active state to let the user join or quit it.

When quitting a room, if the user was the last subscriber, the room is automatically deleted from Kuzzle.

Handling the presentation logic

Instead of binding a single chat room to our template, we can now use our new ChatRoomList model object and add few helpers that will trigger new actions.

.controller('KuzzleChatController', ['$scope', 'ChatRoom', 'ChatRoomList', function ($scope, ChatRoom, ChatRoomList) {
  var chat = this;

  this.me = {
    nickName: 'Anonymous',
    color: '#' + Math.floor(Math.random() * 16777215).toString(16)
  };

  this.rooms = new ChatRoomList();

  this.sendMessage = function () {
    chat.rooms.current.sendMessage(chat.messageText, chat.me);
    chat.messageText = '';
  };

  $scope.updateNickName = function () {
    var newNickName = prompt('Please enter your new nickname:');

    if ($.trim(newNickName) !== '') {
      chat.me.nickName = newNickName;
    }
  };

  $scope.addNewChatRoom = function () {
    var newChatRoom = prompt('Please a name for the new room:');

    if ($.trim(newChatRoom) !== '') {
      chat.rooms.addNewRoom(newChatRoom);
    }
  };

  $scope.activeRoom = function (id) {
    chat.rooms.activeRoom(id);
    $('#inputChatMessage').focus();
  };

  $scope.unactiveRoom = function (id) {
    chat.rooms.unactiveRoom(id);
  };
}]);

 

See it in action