Advanced Angular Chatroom Tutorial: No Database or Sockets Required

Leave a comment Standard

This is an expansion on my basic angular chatroom to add new functionality. In this version of the chat room there is now a popup color picker to change font colors, a way to change from one channel to another, click-able hyperlinks, HTML messages and emoticons.

1. Create a PubNub Account

To get started you will need to create a PubNub account. This will handle all the chat messages for you so you don’t need a server or database in order to quickly and easily create your chat room. There is a free plan and several paid for plans available for you to choose from. Once your account is created you’ll need to find your subscribe key and publish key.

2. Create the chat file

Now you’re going to create the main chat file. This will display your chat room. At the top of the file replace the subscribe_key and publish_key with the one you see in your newly created account.

index.html

<!doctype html>
<html ng-app="angular_chat">
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
    <script src="http://code.jquery.com/jquery-1.9.1.js"></script>
    <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.6.0.js"></script>
    <div pub-key="PUB_KEY_GOES_HERE" sub-key="SUB_KEY_GOES_HERE" ssl="off" origin="pubsub.pubnub.com" id="pubnub"></div>
    <script src="http://cdn.pubnub.com/pubnub-3.1.min.js"></script>
    <script src="angular-chat.js"></script>
    <script src="angular-colorpicker.js"></script>
    <script src="angular-colorpicker-modal.js"></script>
    <link rel="stylesheet" href="style.css" rel="stylesheet">
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
  </head>
  <body>
  <div class='container-fluid' ng-controller="chatCtrl">
    <div ng-include="'chatHeader.html'"></div>
    <div ng-Show="!loggedIn" id="login">
      <h2>Login</h2>
      <label for="username">
        <b>Username:</b>
      </label>
      <label>
        <input type="text" ng-model="message.username" />
      </label>
      <span ng-click="attemptLogin()">
        <i></i> Go Chat
      </span>
    </div>
    <div ng-Show="loggedIn" id="chat">
      <table>
        <tr ng-repeat="chat in chatMessages">
            <td style="width: 30%;">
              <b>{{chat.username}}</b><br/>
              <small>
               {{chat.date}} @ {{chat.time}}
              </small>
            </td>
            <td colspan="2">
              <font color="{{chat.color}}"><span ng-bind-html-unsafe="chat.text"></span></font>
            </td>
        </tr>
        <tr ng-show="chatMessages.length == 0">
          <td colspan="3">No messages yet!</td>
        </tr>
      </table>
    </div>
    <form ng-Show="loggedIn" ui-keypress="{13:'postMessage()'}">
      <div id="inputMessage">
        <div ng-controller="ColorPickerCtrl">
          <script type='text/ng-template' id='fontcolor.html'>
            <div>
              <h3>Change Font Color</h3>
            </div>
            <div ng-modal="colorPicker">
              <ng-color-circle height="250" width="250" model-object="colorpicker" model-property="color"></ng-color-circle>
              <div style="background-color:{{colorpicker.color}}">Selected Color</div>
            </div>
            <div>
              <button ng-click="ok()">Done</button>
            </div>
          </script>
          <span ng-click="open()" id="fontModalButton" style="background:{{color}} !important;">
            <i></i>
          </span>
        </div>
        <span ng-click="toggleEmoticons = !toggleEmoticons">
          <img ng-src="{{emoticon_url}}{{emoticons[':)']}}" />
        </span>
        <input type="text" placeholder="Say hello!" ng-model="message.text" />
        <span ng-click="postMessage()">
          <i></i> Post
        </span>
      </div>
      <span ng-show="toggleEmoticons == true" id="emoticonList" ng-repeat="(key, image) in emoticons">
        <img ng-src="{{emoticon_url}}{{image}}" ng-click="insertEmoticon(key)" />
      </span>
    </form>
    <div ng-include="'chatFooter.html'"></div>
  </div>
  </body>
</html>

3. Create the header and footer files

chatHeader.html

<div>
  <h1>Ng-Angular Chat</h1>
  <h5><i></i> using Bootstrap and PubNub</h5>
  <p>&nbsp;</p>
  <div ng-show="realtimeStatus == 0">
    <span><i></i></span>
  </div>
  <div ng-show="realtimeStatus == 1">
    <span><i></i></span>
  </div>
  <div ng-show="realtimeStatus == 2">
    <span><i></i></span>
  </div>
  <div id="channelDropdown"><select ng-model="selectedChannel" ng-change="initChat(selectedChannel)" ng-options="obj.value as obj.text for obj in chatChannels"></select></div>
  <div ng-show="loggedIn" id="logout">
    <span ng-click="attemptLogout()"><i></i> Logout</span>
  </div>
</div>
<p>&nbsp;</p>
<div ng-show="errorMsg">
  <i></i> <b>Error:</b> {{errorMsg}}
</div>

chatFooter.html

<div>
  2013 &copy; design1online.com
</div>

4. Create the stylesheet

This example is using bootstrap however we still need some additional tweaks to get it looks just right.

style.css

#chat {
 height: 31em; 
 overflow: auto;
 background-color: #EEE;
 border-radius: .25em;
 margin-bottom: 1em;
}

#logout {
  float: right;
  margin-top: -1.35em;
}

#channelDropdown {
  margin-right: .3em;
  margin-left: .3em;
  float: left;
  margin-top: -1.35em;
}

#fontModalButton {
  height: 21px;
}

#selectedColor {
  width: 100%;
  border: 1px solid #000000; 
}

.alert {
  padding: 8px !important;
}

.status {
  float: left;
  margin-top: -1em;
}

.left {
  float: left;
  margin-right: .25em;
}

.icon {
  cursor: pointer;
}

5. Create the angular files

angular-chat.js

/***
 * File: angular-chat.js
 * Author: Jade Krafsig
 * Source: design1online.com, LLC
 * License: GNU Public License
 ***/

/***
 * Purpose: load bootstrap ui angular modules
 * Precondition: none
 * Postcondition: modules loaded
 ***/
angular.module('angular_chat', ['ui.bootstrap', 'directive.colorPicker']);

/***
 * Purpose: load the existing chat logs
 * Precondition: none
 * Postcondition: chat logs have been loaded
 ***/
function chatCtrl($scope, $http) { 

  /***
   * Configurable global variables
   ***/
  $scope.messageLimit = 50;
  $scope.defaultUsername = "Guest";
  $scope.selectedColor = "#000000";

  $scope.chatChannels = [
    {
      text: "Basic Channel",
      value: "angular_chat", 
      default: true
    },
    {
      text: "Advanced Channel",
      value: "angular_chat_advanced"
    },
  ];

  $scope.emoticon_url = "http://www.freesmileys.org/smileys/smiley-basic/";
  $scope.emoticons = {
    ':-)' : 'biggrin.gif',
    ':)' : 'biggrin.gif',
    ':D' : 'laugh.gif',
    ':-D' : 'laugh.gif',
    ':-|' : 'mellow.gif',
    ':|' : 'mellow.gif',
    ':-p' : 'tongue.gif',
    ':p' : 'tongue.gif',
    ':-(' : 'sad.gif',
    ':(' : 'sad.gif'
  };

  /***
   * Static global variables, do not change
   ***/
  $scope.loggedIn = false;
  $scope.errorMsg;
  $scope.realtimeStatus = 0;
  $scope.toggleEmoticons = false;

  /***
   * Purpose: clear the message object
   * Precondition: none
   * Postcondition: message object has been reset
   ***/
  $scope.clearMsg = function() {
    $scope.message = {
      username: $scope.defaultUsername,
      email: 'n/a',
      text: ''
    };
  }

  /***
   * Purpose: listen for a font color change broadcast
   * Precondition: color value
   * Postcondition: selected color has been set
   ***/
  $scope.$on('colorChange', function(obj, color){
    $scope.selectedColor = color;
  });    

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
  $scope.chatLogs = function() {
    PUBNUB.history( {
      channel : $scope.selectedChannel,
      limit   : $scope.messageLimit
    }, function(messages) {
      // Shows All Messages
      $scope.$apply(function(){
        $scope.chatMessages = messages.reverse();          
      }); 
    });
   }

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
   $scope.attemptLogin = function() {
    $scope.errorMsg = "";

    if (!$scope.message.username) {
      $scope.errorMsg = "You must enter a username.";
      return;
    }

    if (!$scope.realtimeStatus) {
      $scope.errorMsg = "You're not connect to PubNub.";
      return;
    }

    $scope.loggedIn = true;
   }

  /***
   * Purpose: remove error message formatting when the message input changes
   * Precondition: none
   * Postcondition: error message class removed from message input
   ***/
  $scope.$watch('message.text', function(newValue, oldValue) {
    if (newValue) {
      $("#inputMessage").removeClass("error");
      $scope.errorMsg = "";
    }
  }, true);

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.postMessage = function() {

    //make sure they are logged in
    if (!$scope.loggedIn) {
      $scope.errorMsg = "You must login first.";
      return;
    }

    //make sure they enter a chat message
    if (!$scope.message.text) {
      $scope.errorMsg = "You must enter a message.";
      $("#inputMessage").addClass("error");
      return;
    }

    //set the message date
    d = new Date();
    $scope.message.date = d.getDay() + "/" + d.getMonth() + "/" + d.getFullYear();
    $scope.message.time = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();

    //change font color
    $scope.message.color = $scope.selectedColor;

    //replace hyperlinks with clickable urls
    $scope.message.text = $scope.replaceURLWithLink($scope.message.text);

    //replace smileys
    $scope.message.text = $scope.replaceEmoticons($scope.message.text);

    //send the message to the selected channel
    PUBNUB.publish({
      channel : $scope.selectedChannel,
      message : $scope.message
    });

    //reset the message input box
    $scope.message.text = "";
  };

  /***
   * Purpose: connect and access pubnub channel
   * Preconditions: pubnub js file init
   * Postconditions: pubnub is waiting and ready
   ***/
  $scope.initChat = function(newChannel) {
    if (newChannel)
      $scope.selectedChannel = newChannel;

    if (!$scope.chatChannels.length) {
      $scope.errorMsg = "Missing chat channels, check config.";
      return;
    }

    if (!$scope.selectedChannel) {
      $scope.errorMsg = "You must select a channel.";
      return;
    }

    //clear out any chat messages from a previous channel
    $scope.chatMessages = Array();

    //unsubscribe to the active channel
    angular.forEach($scope.chatChannels, function(channel, key){
      PUBNUB.unsubscribe({channel: channel.value});
    });

    //subscribe to the selected channel
    PUBNUB.subscribe({
      channel    : $scope.selectedChannel,
      restore    : false, 
      callback   : function(message) { 
          //update messages with the new message
          $scope.$apply(function(){
          $scope.chatMessages.unshift(message);          
        }); 
      },

      error      : function(data) {
        $scope.errorMsg = data;
      },

      disconnect : function() {   
        $scope.$apply(function(){
          $scope.realtimeStatus = 0;
        });
      },

      reconnect  : function() {   
        $scope.$apply(function(){
          $scope.realtimeStatus = 1;
        });
      },

      connect    : function() {
        $scope.$apply(function(){
          $scope.realtimeStatus = 2;
          //load the chat logs
          $scope.chatLogs();
        });
      }
    });
  }

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.attemptLogout = function() {
    $("#inputMessage").removeClass("error");
    $scope.clearMsg();
    $scope.loggedIn = false;
  }

  /***
   * Purpose: set the chat channel to the default channel value
   * Precondition: at least one channel defined
   * Postcondition: selectedChannel set to the default
   ***/
  $scope.defaultChannel = function() {
    var chatChannel;

    angular.forEach($scope.chatChannels, function(channel, key){

     if (!chatChannel)
      chatChannel = channel;

     if (channel.default) {
      chatChannel = channel;
      return;
     }
    });

    $scope.selectedChannel = chatChannel.value;
  }

  /***
   * Purpose: add an emoticon to the message input
   * Precondition: emoticon has been selected
   * Postcondition: emoticon value appended to message input
   ***/
  $scope.insertEmoticon = function(selected) {
    $scope.message.text = $scope.message.text + " " + selected + " ";
  }

  /***
   * Purpose: regex to replace emoticons with image html
   * Precondition: text to replace emoticons in
   * Postcondition: any emoticons found have been replaced with html images
   ***/
  $scope.replaceEmoticons = function(text) {
    patterns = [];
    metachars = /[[\]{}()*+?.\\|^$\-,&#\s]/g;

    // build a regex pattern for each defined property
    for (var i in $scope.emoticons) {
      if ($scope.emoticons.hasOwnProperty(i)){ // escape metacharacters
        patterns.push('('+i.replace(metachars, "\\$&")+')');
      }
    }

    // build the regular expression and replace
    return text.replace(new RegExp(patterns.join('|'),'g'), function (match) {
      return typeof $scope.emoticons[match] != 'undefined' ?
        '<img src="'+$scope.emoticon_url+$scope.emoticons[match]+'"/>' :
        match;
    });
  }

  /***
   * Purpose: replace url text with a clickable link (opens in a new window)
   * Precondition: text to replace urls in
   * Postcondition: urls have been replaced with a clickable hyperlink
   ***/
  $scope.replaceURLWithLink = function(text) {
    var exp = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
    return text.replace(exp,"<a href='$1' target='_blank'>$1</a>"); 
  }

  /***
   * Purpose: Initialize the chatroom
   * Preconditions: none
   * Postconditions: none
   ***/
  $scope.clearMsg();
  $scope.defaultChannel();
  $scope.initChat();

}

angular-colorpicker-modal.js

/***
 * File: angular-colorpicker-modal.js
 * Author: Jade Krafsig
 * Source: design1online.com, LLC
 * License: GNU Public License
 ***/

/***
 * Purpose: modal template controller
 * Preconditions: $rootScope, $modal and ui-bootstrap module
 * Postconditions: creates a modal window using the specified template
 *  and broadcasts the selected color back to the main controller when
 *  the window is closed
 ***/
var ColorPickerCtrl = function ($rootScope, $scope, $modal) {

  $scope.open = function () {
    $scope.color = "#000000";

    var modalInstance = $modal.open({
      templateUrl: 'fontcolor.html',
      controller: 'ColorpickerModalCtrl'
    });

    //when the color changes broadcast back to the main chat controller
    modalInstance.result.then(function (data) {
      $scope.color = data.color;
      $rootScope.$broadcast('colorChange',  $scope.color);
    });
  }
}

//create a modal popup window and pass it back to the modal parent controller
/***
 * Purpose: modal controller
 * Preconditions: instance of the modal being created
 * Postconditions: functionality defined for all modals created of $modalInstance
 ***/
var ColorpickerModalCtrl = function ($scope, $modalInstance) {
  $scope.colorpicker = {};
  $scope.colorpicker.color = "#000000";

  $scope.ok = function () {
    $modalInstance.close({color: $scope.colorpicker.color});
  };
};

angular-colorpicker.js

/***
 * Method taken from Brian Grinstead and modified the return 
 * of rounded r,g and b.
 * @https://github.com/bgrins/TinyColor/blob/master/tinycolor.js
 ***/
(function(angular) {
  var ngColorPicker = angular.module('directive.colorPicker', []);

  function hsvToRgb(h, s, v) {
    h*=6;
    var i = ~~h,
      f = h - i,
      p = v * (1 - s),
      q = v * (1 - f * s),
      t = v * (1 - (1 - f) * s),
      mod = i % 6,
      r = [v, q, p, p, t, v][mod] * 255,
      g = [t, v, v, q, p, p][mod] * 255,
      b = [p, p, t, v, v, q][mod] * 255;

      return [~~r, ~~g, ~~b, "rgb("+ ~~r + "," + ~~g + "," + ~~b + ")"];
  }

  function rgbToHex(r, g, b) {
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  }

  ngColorPicker.directive('ngColorCircle', ['$timeout', function($timeout) {
    return {
      restrict  : 'E',
      replace   : true,
      scope     : '@=',
      template  : '<div>'
      +             '<canvas id="colorCircle">'
      +             '</canvas>'
      +           '</div>',
      compile: function compile(tElement, tAttrs, transclude) {
        return {
          pre: function preLink(scope, iElement, iAttrs, controller) {
            var canvas        = document.getElementById("colorCircle"),
                context       = canvas.getContext('2d'),
                width         = canvas.width = iAttrs.width ||  300,
                height        = canvas.height = iAttrs.height || 300,
                imageData     = context.createImageData(width, height),
                pixels        = imageData.data,
                oneHundred    = 100,
                two55         = 255,
                circleOffset  = 10,
                diameter      = width - circleOffset * 2,
                wheelPixel    = circleOffset * 4 * width + circleOffset * 4;

            iElement.css({
              width: width,
              height: height
            });

            scope.radius            = diameter / 2;
            scope.radiusPlusOffset  = scope.radius + circleOffset;
            scope.radiusSquared     = scope.radius * scope.radius;
            scope.currentY          = oneHundred;
            scope.currentX          = -scope.currentY;

            for (y = 0; y < height; y++) {
              for (x = 0; x < width; x++) {
                var rx  = x - scope.radius,
                    ry  = y - scope.radius,
                    d   = rx * rx + ry * ry,
                    rgb = hsvToRgb((Math.atan2(ry, rx) + Math.PI) / (Math.PI * 2),
                          Math.sqrt(d) / scope.radius, 1); 

                pixels[wheelPixel++] = rgb[0];
                pixels[wheelPixel++] = rgb[1];
                pixels[wheelPixel++] = rgb[2];
                pixels[wheelPixel++] = d > scope.radiusSquared ? 0 : two55;
              }
            }

            context.putImageData(imageData, 0, 0);
          },
          post: function postLink(scope, iElement, iAttrs, controller) { 
            $(iElement).click(function(event) {
              scope.currentX = event.pageX - this.offsetLeft - scope.radiusPlusOffset || scope.currentX;
              scope.currentY = event.pageY - this.offsetTop - scope.radiusPlusOffset || scope.currentY;

              var theta = Math.atan2(scope.currentY, scope.currentX),
                  d = scope.currentX * scope.currentX + scope.currentY * scope.currentY;

              if (d > scope.radiusSquared) {
                scope.currentX = scope.radius * Math.cos(theta);
                scope.currentY = scope.radius * Math.sin(theta);
                theta = Math.atan2(scope.currentY, scope.currentX);
                d = scope.currentX * scope.currentX + scope.currentY * scope.currentY;
              }

              var color = hsvToRgb((theta + Math.PI) / (Math.PI * 2),
                Math.sqrt(d) / scope.radius, 1);
              var hex = rgbToHex(color[0], color[1], color[2]);

              $timeout(function(){
                scope.$parent[iAttrs.modelObject][iAttrs.modelProperty] = hex;                
              });
            });
          }
        };
      }
    };
  }]);

  ngColorPicker.directive('ngColorPicker', [function() {
    return {
      restrict: 'E',
      replace: true,
      scope : '@=',
      template: '<table>'
      +           '<tr>'
      +           '<td ng-repeat="color in colorList">'
      +             '<div style="width: 8px; height: 8px; border: {{color.select}}px solid #000; padding: 5px; background-color: {{color.color}}" ng-click="selectColor(color)">'
      +             '</div>'
      +           '<td>'
      +           '</tr>'
      +         '</table>',
      compile: function compile(tElement, tAttrs, transclude) {
        return {
          post: function postLink(scope, iElement, iAttrs, controller) { 
            scope.modelObject   = iAttrs.modelObject;
            scope.modelProperty = iAttrs.modelProperty;
            scope.colorList = [];
            angular.forEach(colors, function(color) {
              scope.colorList.push({
                color : color,
                select : 1
              });
            });
          }
        };
      },
      controller: function($scope, $element, $timeout) {
        $scope.selectColor = function(color) {
          for (var i = 0; i < $scope.colorList.length; i++) {
            $scope.colorList[i].select = 1;
            if ($scope.colorList[i] === color) {
              $scope[$scope.modelObject][$scope.modelProperty] = color.color;
              $scope.colorList[i].select = 2;
            }
          }
        };
      }
    };
  }]);
})(window.angular);

6. Working Demo

That’s it. Now you can test your files or you can be lazy and fork my working version on plunker or fork it on GitHub.

AngularJS Chatroom Tutorial: No Database or Sockets Required

Comments 8 Standard

So I’ve been playing around with Angular lately and I decide to try my hand at making an Angular chat room since angular is great when it comes to keeping the page display current as the DOM changes. In order to deal with the message sending (and for the ease of getting a working version going) I decided to use PubNub which handles all the message transport quickly and easily.

1. Create A PubNub Account

In order to use this you’ll need to sign up for a PubNub account. There is a free plan and several paid for plans available for you to choose from. Once your account is created you’ll need to find your subscribe key and publish key.

2. Create the chat file

Now you’re going to create the main chat file. This will display your chat room. At the top of the file replace the subscribe_key and publish_key with the one you see in your newly created account.

index.html

<!doctype html>
<html ng-app="angular_chat">
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
    <script src="http://code.jquery.com/jquery-1.9.1.js"></script>
    <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.4.0.js"></script>
    <div pub-key="PUB_KEY_GOES_IN_HERE" sub-key="SUBSCRIBE_KEY_GOES_IN_HERE" ssl="off" origin="pubsub.pubnub.com" id="pubnub"></div>
    <script src="http://cdn.pubnub.com/pubnub-3.1.min.js"></script>
    <script src="angular-chat.js"></script>
    <link rel="stylesheet" href="style.css" rel="stylesheet">
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
  </head>
  <body>
  <div class='container-fluid' ng-controller="chatCtrl">
    <div ng-include="'chatHeader.html'"></div>
    <div ng-Show="!loggedIn" id="login">
      <h2>Login</h2>
      <label for="username">
        <b>Username:</b>
      </label>
      <label>
        <input type="text" ng-model="message.username" />
      </label>
      <span ng-click="attemptLogin()">
        <i></i> Go Chat
      </span>
    </div>
    <div ng-Show="loggedIn" id="chat">
      <table>
        <tr ng-repeat="chat in chatMessages">
            <td style="width: 30%;">
              <b>{{chat.username}}</b><br/>
              <small>
               {{chat.date}} @ {{chat.time}}
              </small>
            </td>
            <td colspan="2">{{chat.text}}</td>
        </tr>
        <tr ng-show="chatMessages.length == 0">
          <td colspan="3">No messages yet!</td>
        </tr>
      </table>
    </div>
    <form ng-Show="loggedIn" ui-keypress="{13:'postMessage()'}">
      <div id="inputMessage">
        <input type="text" placeholder="Say hello!" ng-model="message.text" />
        <span ng-click="postMessage()">
          <i></i> Post
        </span>
      </div>
    </form>
    <div ng-include="'chatFooter.html'"></div>
  </div>
  </body>
</html>

3. Create the angular file

Now we’re going to add in a file for our angular scripts. At the top of the file you’ll find some config variables which you can change as you see fit to set the default username, how many messages to show at a time, and the name of the chat channel you’ll be connecting to.

angular-chat.js

/***
 * File: angular-chat.js
 * Author: Jade Krafsig
 * Source: design1online.com, LLC
 * License: GNU Public License
 ***/

/***
 * Purpose: load bootstrap ui angular modules
 * Precondition: none
 * Postcondition: modules loaded
 ***/
angular.module('angular_chat', ['ui.bootstrap']);

/***
 * Purpose: load the existing chat logs
 * Precondition: none
 * Postcondition: chat logs have been loaded
 ***/
function chatCtrl($scope, $http) { 

  /***
   * Configurable global variables
   ***/
  $scope.chatChannel = "angular_chat";
  $scope.messageLimit = 50;
  $scope.defaultUsername = "Guest";

  /***
   * Static global variables
   ***/
  $scope.loggedIn = false;
  $scope.errorMsg;
  $scope.realtimeStatus = 0;

  /***
   * Purpose: clear the message object
   * Precondition: none
   * Postcondition: message object has been reset
   ***/
  $scope.clearMsg = function() {
    $scope.message = {
      username: $scope.defaultUsername,
      email: 'n/a',
      text: ''
    };
  }

  $scope.clearMsg();

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
  $scope.chatLogs = function() {
    PUBNUB.history( {
      channel : $scope.chatChannel,
      limit   : $scope.messageLimit
    }, function(messages) {
      // Shows All Messages
      $scope.$apply(function(){
        $scope.chatMessages = messages.reverse();          
      }); 
    });
   }

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
   $scope.attemptLogin = function() {
    $scope.errorMsg = "";

    if (!$scope.message.username) {
      $scope.errorMsg = "You must enter a username.";
      return;
    }

    if (!$scope.realtimeStatus) {
      $scope.errorMsg = "You're not connect to PubNub.";
      return;
    }

    $scope.loggedIn = true;
   }

  /***
   * Purpose: remove error message formatting when the message input changes
   * Precondition: none
   * Postcondition: error message class removed from message input
   ***/
  $scope.$watch('message.text', function(newValue, oldValue) {
    if (newValue)
      $("#inputMessage").removeClass("error");
      $scope.errorMsg = "";
  }, true);

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.postMessage = function() {

    //make sure they are logged in
    if (!$scope.loggedIn) {
      $scope.errorMsg = "You must login first.";
      return;
    }

    //make sure they enter a chat message
    if (!$scope.message.text) {
      $scope.errorMsg = "You must enter a message.";
      $("#inputMessage").addClass("error");
      return;
    }

    //set the message date
    d = new Date();
    $scope.message.date = d.getDay() + "/" + d.getMonth() + "/" + d.getFullYear();
    $scope.message.time = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();

   PUBNUB.publish({
      channel : $scope.chatChannel,
      message : $scope.message
    });

    $scope.message.text = "";
  };

  /***
   * Purpose: connect and access pubnub channel
   * Preconditions: pubnub js file init
   * Postconditions: pubnub is waiting and ready
   ***/
  PUBNUB.subscribe({
    channel    : $scope.chatChannel,
    restore    : false, 
    callback   : function(message) { 
      //update messages with the new message
      $scope.$apply(function(){
        $scope.chatMessages.unshift(message);          
      }); 
    },

    error      : function(data) {
      $scope.errorMsg = data;
    },

    disconnect : function() {   
      $scope.$apply(function(){
        $scope.realtimeStatus = 0;
      });
    },

    reconnect  : function() {   
      $scope.$apply(function(){
        $scope.realtimeStatus = 1;
      });
    },

    connect    : function() {
      $scope.$apply(function(){
        $scope.realtimeStatus = 2;
        //load the chat logs
        $scope.chatLogs();
      });
    }
  });

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.attemptLogout = function() {
    $("#inputMessage").removeClass("error");
    $scope.clearMsg();
    $scope.loggedIn = false;
  }
}

4. Create the header and footer files

These files will put a static header and footer on your chat room.

chatHeader.html

<div>
  <h1>Angular Chat</h1>
  <h5><i></i> using Bootstrap and PubNub</h5>
  <p>&nbsp;</p>
  <div ng-show="realtimeStatus == 0">
    <span><i></i> Disconnected</span>
  </div>
  <div ng-show="realtimeStatus == 1">
    <span><i></i> Connecting...</span>
  </div>
  <div ng-show="realtimeStatus == 2">
    <span><i></i> Connected</span>
  </div>
  <div ng-show="loggedIn" id="logout">
    <span ng-click="attemptLogout()"><i></i> Logout</span>
  </div>
</div>
<p>&nbsp;</p>
<div ng-show="errorMsg">
  <i></i> <b>Error:</b> {{errorMsg}}
</div>

chatFooter.html

<div>
  2013 &copy; design1online.com
</div>

5. Create the stylesheet

This will adjust the height of the chat window and the chat header status and logout buttons.

style.css

#chat {
 height: 33em; 
 overflow: auto;
 background-color: #EEE;
 border-radius: .25em;
 margin-bottom: 1em;
}

.status {
  float: left;
  margin-top: -1em;
}

#logout {
  float: right;
  margin-top: -1.2em;
}

6. Try the working version

See what the chat room looks like on my plunkr account or fork it on GitHub.

Pits Of Doom Lesson 10: Member Interaction

Comments 5 Standard

In the last lesson we added the ability to fight monsters on our map and added a graphic library to display our maps and updated the map editor so we can easily customize our maps and how they look from here on out. In this lesson we’ll discuss member interaction, how to build a chat room, and how to display other member’s characters on the map when they’re logged into the game.

Lesson Concepts

No matter what you may think there is no such thing as “real time” as a result of how the Internet works. Let’s take visiting your favorite website for example.

On your computer you open a browser, type in the domain name and hit enter or press the go button. When this happens your computer sends a packet of information out to your Internet connection (or wireless router) and then waits for a response. Your request is passed from place to place until it finds a server that’s waiting and listening for your requests. When the server gets your request it processes it and  sends back a result to your computer. So when you see the google homepage you’ve actually sent a request, the google server has heard your request, generated a response, and then your computer hears the response and displays the information from the google server on your browser screen. This is also why people who have dial up experience slow Internet.

So the question becomes, how do you hide or overcome delays that result from Internet communications? Some features of our Pits of Doom game are inherently perfect for covering up this problem. Take a chat room for instance. When you use a chat room you expect there will be a delay before you get your response. In this case the delay is to some degree expected by the user.

Now take our map. If we see another member on the same map level we don’t expect it to jump from place to place as it moves around. Instead it should seamlessly navigate around the map even if we’re not moving ourselves.

Creating The Chat Room

The concept behind a chat room is simple. Every time someone posts a message we store it to the database. In the chat screen we always select the last few messages inserted into the database. Finally we update the chat display every couple of seconds so it appears as if people are talking simultaneously.

Let’s start by making a table for the chat messages. We’ll need a way to identify which message belongs to which member, a place to store their text, and then the date/time they posted the message to make searching and sorting easier if we ever want to reference a particular day/time in the chat log.

CREATE  TABLE  chatroom (
  id INT NOT  NULL  AUTO_INCREMENT,
  mid INT NOT  NULL,
  message MEDIUMTEXT NOT  NULL,
  date DATETIME NOT  NULL,
  PRIMARY  KEY (id),
  UNIQUE (id)

There are two fields in this table we haven’t talked about before. The first one is mediumtext. This is one of three text options available in MySQL. There’s tinytext, mediumtext, text, and longtext. These store varying amounts of data respectively. For everything we’re doing with this game we’ll only need a mediumtext or a text field type as those are the most common for the amount of data we’re going to store. So the question is, why not always use the text or longtext options? MySQL has to allocate space for the information that would fit in the different sized text fields. By using the smallest one we need we’ll increase our database speeds and cut down on any potentially wasted space.

Implementing The Chat Room

This is a fairly simple process. We have to tie the message box to the chat message submit button. When someone presses the submit button and they have a message we’re going to add that message to the chat table.

How we display the chat messages is a bit more complicated. We have two options:

  1. Use PHP to automatically refresh the screen periodically.
  2. Use Ajax to re-generate the screen without refreshing the whole page (best solution).

Since this is a PHP/MySQL game we’re going to go with the first PHP 0ption so  you don’t have to know any Ajax. If you want the Ajax version you can contact me and purchase it as an addon for $25. Our script to display the most recent chat room messages looks like this:

<?php
/****************
* File: chattext.php
* Date: 6.17.2011
* Author: design1online.com, LLC
* Purpose: display chat text messages
*****************/
require_once('../oop/mysqlobj.php');
require_once('functions.php');
require_once('dbconnect.php');

$MAX_MESSAGES = 25;
$REFRESH_SECONDS = 5;

echo "
<html>
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html\" />
<link rel=\"stylesheet\" type=\"text/css\" href=\"../css/default.css\" />
<meta http-equiv=\"refresh\" content=\"$REFRESH_SECONDS\">
</head>
<body class=\"chat\">";

$loop = mysql_query("
    SELECT
        C.mid,
        M.username,
        C.message,
        C.date
    FROM
        chatroom C
    INNER JOIN members M ON M.id = C.mid
    ORDER BY
        C.id DESC
    LIMIT $MAX_MESSAGES")
    or die ('Cannot load chat room messages ' . mysql_error());

while ($row = mysql_fetch_assoc($loop))
    echo "<span class=\"datetime\">" . date('g:ia', strtotime($row['date'])) . "</span>" . htmlentities($row['username']) . ": " . htmlentities($row['message']) . "<br/>";

echo "</body>
    </html>";

Displaying Other Members

Not only do we want to talk to other members we want to see them on the map. In order to do this we’ll need to check to see if the online flag is set to true. Whenever we login using our login form we automatically set this value to true. The only problem we have now is how to determine if they’re still playing or not.

Therefore we’re going to create a new script called online.php and call it every time someone moves their character around the board. We’ll use this script to automatically logout anyone who hasn’t been active recently and restore the online status to anyone whose currently still playing. So in a nutshell online.php has to do two things:

  1. Remove the online status from any player whose last login is more than 20 minutes ago
  2. Update the member’s last login date to the current date and time any time they interact with the game
/*******
* Check to see if there is another character in this spot
*******/
function hasMember($x, $y, $z, $mapid)
{
    global $_SESSION;

    $result = $_SESSION['database']->query("SELECT M.id FROM characters C
            INNER JOIN members M ON M.id = C.memberid
            WHERE C.active=1 AND M.online=1 AND C.x='$x' AND C.y='$y' and C.z='$z'
            AND C.mapid = '$mapid' LIMIT 1");

    $row = mysql_fetch_assoc($result);

    return $row['id'];
}

Once we’ve established a way to determine which members are still online and which members are currently offline we can modify our displayMap function to show other members currently on the map if there is no monster present. In order to do this we first have to find all active characters (remember they can have more than one character eventually) then check to make sure the owners of those characters are online. Once we know they’re online we can check the character’s coordinates to figure out whether or not they are on a specific map location.

Game Files

This lesson we implemented a chat room, created a script to update whether or not a member is currently logged in and wrapped it up by displaying those member’s characters on our map.

Play The Working Version!

Sign into Pits of Doom using the login below or create an account today! To really see this lesson in action it’s best if you open two different browsers and login to each of them (using the same browser with two windows won’t work!) to see other character’s that are current logged into the game.

Account 1

Account 2

Username: test

Password: test

Username: test2

Password: testing

Download The Source Code

Use the view source utility or download a .zip archive.

Lesson Concepts

Member interaction is a huge part of an online game with any kind of social or interactive quality about it. In pits of doom we’d like to be able to trade/buy weapons and fight other members so it’s important that they be able to communicate and see other characters on the game world. In the next lesson we’ll look at updating our chat room so you can click on another member’s name to view more about their characters and send them private messages.

Go Back to Lesson 9 or Continue to Lesson 11