Fork me on GitHub

WhatsApp Clone with Meteor and Ionic 2 CLI

Legacy Tutorial Version (Last Update: 2016-11-22)

Authentication

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

Meteor Accounts (Server Side)

In this step we will authenticate and identify users in our app.

Before we go ahead and start extending our app, we will add a few packages which will make our lives a bit less complex when it comes to authentication and users management.

First we will update our Meteor server and add few Meteor packages called accounts-base and accounts-phone which will give us the ability to verify a user using an SMS code, so run the following inside api directory:

api$ meteor add accounts-base
api$ meteor add npm-bcrypt
api$ meteor add mys:accounts-phone

For the sake of debugging we gonna write an authentication settings file (api/private/settings.json) which might make our life easier, but once your'e in production mode you shouldn't use this configuration:

5.2 Added settings file api/private/settings.json
1
2
3
4
5
6
7
8
{
  "accounts-phone": {
    "verificationWaitTime": 0,
    "verificationRetriesWaitTime": 0,
    "adminPhoneNumbers": ["+9721234567", "+97212345678", "+97212345679"],
    "phoneVerificationMasterCode": "1234"
  }
}

Now anytime we run our app we should provide it with a settings.json:

$ meteor run --settings private/settings.json

To make it simpler we can add start script to package.json:

5.3 Updated NPM script package.json
4
5
6
7
8
9
10
  "homepage": "http://ionicframework.com/",
  "private": true,
  "scripts": {
    "api": "cd api && meteor run --settings private/settings.json",
    "ionic:build": "ionic-app-scripts build",
    "ionic:serve": "ionic-app-scripts serve"
  },

NOTE: If you would like to test the verification with a real phone number, accounts-phone provides an easy access for twilio's API, for more information see accounts-phone's repo.

We will now apply the settings file we've just created so it can actually take effect:

5.4 Define SMS settings api/server/main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as moment from "moment";
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { initMethods } from "./methods";
import { Chats, Messages } from "../collections/whatsapp-collections";
 
Meteor.startup(() => {
  initMethods();
 
  if (Meteor.settings) {
    Object.assign(Accounts._options, Meteor.settings['accounts-phone']);
    SMS.twilio = Meteor.settings['twilio'];
  }
 
  if (Chats.find({}).cursor.count() === 0) {
    let chatId;
 

Meteor Accounts (Client Side)

Second, we will update the client, and add the corresponding authentication packages to it as well:

$ npm install --save accounts-base-client-side
$ npm install --save accounts-phone

Let's import these packages in the app's main component so they can be a part of our bundle:

5.5 Added accounts packages to client side package.json
21
22
23
24
25
26
27
28
    "@ionic/storage": "1.1.6",
    "@types/meteor": "^1.3.31",
    "@types/underscore": "^1.7.36",
    "accounts-base-client-side": "^0.1.1",
    "accounts-phone": "0.0.1",
    "angular2-moment": "^1.0.0",
    "babel-runtime": "^6.20.0",
    "ionic-angular": "2.0.0-rc.4",

Install the necessary typings:

$ npm install --save @types/meteor-accounts-phone

And import them:

5.6 Add meteor-accounts-phone type declarations package.json
20
21
22
23
24
25
26
    "@angular/platform-server": "2.2.1",
    "@ionic/storage": "1.1.6",
    "@types/meteor": "^1.3.31",
    "@types/meteor-accounts-phone": "0.0.5",
    "@types/underscore": "^1.7.36",
    "accounts-base-client-side": "^0.1.1",
    "accounts-phone": "0.0.1",
5.6 Add meteor-accounts-phone type declarations src/declarations.d.ts
12
13
14
15
16
17
18
  https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html
*/
/// <reference types="meteor-typings" />
/// <reference types="@types/meteor-accounts-phone" />
/// <reference types="@types/underscore" />
/// <reference path="../api/models/whatsapp-models.d.ts" />
declare module '*';

UI

For authentication we gonna create the following flow in our app:

  • login - The initial page. Ask for the user's phone number.
  • verification - Verify a user's phone number by an SMS authentication.
  • profile - Ask a user to pickup its name. Afterwards he will be promoted to the tabs page.

Before we implement these pages, we need to identify if a user is currently logged in. If so, he will be automatically promoted to the chats view, if not, he is gonna be promoted to the login view and enter a phone number.

Let's apply this feature to our app's entry script:

5.8 Wait for user if logging in src/app/main.ts
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'accounts-phone';
 
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
import { MeteorObservable } from 'meteor-rxjs';
 
Meteor.startup(() => {
  const sub = MeteorObservable.autorun().subscribe(() => {
    if (Meteor.loggingIn()) return;
 
    setTimeout(() => {
      sub.unsubscribe();
    });
 
    platformBrowserDynamic().bootstrapModule(AppModule);
  });
});

Great, now that we're set, let's start implementing the views we mentioned earlier.

Let's start by creating the LoginComponent. In this component we will request an SMS verification right after a phone number has been entered:

5.9 Create login component src/pages/auth/login.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
 
@Component({
  selector: 'login',
  templateUrl: "login.html"
})
export class LoginComponent {
  phone = '';
 
  constructor(
    public navCtrl: NavController,
    public alertCtrl: AlertController
  ) {}
 
  onInputKeypress({keyCode}: KeyboardEvent): void {
    if (keyCode == 13) {
      this.login();
    }
  }
 
  login(): void {
    const alert = this.alertCtrl.create({
      title: 'Confirm',
      message: `Would you like to proceed with the phone number ${this.phone}?`,
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: 'Yes',
          handler: () => {
            this.handleLogin(alert);
            return false;
          }
        }
      ]
    });
 
    alert.present();
  }
 
  private handleLogin(alert): void {
    Accounts.requestPhoneVerification(this.phone, (e: Error) => {
      alert.dismiss().then(() => {
        if (e) return this.handleError(e);
 
        // this.navCtrl.push(VerificationComponent, {
        //   phone: this.phone
        // });
      });
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

The onInputKeypress handler is used to detect key press events. Once we press the login button, the login method is called and shows and alert dialog to confirm the action (See reference). If an error has occurred, the handlerError method is called and shows an alert dialog with the received error. If everything went as expected the handleLogin method is called. It requests for an SMS verification using Accounts.requestPhoneVerification, and promotes us to the verification view.

Hopefully that the component's logic is clear now, let's move to the template:

5.10 Create login component view src/pages/auth/login.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<ion-header>
  <ion-navbar color="whatsapp">
    <ion-title>Login</ion-title>
 
    <ion-buttons end>
      <button ion-button class="done-button" (click)="login()">Done</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 
<ion-content padding class="login-page-content">
  <div class="instructions">
    <div>
      Please enter your phone number including its country code.
    </div>
    <br>
    <div>
      The messenger will send a one time SMS message to verify your phone number. Carrier SMS charges may apply.
    </div>
  </div>
 
  <ion-item>
    <ion-input [(ngModel)]="phone" (keypress)="onInputKeypress($event)" type="tel" placeholder="Your phone number"></ion-input>
  </ion-item>
</ion-content>

And add some style into it:

5.11 Add login component view style src/pages/auth/login.scss
1
2
3
4
5
6
7
8
9
10
11
.login-page-content {
  .instructions {
    text-align: center;
    font-size: medium;
    margin: 50px;
  }
 
  .text-input {
    text-align: center;
  }
}

As usual, newly created components should be imported in the app's module:

5.12 Add login component to NgModule src/app/app.module.ts
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
24
25
26
27
28
29
30
31
import { ChatsPage } from "../pages/chats/chats";
import { MomentModule } from "angular2-moment";
import { MessagesPage } from "../pages/messages/messages";
import { LoginComponent } from "../pages/auth/login";
 
@NgModule({
  declarations: [
    MyApp,
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    MyApp,
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})

Now let's add the ability to identify which page should be loaded - the chats page or the login page:

5.13 Add user identifiation in app's main component src/app/app.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { TabsPage } from '../pages/tabs/tabs';
import { LoginComponent } from '../pages/auth/login';
 
@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  rootPage: any;
 
  constructor(platform: Platform) {
    this.rootPage = Meteor.user() ? TabsPage : LoginComponent;
 
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.

Let's proceed and implement the verification page. We will start by creating its component, called VerificationComponent:

5.14 Add verification component src/pages/verification/verification.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { Component, OnInit, NgZone } from '@angular/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';
import { Accounts } from 'meteor/accounts-base';
 
@Component({
  selector: 'verification',
  templateUrl: 'verification.html'
})
export class VerificationComponent implements OnInit {
  code: string = '';
  phone: string;
 
  constructor(
    public navCtrl: NavController,
    public alertCtrl: AlertController,
    public zone: NgZone,
    public navParams: NavParams
  ) {}
 
  ngOnInit() {
    this.phone = this.navParams.get('phone');
  }
 
  onInputKeypress({keyCode}: KeyboardEvent): void {
    if (keyCode == 13) {
      this.verify();
    }
  }
 
  verify(): void {
    Accounts.verifyPhone(this.phone, this.code, (e: Error) => {
      this.zone.run(() => {
        if (e) return this.handleError(e);
 
        // this.navCtrl.setRoot(ProfileComponent, {}, {
        //   animate: true
        // });
      });
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

Logic is pretty much the same as in the login component. When verification succeeds we redirect the user to the ProfileComponent. Let's add the view template and the stylesheet:

5.15 Add verification component view src/pages/verification/verification.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<ion-header>
  <ion-navbar color="whatsapp">
    <ion-title>Verification</ion-title>
 
    <ion-buttons end>
      <button ion-button class="verify-button" (click)="verify()">Verify</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 
<ion-content padding class="verification-page-content">
  <div class="instructions">
    <div>
      An SMS message with the verification code has been sent to {{phone}}.
    </div>
    <br>
    <div>
      To proceed, please enter the 4-digit verification code below.
    </div>
  </div>
 
  <ion-item>
    <ion-input [(ngModel)]="code" (keypress)="onInputKeypress($event)" type="tel" placeholder="Your verification code"></ion-input>
  </ion-item>
</ion-content>
5.16 Add verification component styles src/pages/verification/verification.scss
1
2
3
4
5
6
7
8
9
10
11
.verification-page-content {
  .instructions {
    text-align: center;
    font-size: medium;
    margin: 50px;
  }
 
  .text-input {
    text-align: center;
  }
}

And add it to the NgModule:

5.17 Add VerificationComponent to NgModule src/app/app.module.ts
6
7
8
9
10
11
12
 
14
15
16
17
18
19
20
21
 
27
28
29
30
31
32
33
34
import { MomentModule } from "angular2-moment";
import { MessagesPage } from "../pages/messages/messages";
import { LoginComponent } from "../pages/auth/login";
import { VerificationComponent } from "../pages/verification/verification";
 
@NgModule({
  declarations: [
...some lines skipped...
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})

And now that we have the VerificationComponent we can use it inside the LoginComponent:

5.18 Add navigation to verification page from the login component src/pages/auth/login.ts
1
2
3
4
5
6
 
47
48
49
50
51
52
53
54
55
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
import { VerificationComponent } from "../verification/verification";
 
@Component({
  selector: 'login',
...some lines skipped...
      alert.dismiss().then(() => {
        if (e) return this.handleError(e);
 
        this.navCtrl.push(VerificationComponent, {
          phone: this.phone
        });
      });
    });
  }

Last step of our authentication pattern is to pickup a name. We will create a Profile interface so the compiler can recognize profile-data structures:

5.19 Add profile model decleration api/models/whatsapp-models.d.ts
1
2
3
4
5
6
7
8
9
declare module 'api/models/whatsapp-models' {
  interface Profile {
    name?: string;
    picture?: string;
  }
 
  interface Chat {
    _id?: string;
    title?: string;

And let's create the ProfileComponent:

5.20 Add profile component src/pages/profile/profile.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { Component, OnInit } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
import { MeteorObservable } from 'meteor-rxjs';
import { Profile } from 'api/models/whatsapp-models';
import { TabsPage } from "../tabs/tabs";
 
@Component({
  selector: 'profile',
  templateUrl: 'profile.html'
})
export class ProfileComponent implements OnInit {
  profile: Profile;
 
  constructor(
    public navCtrl: NavController,
    public alertCtrl: AlertController
  ) {}
 
  ngOnInit(): void {
    this.profile = Meteor.user().profile || {
      name: '',
      picture: '/ionicons/dist/svg/ios-contact.svg'
    };
  }
 
  done(): void {
    MeteorObservable.call('updateProfile', this.profile).subscribe({
      next: () => {
        this.navCtrl.push(TabsPage);
      },
      error: (e: Error) => {
        this.handleError(e);
      }
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

The logic is simple. We call the updateProfile method and redirect the user to the TabsPage if the action succeeded. The updateProfile method should look like so:

5.21 Added updateProfile method api/server/methods.ts
1
2
3
4
5
6
7
 
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Meteor } from 'meteor/meteor';
import { Chats, Messages } from "../collections/whatsapp-collections";
import { check, Match } from "meteor/check";
import { Profile } from "api/models/whatsapp-models";
 
const nonEmptyString = Match.Where((str) => {
  check(str, String);
...some lines skipped...
 
export function initMethods() {
  Meteor.methods({
    updateProfile(profile: Profile): void {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');
 
      check(profile, {
        name: nonEmptyString,
        picture: nonEmptyString
      });
 
      Meteor.users.update(this.userId, {
        $set: {profile}
      });
    },
    addMessage(chatId: string, content: string) {
      check(chatId, nonEmptyString);
      check(content, nonEmptyString);

If you'll take a look at the constructor's logic of the ProfileComponent we set the default profile picture to be one of ionicon's svgs. We need to make sure there is an access point available through the network to that asset. If we'd like to serve files as-is we simply gonna add them to the www dir. But first we'll need to update our .gitignore file to contain the upcoming changes:

5.22 Add ionicons to .gitignore .gitignore
26
27
28
29
30
31
32
33
plugins/
plugins/android.json
plugins/ios.json
www/*
!www/ionicons
$RECYCLE.BIN/
 
.DS_Store

And now that git can recognize our changes, let's add a symlink to ionicons in the www dir:

www$ ln -s ../node_modules/ionicons

Now we can implement the view and the stylesheet:

5.24 Add profile component view src/pages/profile/profile.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ion-header>
  <ion-navbar color="whatsapp">
    <ion-title>Profile</ion-title>
 
    <ion-buttons end>
      <button ion-button class="done-button" (click)="done()">Done</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 
<ion-content class="profile-page-content">
  <div class="profile-picture">
    <img [src]="profile.picture">
    <ion-icon name="create"></ion-icon>
  </div>
 
  <ion-item class="profile-name">
    <ion-label stacked>Name</ion-label>
    <ion-input [(ngModel)]="profile.name" placeholder="Your name"></ion-input>
  </ion-item>
</ion-content>
5.25 Add profile component view style src/pages/profile/profile.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.profile-page-content {
  .profile-picture {
    max-width: 300px;
    display: block;
    margin: auto;
 
    img {
      margin-bottom: -33px;
      width: 100%;
    }
 
    ion-icon {
      float: right;
      font-size: 30px;
      opacity: 0.5;
      border-left: black solid 1px;
      padding-left: 5px;
    }
  }
}

Import our newly created component:

5.26 Add profile component to NgModule src/app/app.module.ts
7
8
9
10
11
12
13
 
16
17
18
19
20
21
22
23
 
30
31
32
33
34
35
36
37
import { MessagesPage } from "../pages/messages/messages";
import { LoginComponent } from "../pages/auth/login";
import { VerificationComponent } from "../pages/verification/verification";
import { ProfileComponent } from "../pages/profile/profile";
 
@NgModule({
  declarations: [
...some lines skipped...
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})

And use it in the VerificationComponent:

5.27 Add profile component to verification navigation src/pages/verification/verification.ts
1
2
3
4
5
6
7
 
33
34
35
36
37
38
39
40
41
import { Component, OnInit, NgZone } from '@angular/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';
import { Accounts } from 'meteor/accounts-base';
import { ProfileComponent } from "../profile/profile";
 
@Component({
  selector: 'verification',
...some lines skipped...
      this.zone.run(() => {
        if (e) return this.handleError(e);
 
        this.navCtrl.setRoot(ProfileComponent, {}, {
          animate: true
        });
      });
    });
  }

Our authentication flow is complete! However there are some few adjustments we need to make before we proceed to the next step. For the messaging system, each message should have an owner. If a user is logged-in a message document should be inserted with an additional senderId field:

5.28 Add senderId property to addMessage method api/server/methods.ts
24
25
26
27
28
29
30
31
32
 
37
38
39
40
41
42
43
      });
    },
    addMessage(chatId: string, content: string) {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');
 
      check(chatId, nonEmptyString);
      check(content, nonEmptyString);
 
...some lines skipped...
 
      return {
        messageId: Messages.collection.insert({
          senderId: this.userId,
          chatId: chatId,
          content: content,
          createdAt: new Date()
5.29 Add it also to the model api/models/whatsapp-models.d.ts
17
18
19
20
21
22
    content?: string;
    createdAt?: Date;
    ownership?: string;
    senderId?: string;
  }
}

We can determine message ownership inside the component:

5.30 Determine message ownership based on sender id src/pages/messages/messages.ts
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 
47
48
49
50
51
52
53
54
55
56
57
58
  messages: Observable<Message[]>;
  message: string = "";
  autoScroller: MutationObserver;
  senderId: string;
 
  constructor(navParams: NavParams, element: ElementRef) {
    this.selectedChat = <Chat>navParams.get('chat');
    this.title = this.selectedChat.title;
    this.picture = this.selectedChat.picture;
    this.senderId = Meteor.userId();
 
  }
 
  private get messagesPageContent(): Element {
...some lines skipped...
  }
 
  ngOnInit() {
    this.messages = Messages.find(
      {chatId: this.selectedChat._id},
      {sort: {createdAt: 1}}
    ).map((messages: Message[]) => {
      messages.forEach((message: Message) => {
        message.ownership = this.senderId == message.senderId ? 'mine' : 'other';
      });
 
      return messages;

Now we're going to add the abilities to log-out and edit our profile as well, which are going to be presented to us using a popover. Let's show a popover any time we press on the options icon in the top right corner of the chats view.

A popover, just like a page in our app, consists of a component, view, and style:

5.31 Add chats-options component src/pages/chat-options/chat-options.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { Component } from '@angular/core';
import { NavController, ViewController, AlertController } from 'ionic-angular';
import { ProfileComponent } from '../profile/profile';
import { LoginComponent } from '../auth/login';
 
@Component({
  selector: 'chats-options',
  templateUrl: 'chat-options.html'
})
export class ChatsOptionsComponent {
  constructor(
    public navCtrl: NavController,
    public viewCtrl: ViewController,
    public alertCtrl: AlertController
  ) {}
 
  editProfile(): void {
    this.viewCtrl.dismiss().then(() => {
      this.navCtrl.push(ProfileComponent);
    });
  }
 
  logout(): void {
    const alert = this.alertCtrl.create({
      title: 'Logout',
      message: 'Are you sure you would like to proceed?',
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: 'Yes',
          handler: () => {
            this.handleLogout(alert);
            return false;
          }
        }
      ]
    });
 
    this.viewCtrl.dismiss().then(() => {
      alert.present();
    });
  }
 
  private handleLogout(alert): void {
    Meteor.logout((e: Error) => {
      alert.dismiss().then(() => {
        if (e) return this.handleError(e);
 
        this.navCtrl.setRoot(LoginComponent, {}, {
          animate: true
        });
      });
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}
5.32 Add chats-options component view src/pages/chat-options/chat-options.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<ion-content class="chats-options-page-content">
  <ion-list class="options">
    <button ion-item class="option option-profile" (click)="editProfile()">
      <ion-icon name="contact" class="option-icon"></ion-icon>
      <div class="option-name">Profile</div>
    </button>
 
    <button ion-item class="option option-about">
      <ion-icon name="information-circle" class="option-icon"></ion-icon>
      <div class="option-name">About</div>
    </button>
 
    <button ion-item class="option option-logout" (click)="logout()">
      <ion-icon name="log-out" class="option-icon"></ion-icon>
      <div class="option-name">Logout</div>
    </button>
  </ion-list>
</ion-content>
5.33 Add chats-options component view style src/pages/chat-options/chat-options.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
.chats-options-page-content {
  .options {
    margin: 0;
  }
 
  .option-name {
    float: left;
  }
 
  .option-icon {
    float: right;
  }
}
5.34 Add chats-options component to the module definition src/app/app.module.ts
8
9
10
11
12
13
14
 
18
19
20
21
22
23
24
25
 
33
34
35
36
37
38
39
40
import { LoginComponent } from "../pages/auth/login";
import { VerificationComponent } from "../pages/verification/verification";
import { ProfileComponent } from "../pages/profile/profile";
import { ChatsOptionsComponent } from "../pages/chat-options/chat-options";
 
@NgModule({
  declarations: [
...some lines skipped...
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})

Now let's use it inside the ChatsPage:

5.35 Use the popover component src/pages/chats/chats.ts
2
3
4
5
6
7
8
9
10
 
12
13
14
15
16
17
18
 
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { Observable } from "rxjs";
import { Chat } from "api/models/whatsapp-models";
import { Chats, Messages } from "api/collections/whatsapp-collections";
import { NavController, PopoverController } from "ionic-angular";
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
 
@Component({
  templateUrl: 'chats.html'
...some lines skipped...
export class ChatsPage implements OnInit {
  chats;
 
  constructor(public navCtrl: NavController, public popoverCtrl: PopoverController) {
 
  }
 
...some lines skipped...
      ).zone();
  }
 
  showOptions(): void {
    const popover = this.popoverCtrl.create(ChatsOptionsComponent, {}, {
      cssClass: 'options-popover'
    });
 
    popover.present();
  }
 
  showMessages(chat): void {
    this.navCtrl.push(MessagesPage, {chat});
  }

And let's add an event handler in the view which will show the popover:

5.36 Added event handler src/pages/chats/chats.html
7
8
9
10
11
12
13
      <button ion-button icon-only class="add-chat-button">
        <ion-icon name="person-add"></ion-icon>
      </button>
      <button ion-button icon-only class="options-button" (click)="showOptions()">
        <ion-icon name="more"></ion-icon>
      </button>
    </ion-buttons>

As for now, once you click on the options icon in the chats view, the popover should appear in the middle of the screen. To fix it, we simply gonna edit the scss file of the chats page:

5.37 Add options-popover style to chats stylesheet src/pages/chats/chats.scss
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    }
  }
}
 
.options-popover {
  $popover-width: 200px;
  $popover-margin: 5px;
 
  .popover-content {
    width: $popover-width;
    transform-origin: right top 0px !important;
    left: calc(100% - #{$popover-width} - #{$popover-margin}) !important;
    top: $popover-margin !important;
  }
}

{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/messages-page" next_ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/chats-mutations"}}}