Creating a multiroom chat application

Let's take a brief refresher on the basic Chat App that we built during the course of the previous chapter:

Creating a multiroom chat application

This app effectively sets up a connection to the WebSocket server and lets us talk to random strangers who, for some reason, are loitering in the kitchen and using the Wi-Fi connection. What we want to do here is give these strangers (and ourselves) the possibility to pick separate chat rooms depending on what they are keen to talk about. Since we love programming, programming languages are of course going to be the be-all-and-end-all of what is on the menu.

Configuring the basic layout

In order to create a nice way to navigate between different chat rooms, we will use a tabbed layout, where each tab will correspond to a single chat room.

This means that we will need to make several changes to our HTML as well as the routing for our app. Start out by modifying the index.html file. Make sure that it looks like the following:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title></title>
    <link href="lib/ionic/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">
    <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above
    <link href="css/ionic.app.css" rel="stylesheet">
    -->
    <!-- ionic/angularjs js -->
    <script src="lib/ionic/js/ionic.bundle.js"></script>
    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>
    <!-- your app's js -->
    <script src="https://cdn.socket.io/socket.io-1.3.5.js"></script>
    <script src="js/app.services.js"></script>
    <script src="js/app.controllers.js"></script>
    <script src="js/app.directives.js"></script>
    <script src="js/app.js"></script>
  </head>
  <body ng-app="ionic-chat-app">
    <ion-nav-bar class="bar-stable">
      <ion-nav-back-button>
      </ion-nav-back-button>
    </ion-nav-bar>
    <ion-nav-view></ion-nav-view>
  </body>
</html>

I highlighted the most important part in the preceding code. Here, we created a navigation bar, which corresponds to a toolbar at the top of the screen in Ionic. If you are familiar with Android, you will recognize this as the action bar. Below this navigation bar, we then attached the actual view, which is currently loaded.

Next, we will attach a series of tabs to this layout, which will let us select the chat room that we wish to interact with. In the templates folder, create a file named tabs.html and make sure that it has the following content:

<ion-tabs class="tabs-icon-top tabs-color-active-positive">
  <!-- Node chat -->
  <ion-tab title="Node Chat"
  icon-off="ion-ios-chatboxes-outline"
  icon-on="ion-ios-chatboxes"
  href="#/app/node">
    <ion-nav-view name="node-view">
    </ion-nav-view>
  </ion-tab>
  <!-- Javascript chat -->
  <ion-tab title="JS Chat"
  icon-off="ion-ios-chatboxes-outline"
  icon-on="ion-ios-chatboxes"
  href="#/app/javascript">
    <ion-nav-view name="javascript-view">
    </ion-nav-view>
  </ion-tab>
  <!-- Haskell chat -->
  <ion-tab title="Haskell Chat"
  icon-off="ion-ios-chatboxes-outline"
  icon-on="ion-ios-chatboxes"
  href="#/app/haskell">
    <ion-nav-view name="haskell-view">
    </ion-nav-view>
  </ion-tab>
  <!-- Erlang chat -->
  <ion-tab title="Erlang Chat"
  icon-off="ion-ios-chatboxes-outline"
  icon-on="ion-ios-chatboxes"
  href="#/app/erlang">
    <ion-nav-view name="erlang-view">
    </ion-nav-view>
  </ion-tab>
  <!-- Scala chat -->
  <ion-tab title="Scala Chat"
  icon-off="ion-ios-chatboxes-outline"
  icon-on="ion-ios-chatboxes"
  href="#/app/scala">
    <ion-nav-view name="scala-view">
    </ion-nav-view>
  </ion-tab>
</ion-tabs>

Here, we used the ion-tabs directive, which in essence acts like a horizontal list consisting of ion-tab instances. Note how we associate each tab with a single language view and URL. The router will use both in order to deduce the exact state the app should be in when a tab is clicked. Let's see how it does so. Open the app.js file and make sure that it looks like the following:

angular.module('ionic-chat-app',
[
  'ionic',
  'ionic-chat-app-services',
  'ionic-chat-app-controllers'
])
.run(function ($ionicPlatform) {
  $ionicPlatform.ready(function () {
  if (window.cordova && window.cordova.plugins.Keyboard) {
    cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
  }
  if (window.StatusBar) {
    StatusBar.styleDefault();
  }
})
})
.config(function ($stateProvider, $urlRouterProvider) {
  // Configure the routing
  $stateProvider
  // Each tab has its own nav history stack:
  .state('app', {
     url: '/app',
     abstract: true,
     templateUrl: "templates/tabs.html"
  })
  .state('app.node', {
  url: '/node',
  views: {
    'node-view': {
      templateUrl: 'templates/app-chat.html',
      controller: 'ChatController',
      resolve: {
        chatRoom: function () {
          return 'node';
        }
      }
    }
  }
})
.state('app.javascript', {
  url: '/javascript',
  views: {
    'javascript-view': {
      templateUrl: 'templates/app-chat.html',
      controller: 'ChatController',
      resolve: {
        chatRoom: function () {
          return 'javascript';
        }
      }
    }
  }
})
.state('app.haskell', {
  url: '/haskell',
  views: {
    'haskell-view': {
      templateUrl: 'templates/app-chat.html',
      controller: 'ChatController',
      resolve: {
        chatRoom: function () {
          return 'haskell';
        }
      }
    }
  }
})
.state('app.erlang', {
  url: '/erlang',
  views: {
    'erlang-view': {
      templateUrl: 'templates/app-chat.html',
      controller: 'ChatController',
      resolve: {
        chatRoom: function () {
          return 'erlang';
        }
      }
    }
  }
})
.state('app.scala', {
  url: '/scala',
  views: {
    'scala-view': {
      templateUrl: 'templates/app-chat.html',
      controller: 'ChatController',
      resolve: {
        chatRoom: function () {
          return 'scala';
        }
      }
    }
  }
});
$urlRouterProvider.otherwise('/app/node');
})

Note how we coupled each single tab with a given application state. In doing so, we also tell the app how it should render the view under each tab. In our case, we have a common view for each single chat, templates/app-chat, which is familiar to us from our previous work. Let's take a look at the following code:

<ion-view view-title="chat">
  <ion-content>
    <div class="list">
      <a collection-repeat="message in messages"
      class="item item-avatar"
      ng-class="{'other-chatbox' : message.external}">
        <h2>{{message.name}}</h2>
        <p>{{message.text}}</p>
      </a>
    </div>

  </ion-content>
  <div class="bar bar-footer bar-balanced">
    <label class="item-input-wrapper">
      <input id="message-input"
      type="text"
      placeholder="Message"
      ng-model="inputMessage">
    </label>
    <button class="button button-small"
    ng-click="onSend()">
      Submit
    </button>
  </div>
</ion-view>

Finally, add some custom CSS to the css/style.css file in order to adjust the formatting according to our needs; this will also be familiar, as we saw this in the previous chapter:

#message-input {
  width: 100%;
}

.item-avatar {
  padding-left: 16px;
}

.other-chatbox {
  text-align: right;
}

Your view should now look like what's shown in the following screenshot:

Configuring the basic layout

Now, let's add some actual logic to our app in order to get the actual chat logic going. We are going to implement the namespace pattern that we discussed earlier in this chapter, adding one room for each tab. First, define the following controller in the app.controllers.js file, as follows:

angular.module('ionic-chat-app-controllers', [])
.controller('ChatController', function ($scope, ChatService, chatRoom) {
  var connection = ChatService.connect(chatRoom);
  // The chat messages
  $scope.messages = [];
  // Notify whenever a new user connects
  connection.on.userConnected(function (user) {
    $scope.messages.push({
      name: 'Chat Bot',
      text: 'A new user has connected!'
    });
    $scope.$apply();
  });
  // Whenever a new message appears, append it
  connection.on.messageReceived(function (message) {
    message.external = true;
    $scope.messages.push(message);
    $scope.$apply();
  });
  $scope.inputMessage = '';
  $scope.onSend = function () {
    $scope.messages.push({
      name: 'Me',
      text: $scope.inputMessage
    });
    // Send the message to the server
    connection.emit({
      name: 'Anonymous',
      text: $scope.inputMessage
    });
    // Clear the chatbox
    $scope.inputMessage = '';
  }
});

This controller works very much like what we are used to from the previous app, with the exception that it takes as a parameter the name of the chat room that we should connect to. This name is resolved in app.js in conjunction with the view being resolved, as follows:

.state('app.scala', {
  url: '/scala',
  views: {
    'scala-view': {
      templateUrl: 'templates/app-chat.html',
      controller: 'ChatController',
      resolve: {
        chatRoom: function () {
          return 'scala';
        }
      }
    }
  }
});

The relevant part is emphasized. We simply bind chatRoom to whatever the name of the corresponding language room for the view is in this case.

Finally, we need to expand the ChatService module in order to make sure that we can connect to an individual chat room. Open the app.services.js file and make sure that it has the following:

angular.module('ionic-chat-app-services', [])
.service('ChatService', function ChatService($rootScope) {
  function ChatConnection(chatName) {
    this.chatName = chatName;
    // Init the Websocket connection
    var socket = io.connect('http://localhost:8080/' + chatName);
    // Bridge events from the Websocket connection to the rootScope
    socket.on('UserConnectedEvent', function (user) {
      console.log('User connected:', user);
      $rootScope.$emit('UserConnectedEvent', user);
    });
    /*
    * Send a message to the server.
    * @param message
    */
    socket.on('MessageReceivedEvent', function (message) {
      console.log('Chat message received:', message);
      $rootScope.$emit('MessageReceivedEvent', message);
    });
    this.emit = function (message) {
      console.log('Sending chat message:', message);
      socket.emit('MessageSentEvent', message);
    };
    this.on = {
      userConnected: function (callback) {
        $rootScope.$on('UserConnectedEvent', function (event, user) {
          callback(user);
        });
      },
      messageReceived: function (callback) {
        $rootScope.$on('MessageReceivedEvent', function (event, message) {
          callback(message);
        });
      }
    }
  }
  /**
  * Establishes a new chat connection.
  *
  * @param chatName name of the chat room to connect to
  * @returns {ChatService.ChatConnection}
  */
  this.connect = function (chatName) {
    return new ChatConnection(chatName);
  }
});

In its previous incarnation, this service simply made a socket connection and serviced it. Here, we produce socket connections instead based on the namespace that we are connecting to. This allows us to set up a separate service instance for each individual socket.

That's all that we need for the client! Let's turn to the server in order to wrap things up.

Building the server

We have already seen how to create namespaces on the server. So, let's adjust our own accordingly. However, in order to make it much neater, let's do so by iterating over a list with all the names of the namespaces that we wish to create:

var http = require('http');
var url = require('url');
var fs = require('fs');
var server = http.createServer(function (req, res) {
  var parsedUrl = url.parse(req.url, true);
  switch (parsedUrl.pathname) {
    case '/':
    // Read the file into memory and push it to the client
    fs.readFile('index.html', function (err, content) {
      if (err) {
        res.writeHead(500);
        res.end();
      }
      else {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.end(content, 'utf-8');
      }
    });
    break;
  }
});
server.listen(8080);
server.on('listening', function () {
  console.log('Websocket server is listening on port', 8080);
});
// Connect the websocket handler to our server
var websocket = require('socket.io')(server);
// Configure the chat rooms
['node', 'javascript', 'haskell', 'erlang', 'scala'].forEach(function (chatRoom) {
  websocket.of('/' + chatRoom).on('connection', function (socket) {
    console.log("New user connected to", chatRoom);
    // Tell others a new user connected
    socket.broadcast.emit('UserConnectedEvent', null);
    // Bind event handler for incoming messages
    socket.on('MessageSentEvent', function (chatData) {
      console.log('Received new chat message', chatData);
      // By using the 'broadcast' connector, we will
      // send the message to everyone except the sender.
      socket.broadcast.emit('MessageReceivedEvent', chatData);
    });
  });
});

That's it! You can now start up your server, connect the app to server, and try it out. Pay special attention to your server console when you switch between the rooms. You will see the separate connections to separate rooms being made. Finally, see for yourself that the namespacing actually works. The messages that you send to one chat will only be visible to the users who are already connected to it.

Note

It is actually possible to partition the socket.io connections even further than what we did here. The socket.io connection also features the concept of rooms, which are essentially partitions of a single namespace. We recommend that you study this closely. The official documentation of socket.io contains a great deal of examples. To view this documentation, visit http://socket.io/docs/rooms-and-namespaces/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.10.1