The PubNub AngularJS SDK v 3.2.1 come out of the box with a $pubnubChannel object that abstracts all the steps needed to communicate with PubNub directely.
It allows to interact with PubNub Data Stream Network in a seemless way:
.controller('ScoresCtrl', function($scope, $pubnubChannel) {
$scope.scores = $pubnubChannel('game-scores-channel',{ autoload: 20 })
$scope.score.$publish({player: 'John', result: 32}) // Publish a message in the game-scores-channel channel.
});
Instantiating the $pubnubChannel is the only step needed to have a scope variable that reflects the realtine data from a channel. It subscribes to the channel for you, load initial data if needed and receive new realtime data automatically.
However this abstraction layer hide the direct interaction with the PubNub API and you may want to understand how it works under the hood.
This tutorial will teach you how to create a Message Factory from scratch which will be responsible for storing the messages and communicating directly with PubNub to retrieve them in real-time.
The Message factory will store the messages in an array and expose 5 functions to the outside:
Method | Role |
---|---|
getMessages() | Getting the messages |
sendMessage(string: messageContent) | Sending a message |
subscribeNewMessage(function: callback) | Being notified of new messages |
fetchPreviousMessages() | Fetching the previous messages |
messagesAllFetched() | Indicates if all the messages have been fetched or not |
Furthermore, this factory will emit the event factory:message:populated to notify that the factory has been populated with the previous messages.
This message factory will be standalone. This means that you can inject it anywhere in your app, in your controllers, and directives in order to interact with it.
The code of this service is available in this gist
The source code is well commented enough so you can understand easily how it is working under the hood.
Let’s create the base of our message factory which we will be implementing step-by-step.
→ Create a base of our factory that will be storing an array of messages, keep track of the PubNub channel we synchronise the data with, save the timestamp of the first message stored and expose useful functions to interact with it from the outside.
The code should looks something like this:
angular.module('app')
.factory('MessageService', ['$rootScope', '$q', 'Pubnub', 'currentUser',
function MessageServiceFactory($rootScope, $q, Pubnub, currentUser) {
// Aliasing this by self so we can access to this in the inner functions
var self = this;
this.messages = []
this.channel = 'messages-channel';
// We keep track of the timetoken of the first message of the array
// so it will be easier to fetch the previous messages later
this.firstMessageTimeToken = null;
this.messagesAllFetched = false;
// The service will return useful functions to interact with.
return {};
}
]);
→ Add an init function that will run when the factory is instantiated. It should subscribe to the messages channel in order to receive, send, fetch previous messages and every operation you can do through the PubNub network.
The codes should looks like this :
var init = function() {
Pubnub.subscribe({
channel: self.channel,
triggerEvents: ['callback']
});
}
init();
→ Let’s implement our first method that we are going to be able to call providing a callback function. This callback function will be performed each time a new message is received from the PubNub network:
MessageService.subscribeNewMessage(function(m){
console.log(‘There is a new message and I am aware of that !“)
)})
The PubNub AngularJS SDK is already emitting events on the rootScope, when it receive a new message, and you can simply use it to forward the event to our subscribeNewMessage function.
Here is how our subcribeNewMessage function looks like:
var subcribeNewMessage = function(callback) {
$rootScope.$on(Pubnub.getMessageEventNameFor(self.channel), callback);
};
→ Take the opportunity to use the previous method in the init function to automatically store the new messages received in the messages array.
var init = function() {
//...
subcribeNewMessage(function(ngEvent, m) {
self.messages.push(m)
$rootScope.$digest()
}
});
Don’t forget to call $rootScope.$digest() so that the entire app can be aware that the Message factory has changed.
Now that the new messages received are automatically stored, it will be great to populate the factory with the previous messages. With PubNub, it’s quite easy to achieve this using the history endpoint. Here is an example to fetch 20 messages.
Pubnub.history({
channel: ‘A-CHANNEL-NAME’,
callback: function(payload){ console.log(‘I’m called when the history is fetched’)},
count: 20, // number of messages to retrieve, 100 is the default
reverse: false, // order to retrieve the messages, false is the default
// You can define a timeframe for fetching the messages with START and END
start: 13827485876355504 , // [OPTIONAL] starting timestamp to start retrieving the messages from
end: 13827475876355504, // [OPTIONAL] ending timestamp to finish retrieving the messages from
})};
→ Create a populate function that calls the history method.
→ Ensure to store the messages retrieved, update the variables timeTokenFirstMessage and messagesAllFetched and emit a factory:message:populated event.
var populate = function() {
var defaultMessagesNumber = 20;
Pubnub.history({
channel: self.channel,
callback: function(m) {
// Update the timetoken of the first message
angular.extend(self.messages, m[0]);
if (m[0].length < defaultMessagesNumber) {
self.messagesAllFetched = true;
}
self.timeTokenFirstMessage = m[1]
$rootScope.$digest()
$rootScope.$emit('factory:message:populated')
},
count: 20
});
};
You have probably noticed I used the angular.extend function to update the array with the new messages function. It’s really important to do so and not overriding or replacing the message array with a new one in order to keep the same array reference
Why it matter ? When you display the content of an array through an ng-repeat, AngularJS automatically set up a collection watcher on this array reference. If you change the reference on the fly, the watcher won’t be able to keep track of the changes and the view won’t be updated.
This method is easy and short to implement but we needed to implement the populate() method to populate the method if the messages collection is empty
Here is the code:
var getMessages = function() {
if (_.isEmpty(self.messages))
populate();
return self.messages;
};
→ Create a function that will send a message to the PubNub network. → Append a uuid for each message sent so we can keep track of each message sent.
Here is the code of the sendMessage function:
var sendMessage = function(messageContent) {
// Don't send an empty message
if (_.isEmpty(messageContent))
return;
Pubnub.publish({
channel: self.channel,
message: {
uuid: (Date.now() + currentUser),
content: messageContent,
sender_uuid: currentUser,
date: Date.now()
}
});
};
In the last step for our chat app, we want to implement an infinite scroll feature that will load the previous messages. When scrolling and reaching the top of the screen, we need to trigger a method in order to fetch the previous messages and add them to the top of the collection.
→ Let’s implement a fetchPreviousMessages method and make the method return a promise so we can be notified when the former messages have been loaded.
→ Once again, we will use the History API but we will add the start flag to set the starting timestamp to fetch the previous messages from. Use the self.timeTokenFirstMessage variable of the factory it’s designed for.
Here is the code :
var fetchPreviousMessages = function() {
var defaultMessagesNumber = 10;
var deferred = $q.defer()
Pubnub.history({
channel: self.channel,
callback: function(m) {
// Update the timetoken of the first message
self.timeTokenFirstMessage = m[1]
Array.prototype.unshift.apply(self.messages, m[0])
if (m[0].length < defaultMessagesNumber) {
self.messagesAllFetched = true;
}
$rootScope.$digest()
deferred.resolve(m)
},
error: function(m) {
deferred.reject(m)
},
count: 10,
start: self.timeTokenFirstMessage,
reverse: false
});
return deferred.promise
};