Older Post

We just released CommitList: Curated tutorials for devs

Ionic 3 and Firebase Cloud Firestore Chat App - Part II

In the first part of this series, we started off by setting up our Cloud Firestore project and database. We also created a new Ionic project, installed the AngularFire library and configured our ionic project to use the AngularFire library.

In this article, we will start building the chat application.

Defining the Chat Models

We start by defining two interfaces that will represent our User and Chat models. Inside the src->app folder, create a file named app.models.ts and enter the code below:

export interface User {
  email: string;
  name: string;
  time: string;
}

export interface Chat {
  message: string;
  pair: string;
  sender: string;
  time: number;
}

Creating the Chat Service

The next step is to create a service to do the following:

  • Add a new user to the users collection
  • Add a new chat object to our chats collection
  • Create a pair Id that uniquely identifies a chat message between two users
  • Hold the current chat pair Id when a user clicks to chat with another user
  • Hold the current chat partner when a user clicks to chat with another user

If some of the items above don't make sense to you yet, just follow along, they will eventually as we proceed.

Still, in the app folder, create a file named app.service.ts and enter the following code.

import { Injectable } from "@angular/core";
import {
  AngularFirestore,
  AngularFirestoreCollection
} from "angularfire2/firestore";
import { User, Chat } from "./app.models";
import { appconfig } from "./app.config";

@Injectable()
export class ChatService {
  users: AngularFirestoreCollection<User>;

  chats: AngularFirestoreCollection<Chat>;

  //The pair string for the two users currently chatting
  currentChatPairId;
  currentChatPartner;

  constructor(private db: AngularFirestore) {
    
    this.users = db.collection<User>(appconfig.users_endpoint);
    this.chats = db.collection<Chat>(appconfig.chats_endpoint);
  }

  addUser(payload) {
    return this.users.add(payload);
  } //addUser

  addChat(chat: Chat) {
    return this.chats.add(chat);
  } //addChat

  createPairId(user1, user2) {
    let pairId;
    if (user1.time < user2.time) {
      pairId = `${user1.email}|${user2.email}`;
    } else {
      pairId = `${user2.email}|${user1.email}`;
    }

    return pairId;
  } //createPairString

}

Then add this service to the providers array in app.module.ts.

Great! Now, let's go through what's going on in here.

First, we import AngularFirestore and AngularFirestoreCollection from the angularfire2/firestore module. We also import our models and the application configuration.

Next, we create our users and chats members as types of AngularFirestoreCollection in order to manipulate our collections.

We then assign them to instances of their respective collections in our constructor.

The addUser method

addUser(payload) {
    return this.users.add(payload);
} //addUser

This takes in a user object and calls the users collection's add method to add the user object and return a promise.

The addChat method

addChat(chat: Chat) {
    return this.chats.add(chat);
} //addChat

This takes in an object of the type Chat and calls the chats collection's add method to add the chat object and return a promise.

The createPairId method

createPairId(user1, user2) {
    let pairId;
    if (user1.time < user2.time) {
      pairId = `${user1.email}|${user2.email}`;
    } else {
      pairId = `${user2.email}|${user1.email}`;
    }

    return pairId;
} //createPairString

This method uses a neat trick to create a unique pair Id to identify the messages between two users by concatenating the emails of the two users and separating them by a pipe character. It uses the timestamp at which a user was registered to determine which emails comes first in the pair string.

Creating the Login Page

When our app is loaded, we will display a page for the user to log in with their email and to enter a display name.

To do this, let's swap the default home page that comes with the ionic project with our login page. Simply go to src->app->pages->home, and replace the code in home.html with the code below:

<ion-header>
  <ion-navbar>
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>IonFire Chat</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding id="signInPage">
  <h3>Sign In to IonFire Chat</h3>

  <div id="signInForm">
    <ion-list>

      <ion-item>
        <ion-label floating>Email</ion-label>
        <ion-input type="email" [(ngModel)]="loginForm.email"></ion-input>
      </ion-item>
      <ion-item>
        <ion-label floating>Display Name</ion-label>
        <ion-input type="text" [(ngModel)]="loginForm.name"></ion-input>
      </ion-item>

      <ion-item>
        <button ion-button large block (click)="loginUser()">Sign In</button>
      </ion-item>

    </ion-list>
  </div>



</ion-content>

This is a simple form with bindings to a loginForm object which we will later declare in our page component.
We also have a loginUser handler attached to the Sign In button.

The Login Page Component

Now open up home.ts and enter the code below:

import { Component, OnInit } from "@angular/core";
import {
  NavController,
  LoadingController,
  ToastController
} from "ionic-angular";
import { AngularFirestore } from "angularfire2/firestore";
import { User } from "../../app/app.models";
import { Observable } from "rxjs";
import { ChatService } from "../../app/app.service";
import { Storage } from "@ionic/storage";
import { ChatsPage } from "../chats/chats";
import { appconfig } from "../../app/app.config";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage implements OnInit {
  //email: string;
  loginForm: any = {};
  constructor(
    public navCtrl: NavController,
    private db: AngularFirestore,
    private chatservice: ChatService,
    private loadingCtrl: LoadingController,
    private toastCtrl: ToastController,
    private storage: Storage
  ) {}

  ngOnInit() {
    this.storage.get("chatuser").then(chatuser => {
      if (chatuser && chatuser.email !== "") {
        this.navCtrl.push(ChatsPage);
      }
    });
  }

  loginUser() {
    if (this.loginForm.email != "") {
      //Check if email already exists
      let myLoader = this.loadingCtrl.create({
        content: "Please wait..."
      });
      myLoader.present().then(() => {
        this.db
          .collection<User>(appconfig.users_endpoint, ref => {
            return ref.where("email", "==", this.loginForm.email);
          })
          .valueChanges()
          .subscribe(users => {

            if (users.length === 0) {
              //Register User

              //Add the timestamp
              this.loginForm.time = new Date().getTime();

              this.chatservice
                .addUser(this.loginForm)
                .then(res => {
                  //Registration successful
                  
                  this.storage.set("chatuser", this.loginForm);
                  myLoader.dismiss();

                  let toast = this.toastCtrl.create({
                    message: "Login In Successful",
                    duration: 3000,
                    position: "top"
                  });
                  toast.present();

                  this.navCtrl.push(ChatsPage);
                })
                .catch(err => {
                  console.log(err);
                  myLoader.dismiss();
                });
            } else {
              //User already exists, move to chats page
              
              this.storage.set("chatuser", users[0]);

              let toast = this.toastCtrl.create({
                message: "Login In Successful",
                duration: 3000,
                position: "top"
              });
              toast.present();
              myLoader.dismiss();

              this.navCtrl.push(ChatsPage);
            }
          });
      });
    } else {
      let toast = this.toastCtrl.create({
        message: "Enter Email to log in",
        duration: 3000,
        position: "top"
      });
      toast.present();
    }
  }
}

Cool! Now let's look into what's going on here:

The loginUser method first checks if the email field is not empty. If it isn't, it triggers the ionic loader and first checks if the user already exists in our users collection. If it is, we use Ionic Storage to store the user object.
If the user doesn't exist we simply save the user to our Cloud firestore database and upon success save it in the application using Ionic Storage.
After saving the user object on either occassion, we then navigate to the page that displays the Chat Users which is the next page we will be working on.

In the ngOnInit method, we perform a check to see if there is already a logged in user stored in our application, if so, we simply redirect the user straight to the Chat Users page.

Creating the Chat Users Page

The next page to create is the Chat users page, where a logged-in user can click to chat with another user on the list.

To create the new page, simply run the following ionic command to generate the page:

ionic generate page chats

Then, add this page to the declarations and entryComponents arrays in app.module.ts

After this command successfully runs, open the chats.html file and replace its content with the code below:

<ion-header>

  <ion-navbar>
    <ion-title>{{chatuser?.name}}</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

  <ion-list>
    <ion-list-header>
      USERS
    </ion-list-header>
    <ion-item *ngFor="let user of availableusers" (click)="goToChat(user)">
      <ion-avatar item-start>
        <img src="http://via.placeholder.com/16x16">
      </ion-avatar>
      <h2>{{user.name || "Anonymous"}}</h2>

      <p>{{user.email}}</p>
    </ion-item>

  </ion-list>
  
</ion-content>

Here, we simply loop over an array of availableusers which, from the component, is the list of available users for the logged in-user to chat with.

Open up chats.ts and replace its contents with the following code:

import { Component, OnInit } from "@angular/core";
import { IonicPage, NavController, NavParams } from "ionic-angular";
import { AngularFirestore } from "angularfire2/firestore";
import { Storage } from "@ionic/storage";
import { appconfig } from "../../app/app.config";
import { User } from "../../app/app.models";

import { ChatService } from "../../app/app.service";
import { ChatroomPage } from "../chatroom/chatroom";


@IonicPage()
@Component({
  selector: "page-chats",
  templateUrl: "chats.html"
})
export class ChatsPage implements OnInit {
  availableusers: any = [];
  chatuser;
  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    private db: AngularFirestore,
    private storage: Storage,
    private chatService: ChatService
  ) {}

  ngOnInit() {
    //Fetch other users

    this.storage.get("chatuser").then(chatuser => {
      this.chatuser = chatuser;

      this.db
        .collection<User>(appconfig.users_endpoint)
        .valueChanges()
        .subscribe(users => {
          //this.availableusers = users;
          console.log(users);
          this.availableusers = users.filter(user => {
            if (user.email != chatuser.email) {
              return user;
            }
          });
        });
    });
  }

  goToChat(chatpartner) {
    this.chatService.currentChatPairId = this.chatService.createPairId(
      this.chatuser,
      chatpartner
    );

    this.chatService.currentChatPartner = chatpartner;

    this.navCtrl.push(ChatroomPage);
  } //goToChat
}

As soon as the page loads, we call our Cloud firestore end-point to recieve our users collection. We then filter this array by removing the logged-in user and set the class member variable availableusers to the resulting filtered list.

Then, we implement a goTochat method that takes in the chat user selected as the chatpartner and calls the createPairId method on our chat service to create a pair id and assign it to the currentChatPairId of the chat service so that we can access it on the next page. After this is done, we then navigate to the chatroom page which we will be creating next.

Creating the ChatRoom Page

To begin, we start by running the command below to generate the chatroom page

ionic generate page chatroom

Once this command runs successfully, open up chatroom.html and replace its content with the following code:

<ion-header>

  <ion-navbar>
    <ion-title>{{chatpartner.name}}</ion-title>
  </ion-navbar>

</ion-header>


<ion-content #content padding id="chatPage">

  <ion-list>

    <ion-item *ngFor="let chat of chats | sort:'time'" class="chat" text-wrap [ngClass]="{'chat-partner' : isChatPartner(chat.sender)}">
      {{chat.message}}
    </ion-item>

  </ion-list>

</ion-content>

<ion-footer>
  <ion-toolbar>
    <ion-row>
      <ion-col col-10>
        <ion-input type="text" [(ngModel)]="message" placeholder="Enter Message...."></ion-input>
      </ion-col>
      <ion-col col-2>
        <button ion-button block (click)="addChat()">
          Send
        </button>
      </ion-col>
    </ion-row>


  </ion-toolbar>
</ion-footer>

On this page, we are looping over all available chat messages between two users and sorting them by the time the message was sent using the sort filter which we will create shortly.

We also use a css class to differentiate chat messages coming from the partner the logged-in user is chatting with by using the isChatPartner method to perform the check.

To send messages into the chat room, we create a form in the page footer with a send button that calls the addChat method which we will implement in our component.

Remember to add this page to the declarations and entryComponents arrays in app.module.ts.

Now, open up chatroom.scss and enter these styles to make the page look like a typical chat timeline.


#chatPage {
  display: flex;
  flex-direction: row;
  .chat {
    background-color: pink;
    border: none;
    width: 90%;
    margin-bottom: 20px;
    border-radius: 5px;
    align-self: flex-end;
  }

  .chat-partner {
    margin-left: 10%;
    background-color: skyblue;
  }
}

To complete this page, let's write our chatroom page component

import { Component, OnInit, ViewChild } from "@angular/core";
import { IonicPage, NavController, NavParams } from "ionic-angular";
import { AngularFirestore } from "angularfire2/firestore";
import { Chat } from "../../app/app.models";
import { appconfig } from "../../app/app.config";
import { ChatService } from "../../app/app.service";
import { Storage } from "@ionic/storage";

@IonicPage()
@Component({
  selector: "page-chatroom",
  templateUrl: "chatroom.html"
})
export class ChatroomPage implements OnInit {
  chats: any = [];
  chatpartner = this.chatService.currentChatPartner;
  chatuser;
  message: string;
  chatPayload: Chat;
  intervalScroll;
  @ViewChild("content") content: any;

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    private db: AngularFirestore,
    private chatService: ChatService,
    private storage: Storage
  ) {}

  //scrolls to bottom whenever the page has loaded
  ionViewDidEnter() {
    this.content.scrollToBottom(300); //300ms animation speed
  }

  ngOnInit() {

    this.storage.get("chatuser").then(chatuser => {
      this.chatuser = chatuser;
    });

    this.db
      .collection<Chat>(appconfig.chats_endpoint, res => {
        return res.where("pair", "==", this.chatService.currentChatPairId);
      })
      .valueChanges()
      .subscribe(chats => {
        
        this.chats = chats;
       
      });
  } //ngOnInit

  addChat() {
    if (this.message && this.message !== "") {
      console.log(this.message);
      this.chatPayload = {
        message: this.message,
        sender: this.chatuser.email,
        pair: this.chatService.currentChatPairId,
        time: new Date().getTime()
      };

      this.chatService
        .addChat(this.chatPayload)
        .then(() => {
          //Clear message box
          this.message = "";

          //Scroll to bottom
          this.content.scrollToBottom(300);
        })
        .catch(err => {
          console.log(err);
        });
    }
  } //addChat

  isChatPartner(senderEmail) {
    return senderEmail == this.chatpartner.email;
  } //isChatPartner
}

Ok, a lot is going on here. Let's get right into it

The first thing we do once the page loads is use the currentChatPairId assigned to our chat service on the previous page to fetch the chat messages between our logged in user and the selected user. This is done in the ngOnInit method.

Then we implement the addChat method by creating a chat object and save it to our chat collection using the addChat method of our chat service.

Once the chat is successfully added, we scroll to the bottom of the page by calling this.content.scrollToBottom(300); so that the most recent chat messages can be visible to the user.

The final method in our component class is the isChatPartner method, which simply takes in the email of the user that sent the chat message and checks if it's the current chat partner.

The sort filter

Unlike AngularJS, Angular does not come with a sort filter by default, thus, we need to implement the one we used in our chatroom page template.

Simply run this command to create a new pipe

ionic generate pipe sort

After the command successfully runs, go to src->pipes->sort and replace the code in sort.ts with the one below:

import { Pipe, PipeTransform } from '@angular/core';


@Pipe({
  name: 'sort',
})
export class SortPipe implements PipeTransform {
  
  transform(array: any[], field: string): any[] {
    array.sort((a: any, b: any) => {
      if (a[field] < b[field]) {
        return -1;
      } else if (a[field] > b[field]) {
        return 1;
      } else {
        return 0;
      }
    });
    return array;
  }
}

And there you have it. You can now test this application by opening it on two different browsers and logging in with different emails. From there, you can have a two way chat from one browser to another.

You can also compile it to an ios or android application and test.

This chat application definitely won't be winning any awards, and there are many bells and whistles to be added to make it stand out, but we have been able to implement the core feature of a chat application which is having two users chat with each other in real time.

You can find the complete code here on github.

Conclusion

Firebase and Ionic are two amazing platforms for development, and in this series, we have seen how they make it easy to spin up a working, production-ready chat application.

Happy Coding :)

We'll help you unleash.

Join the 30,000 developers who subscribe to our newsletter.

Scale your
Development team

We help you execute projects by providing trusted developers who can join your team and immediately start delivering high-quality code.

Hire Developers
code, mobile, ionic