Sunteți pe pagina 1din 49

Learn to build a Real-Time

Slack clone with AngularFire

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Stay on the bleeding edge of your favorite technologies.

Albert Pai
AngularJS Firebase

Introduction
The goal of this tutorial is to guide you through the creation of a Slack clone called fireSlack. Upon
completion, you will learn how to build a real time collaborative chat application using angularFire to
integrate Firebase with AngularJS. Your application will be able to provide the following features:
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Sign up for an account


Join/create channels to chat in
Have a user profile
Direct message other users
See who's online

Prerequisites
This course assumes knowledge of programming and at least basic knowledge of JavaScript and
AngularJS. We recommend going through A Better Way to Learn AngularJS if you're not familiar with
AngularJS. We've created a seed repo based off of the Yeoman AngularJS Generator to help you get
started faster. Before you begin, you will need to have Node.js, npm, and Git installed. We'll need
Node.js and npm in order to install Grunt and Bower for managing dependencies. Follow these
instructions for installing Node.js and npm, and these instructions for installing Git. Additionally,
you'll need to have a free Firebase account and create a Firebase for this tutorial.

Final Notes About the Tutorial


open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

You should never copy and paste code from this text unless we tell you to, as we've found that the
skills being taught will stick better if you write out all of the code yourself. If you need more
clarification on a certain part of the tutorial, we recommend that viewing the supplementary
screencast series as we go into far more detail in each of the videos. It's also significantly easier to
learn from the screencast series than the text, as you can actually see how a skilled developer
manipulates the concepts in AngularJS to build a working application.

Getting Started
Once the initial codebase is cloned locally, we'll need to run a few commands to install dependencies
and get our application up and running. Within our codebase, run the following commands:
After running

grunt serve

, open up

http://localhost:4000

and you should see a splash page for

our application, with a non-functional login and register page ready for us to build off of. In this
tutorial, our directory structure will be grouped by feature (see #1 in this list) and we will be using uirouter as our router. We'll also be using the "controller as" syntax for referencing our controllers.
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Authenticating Users using


angularFire
Creating a registration and login system for your app can be tedious, but is often one of the most
important and required features, and usually requires you to create your own backend. Thankfully,
Firebase makes this really easy for us by providing us with a hosted solution.
While you have your Firebase pulled up, keep your Firebase URL handy. It should look something like:
https://firebase-name-here.firebaseio.com/

.constant('FirebaseUrl', 'https://firebase-name-here.firebaseio.com/');

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Creating the Auth Service


angular.module('angularfireSlackApp')
.factory('Auth', function(){
});

Here we'll inject


constant,

$firebaseAuth

FirebaseUrl

constructor and our

, which is a service that AngularFire provides us with, along with our

. Then we'll be able to create a reference to Firebase using the

FirebaseUrl

, which we'll be passing to the

angularFire API docs for a list of available methods


the

$firebaseAuth

$firebaseAuth

$firebaseAuth

Firebase

service. See the

provides. Our factory will return

service associated with our Firebase.

The resulting factory should look like this:

angular.module('angularfireSlackApp')
.factory('Auth', function($firebaseAuth, FirebaseUrl){
var ref = new Firebase(FirebaseUrl);

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

var auth = $firebaseAuth(ref);


return auth;
});

Now that we have an

Auth

service ready for our application to use, let's create a controller to use

with our login and registration forms.

Creating the Auth Controller


angular.module('angularfireSlackApp')
.controller('AuthCtrl', function(Auth, $state){
var authCtrl = this;
});

The

$state

use the

go()

service is provided by
function on

open in browser PRO version

$state

ui-router

for us to control the state of our application. We can

to redirect our application to a specific state. We also create a

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

reference to the

this

keyword within our controller because we're using the

controller as

syntax. For more information about this syntax, see this lesson.

angular.module('angularfireSlackApp')
.controller('AuthCtrl', function(Auth, $state){
var authCtrl = this;
authCtrl.user = {
email: '',
password: ''
};
});

This user object will be used with the

ng-model

directive in our form. Next, we'll need two functions

on our controller, one for registering users and one for logging in users.
with two functions:

$authWithPassword

for logging in users and

$firebaseAuth

$createUser

provides us

for registering users.

Both of these functions take a user object like the one we initialized on our controller, and return a
promise. If you're not familiar with how promises work, read this to learn more about promises.

authCtrl.login = function (){

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Auth.$authWithPassword(authCtrl.user).then(function (auth){
$state.go('home');
}, function (error){
authCtrl.error = error;
});
};

When authentication is successful, we want to send the user to the home state. When it fails, we want
to set the error on our controller so we can display the error message to our user.

authCtrl.register = function (){


Auth.$createUser(authCtrl.user).then(function (user){
authCtrl.login();
}, function (error){
authCtrl.error = error;
});
};

Our

register

controller if

function works very similarly to our

$createUser

open in browser PRO version

fails, however, when

Are you a developer? Try out the HTML to PDF API

login

function. We want to set

$createUser

error

on the

succeeds, it doesn't automatically log in


pdfcrowd.com

the user that was just created so we'll need to call the

login

function we just created to log the user

in. Now that we have our authentication service and controller created, let's update our templates
and put them to use.

<script src="app.js"></script>
<script src="auth/auth.controller.js"></script>
<script src="auth/auth.service.js"></script>

.state('login', {
url: '/login',
controller: 'AuthCtrl as authCtrl',
templateUrl: 'auth/login.html'
})
.state('register', {
url: '/register',
controller: 'AuthCtrl as authCtrl',
templateUrl: 'auth/register.html'
})

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

The resulting form should look like this:

<form ng-submit="authCtrl.register()">
<div class="input-group">
<input type="email" class="form-control" placeholder="Email" ng-model="authCtrl.user.email">
</div>
<div class="input-group">
<input type="password" class="form-control" placeholder="Password" ng-model="authCtrl.user.password">
</div>
<input type="submit" class="btn btn-default" value="Register">
</form>

<div ng-show="authCtrl.error">
<span>{{ authCtrl.error.message }}</span>
</div>

This div will remain hidden until our authentication controller reaches an error, in which case the error
message it will get displayed to our user. Next, let's update our login template in a similar fashion.
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

<form ng-submit="authCtrl.login()">
<div class="input-group">
<input type="email" class="form-control" placeholder="Email" ng-model="authCtrl.user.email">
</div>
<div class="input-group">
<input type="password" class="form-control" placeholder="Password" ng-model="authCtrl.user.password">
</div>
<input type="submit" class="btn btn-default" value="Log In">
</form>

<div ng-show="authCtrl.error">
<span>{{ authCtrl.error.message }}</span>
</div>

Now we should have a working register and login system, but we have no way of telling if the user is
logged in or not. The login and registration pages are still accessible if we are authenticated. We can
resolve this by using the

resolve

property on our states.

resolve

allows us to create dependencies

that can be injected into controllers or child states. These dependencies can depend on services in our
app that return promises, and the promises will get resolved before our controller gets instantiated.
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Read the ui-router Github Wiki if you're not familiar with how

resolve

works with

ui-router

resolve: {
requireNoAuth: function($state, Auth){
return Auth.$requireAuth().then(function(auth){
$state.go('home');
}, function(error){
return;
});
}
}

The

$firebaseAuth

service provides us with a

promise will get resolved with an

auth

$requireAuth

object if the user is logged in. The Firebase Documentation

provides a table of what information is available to us within


the promise gets rejected. In our
them back to the

home

function which returns a promise. This

requireNoAuth

auth

. If the user is not authenticated,

dependency, if the User is logged in we want to send

state, otherwise, we need to catch the error that gets thrown and handle it

gracefully by returning nothing, allowing the promise to be resolved instead of rejected. Now, we
should no longer be able to access the login or register states if we're authenticated.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Storing User Profiles in Firebase


Now that we're able to authenticate users, let's create the ability for users to have custom display
names to use in our app (rather than showing the user's email or uid)

angular.module('angularfireSlackApp')
.factory('Users', function($firebaseArray, $firebaseObject, FirebaseUrl){
var Users = {};
return Users;
});

The purpose of this factory is to provide us with the ability to get either a specific user's data, or to get
a list of all of our users. Note that while Firebase provides us with a means of authentication, all of the
authentication data is separate from our Firebase data and can't be queried. It is up to us to store any
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

custom user data within Firebase manually.

angular.module('angularfireSlackApp')
.factory('Users', function($firebaseArray, $firebaseObject, FirebaseUrl){
var usersRef = new Firebase(FirebaseUrl+'users');
var Users = {};
return Users;
});

Data in Firebase is stored in a tree structure and child nodes can be referenced by adding a path to
our

FirebaseUrl

, so

https://firebase-name-here.firebase.io.com/users

refers to the

users

node.

angular.module('angularfireSlackApp')
.factory('Users', function($firebaseArray, $firebaseObject, FirebaseUrl){
var usersRef = new Firebase(FirebaseUrl+'users');
var users = $firebaseArray(usersRef);
var Users = {};
return Users;

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

});

It's also good to know that while

$firebaseArray

an array in javascript, however, methods like


and not on the Firebase. Instead,

will return pseudo array, meaning it will act a lot like

splice()

$firebaseArray

push()

pop()

will only affect data locally

provides methods named

$add

and

$remove

to

provide similar functionality while keeping your data in sync. Read the $firebaseArray
Documentation For a complete understanding of how

$firebaseArray

should be used.

var Users = {
getProfile: function(uid){
return $firebaseObject(usersRef.child(uid));
},
getDisplayName: function(uid){
return users.$getRecord(uid).displayName;
},
all: users
};

getProfile(uid)
open in browser PRO version

allows us to get a

$firebaseObject

Are you a developer? Try out the HTML to PDF API

of a specific user's profile, while

all

returns a
pdfcrowd.com

$firebaseArray

of all the users.

when given a

displayName

uid

getDisplayName(uid)

is a helper function that returns a user's

. We will be keying our data by the

uid

that comes back from our

Firebase auth data, so data in our Firebase will look similar to:

{
"users": {
"simplelogin:1":{
"displayName": "Blake Jackson"
}
}
}

Now that our

Users

service is created, let's create a controller for updating a user's profile. First we'll

need to create a new state in


user's

auth

app/app.js

to

resolve

a couple dependencies. We want to have the

data and their profile available to us before our controller is instantiated.

.state('profile', {
url: '/profile',
resolve: {
auth: function($state, Users, Auth){

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

return Auth.$requireAuth().catch(function(){
$state.go('home');
});
},
profile: function(Users, Auth){
return Auth.$requireAuth().then(function(auth){
return Users.getProfile(auth.uid).$loaded();
});
}
}
})

We left the

controller

and

templateUrl

because we haven't created them yet. The


dependency we created for
redirected to the

home

login

and

properties out of the state configuration temporarily


auth

dependency is similar to the

register

, except it does the inverse, where the user is

state if they're not authenticated. The

.catch

handling promises if we don't want to provide a success handler. The


ensures authentication, but resolves to the user's profile using the
in our

Users

service.

$firebaseArray

$loaded

requireNoAuth

is a function provided by both

function is a shorthand for


profile

dependency also

getProfile

$firebaseObject

function we created
and

that returns a promise that gets resolved when the data from Firebase is available

locally.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

angular.module('angularfireSlackApp')
.controller('ProfileCtrl', function($state, md5, auth, profile){
var profileCtrl = this;
});

We'll be using Gravatar to get profile picture functionality in our application. Gravatar is a service that
provides us with a user's profile picture when given an email, however the email needs to be md5
hashed. Luckily, there are many modules available that can do this for us, and angular-md5 was
already included in our seed codebase.

profileCtrl.profile = profile;

profileCtrl.updateProfile = function(){
profileCtrl.profile.emailHash = md5.createHash(auth.password.email);
profileCtrl.profile.$save();
};

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Here we're getting the current user's email from the


hashing it and setting to
creating next using

emailHash

ng-model

on

profile

auth

data that was resolved from our router,

displayName

will be set from the template we'll be

getGravatar: function(uid){
return '//www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash;
},

<script src="auth/auth.service.js"></script>
<script src="users/users.service.js"></script>
<script src="users/profile.controller.js"></script>

url: '/profile',
controller: 'ProfileCtrl as profileCtrl',
templateUrl: 'users/profile.html',

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

<div class="page-wrapper">
<div class="page-header">
<h1>Edit Profile</h1>
</div>
<form ng-submit="profileCtrl.updateProfile()">
<p ng-hide="profileCtrl.profile.displayName">
You'll need a display name before you can start chatting.
</p>
<div class="input-group">

<input required type="text" class="form-control" placeholder="Display Name" ng-model="profileCtrl.profile.disp


</div>
<input type="submit" class="btn btn-default" value="Set Display Name">
</form>
</div>

We should now be able to navigate to

http://localhost:4000/#/profile

, specify a display name for

our user, submit the form and it should persist when we refresh the page.
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Creating the Channels Sidebar


Now that our Users can authenticate and have profiles, we can finally start to create the functionality
for Channels. We're going to start by creating the left sidebar for listing channels and display the
current user's profile.

angular.module('angularfireSlackApp')
.factory('Channels', function($firebaseArray, FirebaseUrl){
var ref = new Firebase(FirebaseUrl+'channels');
var channels = $firebaseArray(ref);
return channels;
});

.state('channels', {
open in browser PRO version Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

url: '/channels',
resolve: {
channels: function (Channels){
return Channels.$loaded();
},
profile: function ($state, Auth, Users){
return Auth.$requireAuth().then(function(auth){
return Users.getProfile(auth.uid).$loaded().then(function (profile){
if(profile.displayName){
return profile;
} else {
$state.go('profile');
}
});
}, function(error){
$state.go('home');
});
}
}
})

templateUrl

and

controller

are temporarily omitted since we haven't created them yet. We're

resolving two dependencies here:


and

profile

, which is a lot like the

channels
profile

, which is promising our


dependency in the

$firebaseArray

profile

that the user already a displayName set, otherwise they're taken to the
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

of channels,

state, but we're ensuring

profile

state, and if they're


pdfcrowd.com

not authenticated, they get sent to the

home

state.

angular.module('angularfireSlackApp')
.controller('ChannelsCtrl', function($state, Auth, Users, profile, channels){
var channelsCtrl = this;
});

channelsCtrl.profile = profile;
channelsCtrl.channels = channels;

channelsCtrl.getDisplayName = Users.getDisplayName;
channelsCtrl.getGravatar = Users.getGravatar;

channelsCtrl.logout = function(){

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Auth.$unauth();
$state.go('home');
};

<script src="users/profile.controller.js"></script>
<script src="channels/channels.controller.js"></script>
<script src="channels/channels.service.js"></script>

<div class="main">
<div class="sidebar">
<div class="slack-name">
<h2>FireSlack</h2>
</div>
<div class="channel-list">
<div class="list-head">Channels</div>
</div>
<div class="my-info">
<img class="user-pic" ng-src="{{ channelsCtrl.getGravatar(channelsCtrl.profile.$id) }}" />

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

<div class="user-info">
<div class="user-name">
{{ channelsCtrl.profile.displayName }}
</div>
<div class="options">
<a ui-sref="profile">edit profile</a>
/
<a href="#" ng-click="channelsCtrl.logout()">logout</a>
</div>
</div>
</div>
</div>
</div>

url: '/channels',
controller: 'ChannelsCtrl as channelsCtrl',
templateUrl: 'channels/index.html',

resolve: {

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

requireNoAuth: function($state, Auth){


return Auth.$requireAuth().then(function(auth){
$state.go('channels');
}, function(error){
return;
});
}
}

profileCtrl.updateProfile = function(){
profileCtrl.profile.emailHash = md5.createHash(auth.password.email);
profileCtrl.profile.$save().then(function(){
$state.go('channels');
});
};

This
the

requireNoAuth
channels

register

dependency is a lot like the one on

state. If you want, you can change the

to also send the user to the

http://localhost:4000
open in browser PRO version

channels

we should be sent to the

Are you a developer? Try out the HTML to PDF API

login

and

requireNoAuth

register

but it sends the user to

dependency on

login

and

state as well. Now when we're logged in and visit


channels

state. In that state, we can see the


pdfcrowd.com

sidebar we just created for our application. There should be the logged in user's name and Gravatar
at the bottom of the sidebar, and an edit profile and logout link next to it. We're using the
directive that comes with

ui-router

ui-sref

to specify what state we should navigate to on click. If we click

on edit profile, we can update the user's

displayName

and it should send us back to the

channels

state when we submit the form. The logout link should log us out and send us back to the

home

state.

Now let's create the view to create channels.

.state('channels.create', {
url: '/create',
templateUrl: 'channels/create.html',
controller: 'ChannelsCtrl as channelsCtrl'
})

This state is a child state of the


parentState.childState

channels

controller (the dot notation in the state name specifies

). This state will also use

ChannelsCtrl

states to the right of the sidebar. We need to declare another


states to appear. You can read more about

open in browser PRO version

ui-router

Are you a developer? Try out the HTML to PDF API

. We'll want to render our child

ui-view

tag in order for our child

's nested states at this Github Wiki.

pdfcrowd.com

<div class="message-pane">
<ui-view></ui-view>
</div>

channelsCtrl.newChannel = {
name: ''
};

channelsCtrl.createChannel = function(){
channelsCtrl.channels.$add(channelsCtrl.newChannel).then(function(){
channelsCtrl.newChannel = {
name: ''
};
});
};

The

$add

function on the channels

open in browser PRO version

$firebaseArray

Are you a developer? Try out the HTML to PDF API

provides similar functionality to the

.push()
pdfcrowd.com

function on a Javascript

Array

, but keeps our data in sync with Firebase, while returning a promise.

Once the new channel is created we'll need to clear out the

newChannel

object.

<div class="header">
<h1>Create a channel</h1>
</div>
<form ng-submit="channelsCtrl.createChannel()">
<div class="input-group">
<input type="text" class="form-control" placeholder="Channel Name" ng-model="channelsCtrl.newChannel.name">
</div>
<input type="submit" class="btn btn-default" value="Create Channel">
</form>

<div class="channel-list">
<div class="list-head">Channels</div>
<div class="channel" ng-repeat="channel in channelsCtrl.channels">
<a># {{ channel.name }}</a>
</div>
</div>

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

We're using the

ng-repeat

directive to iterate over our array of

channels

<div class="channel-list">
<div class="list-head">Channels</div>
<div class="channel" ng-repeat="channel in channelsCtrl.channels">
<a># {{ channel.name }}</a>
</div>
<div class="channel create">
<a ui-sref="channels.create">+ create channel</a>
</div>
</div>

We're now able to click on the create channel link and start creating channels!

Adding Messaging Functionality


open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

angular.module('angularfireSlackApp')
.factory('Messages', function($firebaseArray, FirebaseUrl){
var channelMessagesRef = new Firebase(FirebaseUrl+'channelMessages');
return {
forChannel: function(channelId){
return $firebaseArray(channelMessagesRef.child(channelId));
}
};
});

The

forChannel

channelId

function on our service returns a

. Later in this tutorial, we'll create a

$firebaseArray

forUsers

of messages when provided a

function for retrieving direct messages.

.state('channels.messages', {
url: '/{channelId}/messages',
resolve: {
messages: function($stateParams, Messages){
return Messages.forChannel($stateParams.channelId).$loaded();
},
channelName: function($stateParams, channels){

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

return '#'+channels.$getRecord($stateParams.channelId).name;
}
}
})

This state will again be a child state of


access this parameter with
which is using the

channels

$stateParams

forChannel

. Our url will have a

, provided by

function from our

ui-router

Messages

channelId

parameter. We can

. We're resolving

service, and

messages

channelName

which we'll be

using to display the channel's name in our messages pane. Channel names will be prefixed with a
The

channels

dependency we're injecting is coming from the parent state

states inherit their parent's dependencies. We'll come back and add the
templateUrl

channels

controller

since child
and

properties once we create our controller and template.

angular.module('angularfireSlackApp')
.controller('MessagesCtrl', function(profile, channelName, messages){
var messagesCtrl = this;
});

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Again, the
channels

profile

dependency that we're injecting will actually come from the parent state

that resolves to the current user's profile.

messagesCtrl.messages = messages;
messagesCtrl.channelName = channelName;

messagesCtrl.message = '';

messagesCtrl.sendMessage = function (){


if(messagesCtrl.message.length > 0){
messagesCtrl.messages.$add({
uid: profile.$id,
body: messagesCtrl.message,
timestamp: Firebase.ServerValue.TIMESTAMP
}).then(function (){
messagesCtrl.message = '';
});
}

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

};

A message object will need to contain


body

uid

, which will be how we identify who sent the message.

contains the message our user input, and

timestamp

is a constant from

Firebase

that tells

the Firebase servers to use the their clock for the timestamp. When a message sends successfully,
we'll want to clear out

messagesCtrl.message

so the user can type a new message.

<script src="channels/channels.service.js"></script>
<script src="channels/messages.service.js"></script>
<script src="channels/messages.controller.js"></script>

<div class="header">
<h1>{{ messagesCtrl.channelName }}</h1>
</div>
<div class="message-wrap" ng-repeat="message in messagesCtrl.messages">
<img class="user-pic" ng-src="{{ channelsCtrl.getGravatar(message.uid) }}" />
<div class="message-info">
<div class="user-name">

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

{{ channelsCtrl.getDisplayName(message.uid) }}
<span class="timestamp">{{ message.timestamp | date:'short' }}</span>
</div>
<div class="message">
{{ message.body }}
</div>
</div>
</div>
<form class="message-form" ng-submit="messagesCtrl.sendMessage()">
<div class="input-group">
<input type="text" class="form-control" ng-model="messagesCtrl.message" placeholder="Type a message...">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Send</button>
</span>
</div>
</form>

Here we're creating a header to display the


ng-repeat

ing over

messages

and using

channelName

message.uid

from our controller. Then we're

and the helper functions from

to get the user's display name and Gravatar. We're also using Angular's

date

channelsCtrl

filter on the timestamp

to display a short timestamp. Finally, at the bottom of our view we have the form for sending
messages which submits to the
open in browser PRO version

sendMessage

Are you a developer? Try out the HTML to PDF API

function from our controller.


pdfcrowd.com

url: '/{channelId}/messages',
templateUrl: 'channels/messages.html',
controller: 'MessagesCtrl as messagesCtrl',

<a ui-sref="channels.messages({channelId: channel.$id})" ui-sref-active="selected"># {{ channel.name }}</a>

We're specifying the parameters for the


ui-sref-active

channels.messages

directive will add the specified class (

state specified in a sibling or child

ui-sref

state within the

selected

ui-sref

directive. The

in our case) to the element when a

directive. Now we should be able to navigate between

channels and start chatting!

channelsCtrl.createChannel = function(){
channelsCtrl.channels.$add(channelsCtrl.newChannel).then(function(ref){
$state.go('channels.messages', {channelId: ref.key()});
});
};

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Creating Direct Messages


Now that we have working channels with messaging, adding direct messages will be easier since we
can reuse a lot of the existing functionality we have.

var userMessagesRef = new Firebase(FirebaseUrl+'userMessages')

return {
forChannel: function(channelId){
return $firebaseArray(channelMessagesRef.child(channelId));
},
forUsers: function(uid1, uid2){
open in browser PRO version Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

var path = uid1 < uid2 ? uid1+'/'+uid2 : uid2+'/'+uid1;


return $firebaseArray(userMessagesRef.child(path));
}
};

We'll be storing our direct messages in Firebase like so:

{
"userMessages": {
"simplelogin:1": {
"simplelogin:2": {
"messageId1": {
"uid": "simplelogin:1",
"body": "Hello!",
"timestamp": Firebase.ServerValue.TIMESTAMP
},
"messageId2": {
"uid": "simplelogin:2",
"body": "Hey!",
"timestamp": Firebase.ServerValue.TIMESTAMP
}
}
}

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

}
}

Since we always want to reference the same path in our Firebase regardless of which id was passed
first, we'll need to sort our ids before referencing the direct messages.

.state('channels.direct', {
url: '/{uid}/messages/direct',
templateUrl: 'channels/messages.html',
controller: 'MessagesCtrl as messagesCtrl',
resolve: {
messages: function($stateParams, Messages, profile){
return Messages.forUsers($stateParams.uid, profile.$id).$loaded();
},
channelName: function($stateParams, Users){
return Users.all.$loaded().then(function(){
return '@'+Users.getDisplayName($stateParams.uid);
});
}
}
});

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

This state is almost identical to


We're using a different

url

channels.messages

, and the

function that we just created. The


name, and prefixes it with

messages

channelName

, using the same

templateUrl

dependency is using the

and

controller

Messages.forUsers

dependency also looks up the other user's display

channelsCtrl.users = Users.all;

<div class="channel create">


<a ui-sref="channels.create">+ create channel</a>
</div>
<div class="list-head">Direct Messages</div>
<div class="channel" ng-repeat="user in channelsCtrl.users">

<a ng-if="user.$id !== channelsCtrl.profile.$id" ui-sref="channels.private({uid: user.$id})" ui-sref-active="selec


{{ user.displayName }}
</a>
</div>

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

We're now able to chat directly to other users in our application!

Adding Presence to Users


Having direct messaging is an important feature to any chat application, but it's also very useful to
know what users are online. Firebase makes this very easy for us. Read the Firebase Documentation
to see some example code using presence. While this code is written using the core Firebase library,
we're going to replicate the same functionality using AngularFire.

var usersRef = new Firebase(FirebaseUrl+'users');


var connectedRef = new Firebase(FirebaseUrl+'.info/connected');

setOnline: function(uid){
var connected = $firebaseObject(connectedRef);
open in browser PRO version Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

var online = $firebaseArray(usersRef.child(uid+'/online'));


connected.$watch(function (){
if(connected.$value === true){
online.$add(true).then(function(connectedRef){
connectedRef.onDisconnect().remove();
});
}
});
}

This function watches for changes at the


connections to a

$firebaseArray

.info/connected

keyed under

online

node and will

$add

any open

within the user's profile. This allows us to

track multiple connections (in case the user has multiple browser windows open), which will get
removed when the client disconnects.

Users.setOnline(profile.$id);

channelsCtrl.logout = function(){
open in browser PRO version Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

channelsCtrl.profile.online = null;
channelsCtrl.profile.$save().then(function(){
Auth.$unauth();
$state.go('home');
});
};

<div class="user-name">
<span class="presence" ng-class="{online: channelsCtrl.profile.online}"></span>
{{ channelsCtrl.profile.displayName }}
</div>

We're also dynamically adding the


$firebaseArray

online

class to the span tag using

ng-class

, based on if the

containing connections in the profile is present.

<a ng-if="user.$id !== channelsCtrl.profile.$id" ui-sref="channels.direct({uid: user.$id})" ui-sref-active="selected


<span class="presence" ng-class="{online: user.online}"></span>
{{ user.displayName }}
</a>

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

We're now able to see when our users are online! Our application is almost ready for production. In
the next sections we will go over securing our data and deploying our application live.

Securing Your Data with Security


Rules
When you first create a Firebase, the default security rules allow full read and write access. While this
makes it a lot easier to get started developing, it's always strongly recommended that you create
security rules to make sure that your data stays consistent and secured. There are three kinds of
rules,

.read

.write

, and

.validate

for controlling access and validating your data.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

"rules":{
".read": true,
"users":{
"$uid":{
".write": "auth !== null && $uid === auth.uid",
"displayName":{
".validate": "newData.exists() && newData.val().length > 0"
},
"online":{
"$connectionId":{
".validate": "newData.isBoolean()"
}
}
}
},
"channels":{
"$channelId":{
".write": "auth !== null",
"name":{
".validate": "newData.exists() && newData.isString() && newData.val().length > 0"
}
}
},
"channelMessages":{
"$channelId":{
"$messageId":{
".write": "auth !== null && newData.child('uid').val() === auth.uid",
".validate": "newData.child('timestamp').exists()",
"body":{
".validate": "newData.exists() && newData.val().length > 0"

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

}
}
}
},
"userMessages":{
"$uid1":{
"$uid2":{
"$messageId":{
".read": "auth !== null && ($uid1 === auth.uid || $uid2 === auth.uid)",
".write": "auth !== null && newData.child('uid').val() === auth.uid",
".validate": "$uid1 < $uid2 && newData.child('timestamp').exists()",
"body":{
".validate": "newData.exists() && newData.val().length > 0"
}
}
}
}
}
}
}

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Deploying to Firebase
{
"firebase": "firebase-name-here",
"public": "dist"
}

firebase deploy

may prompt you to log in, but afterwards it should push your application to

Firebase's hosting service. Now if you visit https://firebase-name-here.firebaseapp.com/ you should


see our completed app, ready for the world to use!

Stay on the bleeding edge of


your favorite technologies.
Enter your email to receive courses on AngularJS,
Node, Android, Swift and more. 1-2 emails per
open in browser PRO version

Are you a developer? Try out the HTML to PDF API

Did you like this course? Share


it!

pdfcrowd.com

Node, Android, Swift and more. 1-2 emails per


week, no spam.

There's more to learn! Check out our latest Firebase courses

Help us educate millions of people around the


world for free

Home
About

Topics

Pro

Contact Us
2015 Thinkster

Privacy Policy | Terms of Use

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

S-ar putea să vă placă și