Firebase
Build a Real-Time Slack Clone using AngularFire

Adding Messaging Functionality

PRO
Outline
Create a service for retrieving messages
angular.module('angularfireSlackApp')
  .factory('Messages', function($firebaseArray){
    var channelMessagesRef = firebase.database().ref('channelMessages');

    return {
      forChannel: function(channelId){
        return $firebaseArray(channelMessagesRef.child(channelId));
      }
    };
  });

The forChannel function on our service returns a $firebaseArray of messages when provided a channelId. Later in this tutorial, we'll create a forUsers function for retrieving direct messages.

Create a new state channels.messages with the following code:
.state('channels.messages', {
  url: '/{channelId}/messages',
  resolve: {
    messages: function($stateParams, Messages){
      return Messages.forChannel($stateParams.channelId).$loaded();
    },
    channelName: function($stateParams, channels){
      return '#'+channels.$getRecord($stateParams.channelId).name;
    }
  }
})

This state will again be a child state of channels. Our url will have a channelId parameter. We can access this parameter with $stateParams, provided by ui-router. We're resolving messages, which is using the forChannel function from our Messages service, and 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 channels since child states inherit their parent's dependencies. We'll come back and add the controller and templateUrl properties once we create our controller and template.

Create a new controller MessagesCtrl in app/channels/messages.controller.js. We'll be injecting profile, channelName, and messages
angular.module('angularfireSlackApp')
  .controller('MessagesCtrl', function(profile, channelName, messages){
    var messagesCtrl = this;
  });

Again, the profile dependency that we're injecting will actually come from the parent state channels that resolves to the current user's profile.

Set messages and channelName on messagesCtrl to the respective dependencies
messagesCtrl.messages = messages;
messagesCtrl.channelName = channelName;
Set message on messagesCtrl to an empty string.
messagesCtrl.message = '';
Create a function sendMessage to $add a message to messages.
messagesCtrl.sendMessage = function (){
  if(messagesCtrl.message.length > 0){
    messagesCtrl.messages.$add({
      uid: profile.$id,
      body: messagesCtrl.message,
      timestamp: firebase.database.ServerValue.TIMESTAMP
    }).then(function (){
      messagesCtrl.message = '';
    });
  }
};

A message object will need to contain uid, which will be how we identify who sent the message. body 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.

In app/index.html, include our messages service and controller.
<script src="channels/channels.service.js"></script>
<script src="channels/messages.service.js"></script>
<script src="channels/messages.controller.js"></script>
Create the following template in app/channels/messages.html:
<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">
      {{ 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 channelName from our controller. Then we're ng-repeating over messages and using message.uid and the helper functions from channelsCtrl to get the user's display name and Gravatar. We're also using Angular's date 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 sendMessage function from our controller.

Update the channels.messages state to use the template and controller we just created.
url: '/{channelId}/messages',
templateUrl: 'channels/messages.html',
controller: 'MessagesCtrl as messagesCtrl',
Update the channel links in the sidebar so we can navigate to channels.
<a ui-sref="channels.messages({channelId: channel.$id})" ui-sref-active="selected"># {{ channel.name }}</a>

We're specifying the parameters for the channels.messages state within the ui-sref directive. The ui-sref-active directive will add the specified class (selected in our case) to the element when a state specified in a sibling or child ui-sref directive. Now we should be able to navigate between channels and start chatting!

Update the createChannel function to send the user to the newly created channel upon creation.
channelsCtrl.createChannel = function(){
  channelsCtrl.channels.$add(channelsCtrl.newChannel).then(function(ref){
    $state.go('channels.messages', {channelId: ref.key});
  });
};

Check your work

You can view the completed & working code for this tutorial here:

 

I finished! On to the next chapter