Build

AngularJS Chat Tutorial: Customizable Friend Lists (6/6)

10 min read Michael Carroll on Jul 29, 2019

This tutorial walks through building chat with our core pub/sub technology (and other PubNub features). We recently launched ChatEngine, a new framework for rapid chat development.

Check out the PubNub Chat AngularJS tutorial. →

Welcome back to Part 6 of our PubNub series on how to build a complete chat app with PubNub’s AngularJS SDK.

In Part 5, AngularJS User Authentication with OAuth 2.0 and Access Manager, you learned how to use OAuth 2.0 to authenticate users in your app with Access Manager API to secure the communications.

In this tutorial, you will learn how to use the channel groups to create a friends list that shows the online status of your friends, but not all of the users in the chat room. Think about a huge social network app, there are billions of users online and we wouldn’t like every users to be able to know about the current online status of any random user. Each user should only be authorized to see the online status of his friends. It’s exactly what you are going to learn to implement in this tutorial.

This tutorial will walk through two topics:

  1. Displaying the friend list in the application
  2. Adding an online status to your friends in the list that gets updated.

Here’s how our chat app will look like at the end of this tutorial:

AngularJS chat app with friends list
Open the demo in the browser Demo:
The demo of the AngularJS chat app is available here
Get the AngularJS chat app demo source code on GitHub Source code:
The source code is available in the github repository

If you haven’t followed Part 5: AngularJS User Authentication with GitHub OAuth 2.0 and Access Manager tutorial, get started by cloning the chat project in the state where we stopped.

Just type this commands in the terminal:

git clone https://github.com/pubnub/angularjs-hubbub.git
cd angularjs-hubbub
git reset --hard blog5

Displaying Friends in the Application

Our own NodeJS server is exposing a friends endpoint

Fetching the friends from GitHub

The first step is to get our list of friends from the server and create an endpoint that will return the list of GitHub friends of the users as JSON.

In this application, we will be using the list of followers and followees as our friends list.

→ In server/app.js create an api/friends endpoint that will get the list of the followers and followees, merge them, and return them as JSON:

app.get('/api/friends', ensureAuthenticated, function(req, res) {
     var github_client = github.client(req.user.oauth_token);
     github_client.requestDefaults['qs'] = {
         page: 1,
         per_page: 100
     };
     var ghme = github_client.me()
     ghme.followers(function(err, followers) {
         if (!err && res.statusCode == 200) {
             ghme.following(function(err, following) {
                 if (!err && res.statusCode == 200) {
                     var comparator = function(friend1, friend2) {
                         return friend1.id == friend2.id
                     };
                     var friends = _.unionWith(followers, following, comparator);
                     res.status(200).send(friends);
                 } else {
                     res.status(500).send();
                 }
             });
         } else {
             res.status(500).send();
         }
     });
 });

The friends service

Fetching the friends from the server

→ In services/friends.service.js, create a friends service that fetches the friends from the friends endpoint and expose them through a Friends.all() method.

 

angular.module('app')
    .factory('Friends', ['$http', 'config', function FriendsFactory($http, config) {
        return {
            all: function() {
                return $http({
                    method: 'GET',
                    url: config.SERVER_URL + "api/friends"
                });
            }
        };
    }]);

The friend-list component

The friend list component

In components/friend-list/friend-list.directive.js, create a friend-list directive that exposes the friends from the friends service.

angular.module('app').directive('friendList', function() {
    return {
        restrict: "E",
        replace: true,
        templateUrl: 'components/friend-list/friend-list.html'
        controller: function($scope, Friends) {
            Friends.all().then(function(friends) {
                $scope.friends = friends.data;
            });
        }
    };
});

In components/friend-list/friend-list.html, create a template to display the friends:

<ul class="friend-list collection">
    <li class="collection-item avatar" ng-repeat="friend in friends track by friend.id">
        <user-avatar uuid="{{friend.id}}"></user-avatar>
        <div class="login">{{friend.login}}</div>
    </li>
</ul>

→ In components/friends-search-box/friends-search-box.directive.js, create a friends search box directive:

angular.module('app').directive('friendsSearchBox', function() {
  return {
    restrict: "E",
    replace: true,
    templateUrl: 'components/friends-search-box/friends-search-box.html',
    scope: false
  };
})

→ In components/friends-search-box/friends-search-box.html, create the associated template that includes an input field with ng-model=”search”.

→ Update the friends-list template in components/friend-list/friend-list.html to automatically update it according to the search box value. Update the HTML tag to add an angular filter bound to the search input.

<li class="collection-item avatar" ng-repeat="friend in friends | orderBy:'-login':true | filter:search:strict track by friend.id ">

→ In client/app/views/chat.html, separate the view in two parts: a chat-sidebar and chat-main section.

<div class="chat">
    <section class="chat-sidebar">
    </section>
    <section class="chat-main">
        <online-user-list></online-user-list>
        <message-list></message-list>
        <typing-indicator-box></typing-indicator-box>
        <message-form></message-form>
    </section>
</div>

You can use CSS Flexbox to properly display these two parts as two columns in your application.

→ Add the and components in the chat-sidebar section we previously added:

That’s it. We now have a searchable friends list displayed on the left side of our application. In the next step, we are going to animate this friends list in order to see which of our friends are online.

Update The Friends List with an Online Status

In the following part, we will be using the presence and the channel groups features to build our online friends list. We will be applying Access Manager to restrict channel groups access.

What is PubNub Channel Groups?

The Channel Groups feature groups channels into a persistent collection of channels that you can modify dynamically. It will also allow you to subscribe to all of these channels by simply subscribing to the group.

In this online friends list feature, we will be using Channel Groups. Every user will subscribe to his own dedicated presence channel to indicate if he is online. Each user will subscribe to his own friends_presence channel group that will aggregate the presence channels of the users. See the illustration below.

Explanation about presence channel groups

If you want to learn more about channel groups, you can watch the following video from the University of PubNub

Individual Presence Channel

Each user is subscribing to his own presence channel to indicate if he is online.

Subscribing to this channel is triggering a join event. Likewise unsubscribing triggers a leave event, also there is a timeout event triggered when a user is disconnected after some time. These events get broadcasted and you get notified in your AngularJS app.

For the own presence channel, we will follow this naming convention:

user-presence_[user-login]

Individual Presence Channel

→ In services/authentication.service.js, makes the user subscribe to his own user_presence channel:

 

var channels = [
    'messages',
    'user_presence’ + currentUser.get().id
]
Pubnub.subscribe({
 	          channel: channel,
 	          channel: channels,
  	          triggerEvents: true
  	    });

In order to only allow users to subscribe to their own presence channel and prevent other users from subscribing to the presence channels of other users, we need to apply Access Manager to this channel.

→ In server/app.js, when logging in while calling the /auth/github endpoint, grant access to the own presence channel:

 

pubnub.grant({
    channel: 'user_presence' + currentUser.get().id ,
    auth_key: user.oauth_token,
    read: true,
    write: true,
    ttl: 0
});

The Friends Presence Channel Group

Now that all users have their own channel where we see them online, we can create a channel group for each of them that will aggregate the presence channels of friends.

For the friends presence channel group of the user, we will follow this naming convention:

user-friends-presence_login-user

The Friends Presence Channel Group

Giving the server the manage permission

The first step is to allow the server to create channel groups. It is called the manage permission.

→ In server/app.js, grant the manage permission to the server auth_key. Use ‘:’ as the channel name in order to give it permission to create or manage any channel group.

 

pubnub.grant({
    channel_group: ':'
    auth_key: 'THE_SERVER_AUTH_KEY',
    manage: true,
    read: true,
    write: true,
    ttl: 0
});

Creating the user friends channel group

→ In server/app.js, write a function that takes two parameters : a user and an array of friends

→ Make this function create a channel group named friends_presence_[user-login]

→This channel group will include all of the channels the friends are subscribing to trigger their presence events. (join, leave, timeout). As a reminder, Each channel follow this naming convention : friends_presence_[user-id]

 

var createOwnUserFriendsPresenceChannelGroup = function(user, friends) {
    var deferred = Q.defer();
    var friends_presence_channels = _.map(friends, function(friend) {
        return "user_presence" + friend.id
    });
    var user_friends_presence_channel = friends_presence_'+user._id
    pubnub.channel_group_add_channel({
        channel_group: user_friends_presence_channel,
        channel: friends_presence_channels,
        callback: function(res) {
            deferred.resolve(res)
        },
        error: function(res) {
            deferred.reject(res)
        }
    });
    return deferred.promise;
}

Note, that in this code sample, I’m using the Q library that allows us to use the Promises.

→ Call this function when fetching friends via the /api/friends endpoint just before the request is returning the friends:

 

createOwnUserFriendsPresenceChannelGroup(req.user, friends).then(function() {
    res.status(200).send(friends);
}).catch(function() {
    res.status(500).send();
})

→ In server/app.js, when the user is logging in, grant access to his own friends_presence-[user-login] channel group.

 

pubnub.grant({
    channel_group: friends_presence_'+user._id
    auth_key: user.oauth_token,
    read: true,
    ttl: 0
});

Displaying the Online Users in Friends List

In this part, we are going to update our friends service in order to expose the online status of our friends. In order to do this, we will need to fetch our friends from the server and fetch the online friends from PubNub. We will be merging both lists to add an “online” attribute and will be updating the online statuses each time a friends is getting online or offline.

Injecting the online status in the friend list

→ Update services/friends.service.js, to fetch the friends list from the server and store it in an array:

The base code of our friends service should be similar to the following:

 

angular.module('app')
    .factory('Friends', ['$rootScope', 'Pubnub', 'currentUser', '$http', '$q', 'config',
        function FriendsService($rootScope, Pubnub, currentUser, $http, $q, config) {
            // Aliasing this by self so we can access to this trough self in the inner functions
            var self = this;
            this.friends = []
            this.channel_group = 'friends_presence_' + currentUser.get().id.toString()
            /*
             |--------------------------------------------------------------------------
             | Fetch the friends list from the server
             |--------------------------------------------------------------------------
            */
            var fetchFriends = function() {
                return $http({
                    method: 'GET',
                    url: config.SERVER_URL + "api/friends/"
                })
            }
      
            var all = function() {
  
              fetchFriends().then(function(friends) {
                self.friends = friends.data
                return self.friends
              });
            }
            /*
             |--------------------------------------------------------------------------
             | Public API
             |--------------------------------------------------------------------------
            */
            return {
                all: all
            }
        }
    ]);

Now that we have our list of users stored in the service itself, we will want to fetch from PubNub to find who is online in the friends_presence channel group.

Let’s fetch the list of users already connected when we are initiating our service. To achieve this, we will be using the Pubnub.here_now() method that returns an array of UUIDs currently online.

Calling here_now() from the Friends service

→ Add a fetchOnlineFriends method that will return a promise resolved with the online friends.

 

var fetchOnlineFriends = function() {
    var deferred = $q.defer()
    Pubnub.here_now({
        channel_group: self.channel_group,
        callback: function(m) {
            var online_users = _.map(m['channels'], function(ch) {
                return ch['uuids'][0]['uuid'];
            })
            // Remove the current user from the list of online users
            _.remove(online_users, function(user) {
                return user['uuid'] == currentUser.get().id;
            });
            deferred.resolve(online_users)
        },
        error: function(err) {
            deferred.reject(err)
        }
    });
    return deferred.promise
}

→ Write a injectOnlineStatusToFriendList function that will add an online status property to the list of friends:

Injecting the online status in the friend list

 

 /*
 |--------------------------------------------------------------------------
 | Inject the online status to the friend list
 |--------------------------------------------------------------------------
*/
 var injectOnlineStatusToFriendList = function(friends, onlineFriends) {
     // Key that is used to merge the online status
     var userIdKey = 'id';
     return friends = _.map(friends, function(friend) {
         // Add 
         friend['online'] = _.includes(onlineFriends, _.toString(friend[userIdKey]))
         return friend;
     })
 }
 /*
  |--------------------------------------------------------------------------
  | Store the friend list in our Friends Service
  |--------------------------------------------------------------------------
 */
 var storeFriendList = function(friends) {
     angular.extend(self.friends, friends);
 }

→ Now, we have the list of friends already connected. Let’s update our online friends list when a friend is connecting or leaving the chat application.

Subscribing to the presence events of the friends channel group

→ Update services/authentication.service.js so that the user is subscribing to his friends_presence channel. It notifies him when a new user joins or leave the application.

 

var channel_groups = [
    user_friends_presence_” + currentUser.get().id.toString() + '-pnpres'
]
Pubnub.subscribe({
 	          channel_group: channel_groups,
 	          triggerEvents: ['callback']
               });

→ In services/friends.service, subscribe to the presence events of the user_friends_presence channel group and update the friends list accordingly:

 

 /*
 |--------------------------------------------------------------------------
 | Subscribe to new friends presence events
 |--------------------------------------------------------------------------
*/
 var subscribeToFriendsPresenceEvents = function() {
     // We listen to Presence events :
     $rootScope.$on(Pubnub.getMessageEventNameFor(self.channel_group), function(ngEvent, presenceEvent) {
         updateOnlineFriendList(presenceEvent);
     });
 };
 /*
  |--------------------------------------------------------------------------
  | Update the list of friend according to presence events
  |--------------------------------------------------------------------------
 */
 var updateOnlineFriendList = function(event) {
     var online = (event['action'] === 'join');
     var index = _.findIndex(self.friends, {
         id: _.toNumber(event['uuid'])
     });
     // Only change the status if necessary
     if (index != -1 && online != self.friends[index]['online']) {
         self.friends[index]['online'] = online;
         $rootScope.$digest();
     }
 };

Finally update the Friends.all() function in order to call the methods we created:

  • Fetch both of the friends list from the server and the list of online friends from the presence channel group.
  • Merge both list by adding an online attribute in the friends list.
  • Subscribe to new friends presence events to update our friends list when a friend is connection or leaving.
  • Return a reference to the array containing the friends list.

The all() method should look like the following:

var all = function() {
    // We return the the array reference if already populated
    if (!(_.isEmpty(self.friends))) {
        var deferred = $q.defer()
        deferred.resolve(self.friends);
        return deferred.promise;
    }
    return $q.all([fetchFriends(), fetchOnlineFriends()]).then(function(data) {
        var friends = data[0].data;
        var onlineFriends = data[1];
        return injectOnlineStatusToFriendList(friends, onlineFriends);
    }).then(function(friends) {
        storeFriendList(friends)
        subscribeToFriendsPresenceEvents();
        return self.friends
    })
}

Now our service is storing our friends with their online status updated in real time!

Displaying the online status in the friends list component

→ In components/friend-list/friend-list.html, display the online status near the user login:

<ul class="friend-list collection">
    <li class="collection-item avatar" ng-repeat="friend in friends | orderBy:'-login':true | orderBy:'-online' | filter:search:strict track by friend.id ">
        <user-avatar uuid="{{friend.id}}"></user-avatar>
        <div class="login">{{friend.login}}</div>
        <div ng-if="friend.online == true" class="online-status">
            <i class="tiny material-icons online">lens</i> online</div>
        </div>
        <div ng-if="friend.online == false" class="online-status">
            <i class="tiny material-icons offline">lens</i> offline</div>
        </div>
    </li>
</ul>
0