Gaming

Creating Private Chat Requests with Popup Alerts

0 MIN READ • Aug 27, 2014

Waving Hand

Good News! We've launched an all new Chat Resource Center.

We recommend checking out our new Chat Resource Center, which includes overviews, tutorials, and design patterns for building and deploying mobile and web chat.

Take me to the Chat Resource Center →

Building on our previous spawning private chat channels on demand, we'll now build the ability to private chat another user and send a chat request. When two users want to chat, one user can request the chat through a push notification (popup chat request alert), and the other user can accept or decline.


We've now covered both building a multiplayer game lobby with a chatroom and the different ways we can use matchmaking to connect two different users. Here's what we've covered so far:

This blog post is Part Nine of PubNub Developer Evangelist Ian Jennings‘s series on creating a multiplayer game with JavaScript.


Spawning Chat Request Popups

Now that we've refactored our code, let's build the ability to private chat another user and send a chat request.

chat request

Previously we used target to direct messages toward a specific user. In this case, we're going to create entirely separate channels to communicate on. It's the whole reason we refactored the code in the first place!

The first thing we need to do is let one user know the other wants to private chat with a chat request.

We'll add a new method to the Client() that tells the target user that they should get ready for a private chat! We'll also create anew PrivateChat() object for ourselves, but more on that later.

1.js
self.private_chat = function(target) {

  // open a modal for ourselves
  new PrivateChat(target);

  pubnub.publish({
    channel: channel,
    message: {
      type: 'new-private-chat',
      payload: {
        uuid: self.uuid,
        target: target.uuid
      }
    }
  });

};

I Initiate Chat

When a User object is clicked on, we're going to fire the function above rather than challenging the user. The private chat modal will have a button that adds the ability to challenge a user back.

2.js
self.init = function() {  

  $tpl.click(function() {
    me.private_chat(self);
    return false;
  });

  $('#online-users').append($tpl);

};

Someone Else Initiates Chat

We also need to listen for type: 'new-private-chat' in our subscribe call. When we receive that message, we'll also create anew PrivateChat().

3.js
message: function(data) {

  if(data.type == 'chat') {    
    Users.get(data.payload.uuid).chat(data.payload.text, $output);
  }

  if(data.payload.target == me.uuid) {    

    if(data.type == 'new-private-chat') {
      new PrivateChat(Users.get(data.payload.uuid));
    } 

    if(data.type == 'challenge') {

      var challenger = Users.get(data.payload.uuid);

      if(data.payload.action == 'request') {
        me.on_request(challenger);
      }

      if(data.payload.action == 'response') {
        me.on_response(challenger, data.payload.accepted);
      }

    }

  }

}

When we get a new-private-chat message where we are the target, we emit an alert.

Spawn the Chatroom

Remember our Spawn Chatrooms on Demand tutorial? We're about to do a very similar thing here.

Now when we receive the new-private-chat message, we'll create anew Private Chat modal.

4.js
new PrivateChat(Users.get(data.payload.uuid));

PrivateChat() is going to be in a Bootstrap modal. So we'll create a$tpl just like earlier, except this time we'll add a few extra methods likeshow() and hide().

chat request

5.js
var PrivateChat = function(user) {

  var self = this;

  var $tpl = $('\
    <div class="modal fade"> \
      <div class="modal-dialog"> \
        <div class="modal-content"> \
          <div class="modal-header"> \
            <button type="button" class="close" data-dismiss="modal">&times;</button> \
            <h4 class="modal-title">' + user.uuid + ' <span class="badge">' + user.state.skill + '</span></h4> \
          </div> \
          <div class="modal-body"> \
            <div class="panel panel-default"> \
              <div class="panel-heading">Private Chat</div> \
              <ul class="list-group private-chat-output"></ul> \
              <div class="panel-body"> \
                <form class="private-chat"> \
                  <div class="input-group"> \
                    <input type="text" class="form-control private-chat-input" /> \
                    <span class="input-group-btn"> \
                      <button type="submit" class="btn btn-default">Send Message</button> \
                    </span> \
                  </div> \
                </form> \
              </div> \
            </div> \
          </div> \
          <div class="modal-footer"> \
            <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> \
            <button type="button" class="challenge-user btn btn-primary">Challenge</button> \
          </div> \
        </div> \
      </div> \
    </div>');

  var $chat_form = $tpl.find('.private-chat');
  var $output = $tpl.find('.private-chat-output');
  var $input = $tpl.find('.private-chat-input');
  var $challenge = $tpl.find('.challenge-user');

  private_channel = channel + ':' + [me.uuid, user.uuid].sort().join(':');

  self.hide = function(callback) {
    $tpl.on('shown.bs.modal', callback);
    $tpl.modal('hide');
  };

  self.show = function(callback) {
    $tpl.on('hidden.bs.modal', callback);
    $tpl.modal('show');
  }

  var init = function(callback) {

    $('body').append($tpl);

    pubnub.subscribe({
      channel: private_channel,
      message: function(data) {   
        Users.get(data.payload.uuid).chat(data.payload.text, $output);

      }
    });

    $chat_form.submit(function() {

      pubnub.publish({
        channel: private_channel,
        message: {
          type: 'private-chat',
          payload: {
            text: $input.val(),
            uuid: me.uuid
          }
        }
      });

      $input.val('');

      return false;

    });

    $challenge.click(function(){

      pubnub.publish({
        channel: channel,
        message: {
          type: 'challenge',
          payload: {
            action: 'request',
            uuid: me.uuid,
            target: user.uuid
          }
        }
      });

      alert('Challenging ' + user.uuid + '...');

    });

    self.show();

  };

  init();

  return self;

};

Creating a Unique but Predictable Channel Name

Notice how we subscribe to a completely new channel.

6.js
private_channel = channel + ':' + [me.uuid, user.uuid].sort().join(':');

We take our original global channel, and append our two usernames in alphabetical order. This creates a new channel that is unique to these two users.

For example, a channel may be:

7.js
memewarz-lobby-demo-21:green_pigeon:purple_buffalo

We must sort the usernames by alphabetical order. The reason is because the channel needs to remain the same when me.uuid and user.uuid are swapped. Remember, these variables are different depending on who initiates the chatroom first.

More Publish and Subscribe

Then, we go about business as usual. We use pubnub.subscribe() andpubnub.publish() with the private_channel variable.

8.js
pubnub.subscribe({
  channel: private_channel,
  message: function(data) {   
    Users.get(data.payload.uuid).chat(data.payload.text, $output);
  }
});

$chat_form.submit(function() {

  pubnub.publish({
    channel: private_channel,
    message: {
      type: 'private-chat',
      payload: {
        text: $input.val(),
        uuid: me.uuid
      }
    }
  });

  $input.val('');

  return false;

});

Adding Challenge back

We also rebind the new “Challenge” button to the old function.

9.js
$challenge.click(function(){

  pubnub.publish({
    channel: channel,
    message: {
      type: 'challenge',
      payload: {
        action: 'request',
        uuid: me.uuid,
        target: user.uuid
      }
    }
  });

  alert('Challenging ' + user.uuid + '...');

});

Reuse the User.chat() function for Private Chat

Notice we have a new field in the User.chat function.

10.js
Users.get(data.payload.uuid).chat(data.payload.text, $output);

Because the chat output looks exactly the same for lobby chat and private chat, we simply add a $target to the function so we can choose what element to output the chat line to.

11.js
self.chat = function(text, $target) {

  var $line = $('<li class="list-group-item"><strong>' + self.uuid + ':</strong> </span>');
  var $message = $('<span class="text" />').text(text).html();

  $line.append($message);
  $target.append($line);

  $target.scrollTop($target[0].scrollHeight);

};

Full Private Chat Request Example

Check out the full example code below:

12.js
var pubnub = null;
var me = null;
var Users = null;

var channel = 'memewarz-lobby-demo-21';

var $online_users = $('#online-users');
var $input = $('#chat-input');
var $output = $('#chat-output');

var randomName = function() {

  var animals = ['pigeon', 'seagull', 'bat', 'owl', 'sparrows', 'robin', 'bluebird', 'cardinal', 'hawk', 'fish', 'shrimp', 'frog', 'whale', 'shark', 'eel', 'seal', 'lobster', 'octopus', 'mole', 'shrew', 'rabbit', 'chipmunk', 'armadillo', 'dog', 'cat', 'lynx', 'mouse', 'lion', 'moose', 'horse', 'deer', 'raccoon', 'zebra', 'goat', 'cow', 'pig', 'tiger', 'wolf', 'pony', 'antelope', 'buffalo', 'camel', 'donkey', 'elk', 'fox', 'monkey', 'gazelle', 'impala', 'jaguar', 'leopard', 'lemur', 'yak', 'elephant', 'giraffe', 'hippopotamus', 'rhinoceros', 'grizzlybear'];

  var colors = ['silver', 'gray', 'black', 'red', 'maroon', 'olive', 'lime', 'green', 'teal', 'blue', 'navy', 'fuchsia', 'purple'];

  return colors[Math.floor(Math.random() * colors.length)] + '_' + animals[Math.floor(Math.random() * animals.length)];

};

var randomSkill = function() {
  return Math.floor(Math.random() * 3) + 1;
};

var User_factory = function() {

  var user_list = {};
  var self = this;

  self.remove = function(uuid) {
    delete user_list[uuid];
  };

  self.get = function(uuid) {
    if(user_list.hasOwnProperty(uuid)) {
      return user_list[uuid];
    } else {
      console.error('Trying to retrieve user that is not present.');
    }
  };

  self.set = function(uuid, data) {
    if(!user_list.hasOwnProperty(uuid)) {
      user_list[uuid] = new User(uuid, data);
    }
    return user_list[uuid];
  };

  self.all = function() {
    return user_list;
  }

};

var User = function(uuid, state) {

  var self = this;

  self.uuid = uuid || randomName();
  self.state = state || {skill: randomSkill()};

  var $tpl = $('\
    <li id="' + self.uuid + '" class="list-group-item"> \
    <span class="badge">' + self.state.skill + '</span> \
    ' + self.uuid + ' \
    </li>');

  self.chat = function(text, $target) {

    var $line = $('<li class="list-group-item"><strong>' + self.uuid + ':</strong> </span>');
    var $message = $('<span class="text" />').text(text).html();

    $line.append($message);
    $target.append($line);

    $target.scrollTop($target[0].scrollHeight);

  };

  self.leave = function() {
    $tpl.remove();
  };

  self.init = function() {  

    $tpl.click(function() {
      me.private_chat(self);
      return false;
    });

    $('#online-users').append($tpl);

  };

  return self;

};

var Client = function() {

  var self = new User(randomName());

  self.on_request = function(caller) {

    var response = confirm(caller.uuid + ' is challenging you to a match! Press OK to accept or Cancel to deny.');

    pubnub.publish({
      channel: channel,
      message: {
        type: 'challenge',
        payload: {
          action: 'response',
          accepted: response,
          uuid: self.uuid,
          target: caller.uuid
        }
      },
      callback: function(){
        alert('Your response has been sent.');
      }
    });
  };

  self.on_response = function(caller, accepted) {
    if(accepted) {
      alert(caller.uuid + ' has accepted your challenge!');
    } else {
      alert(caller.uuid + ' has rejected your challenge!');
    }
  };

  self.private_chat = function(target) {

    // open a modal for ourselves
    new PrivateChat(target);

    // tell the other user to open the modal too
    pubnub.publish({
      channel: channel,
      message: {
        type: 'new-private-chat',
        payload: {
          uuid: self.uuid,
          target: target.uuid
        }
      }
    });

  };

  Users.set(self.uuid, self.state);

  return self;

};

var PrivateChat = function(user) {

  var self = this;

  var $tpl = $('\
    <div class="modal fade"> \
      <div class="modal-dialog"> \
        <div class="modal-content"> \
          <div class="modal-header"> \
            <button type="button" class="close" data-dismiss="modal">&times;</button> \
            <h4 class="modal-title">' + user.uuid + ' <span class="badge">' + user.state.skill + '</span></h4> \
          </div> \
          <div class="modal-body"> \
            <div class="panel panel-default"> \
              <div class="panel-heading">Private Chat</div> \
              <ul class="list-group private-chat-output"></ul> \
              <div class="panel-body"> \
                <form class="private-chat"> \
                  <div class="input-group"> \
                    <input type="text" class="form-control private-chat-input" /> \
                    <span class="input-group-btn"> \
                      <button type="submit" class="btn btn-default">Send Message</button> \
                    </span> \
                  </div> \
                </form> \
              </div> \
            </div> \
          </div> \
          <div class="modal-footer"> \
            <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> \
            <button type="button" class="challenge-user btn btn-primary">Challenge</button> \
          </div> \
        </div> \
      </div> \
    </div>');

  var $chat_form = $tpl.find('.private-chat');
  var $output = $tpl.find('.private-chat-output');
  var $input = $tpl.find('.private-chat-input');
  var $challenge = $tpl.find('.challenge-user');

  private_channel = channel + ':' + [me.uuid, user.uuid].sort().join(':');

  self.hide = function(callback) {
    $tpl.on('shown.bs.modal', callback);
    $tpl.modal('hide');
  };

  self.show = function(callback) {
    $tpl.on('hidden.bs.modal', callback);
    $tpl.modal('show');
  }

  var init = function(callback) {

    $('body').append($tpl);

    pubnub.subscribe({
      channel: private_channel,
      message: function(data) {   
        Users.get(data.payload.uuid).chat(data.payload.text, $output);

      }
    });

    $chat_form.submit(function() {

      pubnub.publish({
        channel: private_channel,
        message: {
          type: 'private-chat',
          payload: {
            text: $input.val(),
            uuid: me.uuid
          }
        }
      });

      $input.val('');

      return false;

    });

    $challenge.click(function(){

      pubnub.publish({
        channel: channel,
        message: {
          type: 'challenge',
          payload: {
            action: 'request',
            uuid: me.uuid,
            target: user.uuid
          }
        }
      });

      alert('Challenging ' + user.uuid + '...');

    });

    self.show();

  };

  init();

  return self;

};

var App = function() {

  Users = new User_factory();
  me = new Client();

  pubnub = PUBNUB.init({
    publish_key: 'demo',
    subscribe_key: 'demo',
    uuid: me.uuid
  });

  pubnub.subscribe({
    channel: channel,
    state: me.state,
    message: function(data) {

      if(data.type == 'chat') {    
        Users.get(data.payload.uuid).chat(data.payload.text, $output);
      }

      if(data.payload.target == me.uuid) {    

        if(data.type == 'new-private-chat') {

          new PrivateChat(Users.get(data.payload.uuid));

        } 

        if(data.type == 'challenge') {

          var challenger = Users.get(data.payload.uuid);

          if(data.payload.action == 'request') {
            me.on_request(challenger);
          }

          if(data.payload.action == 'response') {
            me.on_response(challenger, data.payload.accepted);
          }

        }

      }

    }, 
    presence: function(data) {

      if(data.action == "join") {
        Users.set(data.uuid, data.state).init();
      }

      if(data.action == "leave" || data.action == "timeout") {
        Users.remove(data.uuid);
      }

    }
  });

  $('#find-match').click(function(){

    var matches = [];
    var users = Users.all();

    for(var uuid in users) {
      if(uuid !== me.uuid && users[uuid].state.skill == me.state.skill) {
        matches.push(users[uuid]);
      }
    }

    if(!matches.length) {
      alert('Nobody is online with the same skill level.');
    } else {
      var opponent = matches[Math.floor(Math.random() * matches.length)];
      alert('Opponent Found: ' + opponent.uuid + ' with skill level ' + opponent.state.skill);
    }

  });

  $('#chat').submit(function() {

    pubnub.publish({
      channel: channel,
      message: {
        type: 'chat',
        payload: {
          text: $input.val(),
          uuid: me.uuid
        }
      }
    });

    $input.val('');

    return false;

  });

  $('#whoami').text(me.uuid);
  $('#my_skill').text(me.state.skill);

};

App();
PubNub Portal
Sep 9, 2014

Granting and Revoking Permissions - Private Chat API

With our private chat API, in this tutorial, we'll show you how to grant and revoke permissions and access to chat users for secure private chat.
Michael Carroll
Michael Carroll
Gaming
Aug 20, 2014

Enabling Users to Create Chat via Private Channels

In this blog post, we'll show you how to enable users to create private chat rooms with other users using private channels.
Michael Carroll
Michael Carroll
Gaming
Jul 10, 2014

Adding Users and Usernames to a Chatroom | PubNub

How to automatically add unique users and usernames to a multiplayer game lobby chatroom
Michael Carroll
Michael Carroll