Fork me on GitHub

WhatsApp Clone with Ionic 2 and Meteor CLI

Legacy Tutorial Version (Last Update: 22.11.2016)

Authentication

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

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.

Firt 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:

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

We will also need to install their decleration files so Typescript know how to handle them:

$ typings install dt~meteor-accounts-phone --save --global

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

5.3 Add authentication settings 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.4 Run the App with settings package.json
2
3
4
5
6
7
8
  "name": "angular2-meteor-base",
  "private": true,
  "scripts": {
    "start": "meteor run --settings private/settings.json",
    "test": "meteor test --driver-package practicalmeteor:mocha",
    "test:ci": "meteor test --once --driver-package dispatch:mocha-phantomjs",
    "postinstall": "typings i"

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.5 Define SMS settings server/imports/api/sms.ts
1
2
3
4
5
6
7
8
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
 
 
if (Meteor.settings) {
  Object.assign(Accounts._options, Meteor.settings['accounts-phone']);
  SMS.twilio = Meteor.settings['twilio'];
}

We created server/imports/api/sms.ts file, now we have to import it inside main.ts:

5.6 Import those settings server/main.ts
1
2
3
4
5
6
import { Main } from './imports/server-main/main';
import './imports/methods/methods';
import './imports/api/sms';
 
const mainInstance = new Main();
mainInstance.start();

For authentication we're going to 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 page, 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 going to be promoted to the login view and enter a phone number.

Let's apply this feature to our app's main component:

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

We don't have yet a proper component with auth logic but let's add it anyway as LoginComponent:

5.8 Use LoginComponent if user is not logged in client/imports/app/app.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component } from '@angular/core';
import { Platform } from "ionic-angular";
import { StatusBar } from "ionic-native";
import { Meteor } from 'meteor/meteor';
import template from './app.component.html';
import {TabsContainerComponent} from "../pages/tabs-container/tabs-container.component";
import {LoginComponent} from '../pages/auth/login.component';
 
@Component({
  selector: 'app',
  template
})
export class AppComponent {
  rootPage: any;
 
  constructor(platform: Platform) {
    this.rootPage = Meteor.user() ? TabsContainerComponent : 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.

Great, now that we're set, let's start implementing the views we mentioned earlier. We will start with the login component.

In this component we will request an SMS verification right after a phone number has been entered:

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
71
72
73
74
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
import { Accounts } from 'meteor/accounts-base';
import { VerificationComponent } from './verification.component';
import template from './login.component.html';
import style from "./login.component.scss";
 
@Component({
  selector: 'login',
  template,
  styles: [
    style
  ]
})
export class LoginComponent {
  phone = '';
 
  constructor(
    private navCtrl: NavController,
    private 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();
  }
}

Few things to be explained:

  • onInputKeypress is to catch Enter key press
  • login method creates an alert (see documentation) to confirm the action
  • handleError creates an alert with an error message
  • handleLogin calls Accounts.requestPhoneVerification request an SMS verification and moves to verification view.

Okay, the logic is clear. Let's move to the template:

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 styles:

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;
  }
}

Our new component is not available yet:

5.12 Register LoginComponent in the AppModule client/imports/app/app.module.ts
5
6
7
8
9
10
11
 
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {ChatsComponent} from "../pages/chats/chats.component";
import {MomentModule} from "angular2-moment";
import {MessagesPage} from "../pages/chat/messages-page.component";
import {LoginComponent} from '../pages/auth/login.component';
 
@NgModule({
  // Components, Pipes, Directive
...some lines skipped...
    AppComponent,
    TabsContainerComponent,
    ChatsComponent,
    MessagesPage,
    LoginComponent
  ],
  // Entry Components
  entryComponents: [
    AppComponent,
    TabsContainerComponent,
    ChatsComponent,
    MessagesPage,
    LoginComponent
  ],
  // Providers
  providers: [

That's great, everything is set up. We can now move to verification page.

Let's create a component called VerificationComponent:

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
import {Component, OnInit, NgZone} from '@angular/core';
import {NavController, NavParams, AlertController} from 'ionic-angular';
import {Accounts} from 'meteor/accounts-base';
import {ProfileComponent} from './profile.component';
import template from './verification.component.html';
import style from './verification.component.scss';
 
 
@Component({
  selector: 'verification',
  template,
  styles: [
    style
  ]
})
export class VerificationComponent implements OnInit {
  code: string = '';
  phone: string;
 
  constructor(
    private navCtrl: NavController,
    private alertCtrl: AlertController, 
    private zone: NgZone, 
    private 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 LoginComponent. When verification succeed we redirect user to the ProfileComponent.

So let's add the view and the styles:

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>
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;
  }
}

Make it available to AppModule:

5.16 Register VerificationComponent client/imports/app/app.module.ts
6
7
8
9
10
11
12
 
15
16
17
18
19
20
21
22
 
24
25
26
27
28
29
30
31
import {MomentModule} from "angular2-moment";
import {MessagesPage} from "../pages/chat/messages-page.component";
import {LoginComponent} from '../pages/auth/login.component';
import {VerificationComponent} from '../pages/auth/verification.component';
 
@NgModule({
  // Components, Pipes, Directive
...some lines skipped...
    TabsContainerComponent,
    ChatsComponent,
    MessagesPage,
    LoginComponent,
    VerificationComponent
  ],
  // Entry Components
  entryComponents: [
...some lines skipped...
    TabsContainerComponent,
    ChatsComponent,
    MessagesPage,
    LoginComponent,
    VerificationComponent
  ],
  // Providers
  providers: [

Last step of our authentication pattern is to pickup a name.

Let's add a Method that updates user's profile:

5.17 Add 'updateProfile' method server/imports/methods/methods.ts
2
3
4
5
6
7
8
 
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {Chats} from "../../../both/collections/chats.collection";
import {Messages} from "../../../both/collections/messages.collection";
import {check, Match} from 'meteor/check';
import {Profile} from '../../../both/models/profile.model';
 
const nonEmptyString = Match.Where((str) => {
  check(str, String);
...some lines skipped...
});
 
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): void {
    check(chatId, nonEmptyString);
    check(content, nonEmptyString);

It would be nice to define a separate model for a profile:

5.18 Define Profile model both/models/profile.model.ts
1
2
3
4
export interface Profile {
  name?: string;
  picture?: string;
}

Now we can create the ProfileComponent:

5.19 Create ProfileComponent client/imports/pages/auth/profile.component.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
import {Component, OnInit} from '@angular/core';
import {NavController, AlertController} from 'ionic-angular';
import {Meteor} from 'meteor/meteor';
import {MeteorObservable} from 'meteor-rxjs';
import {Profile} from '../../../../both/models/profile.model';
import {TabsContainerComponent} from '../tabs-container/tabs-container.component';
 
import template from './profile.component.html';
import style from './profile.component.scss';
 
@Component({
  selector: 'profile',
  template,
  styles: [
    style
  ]
})
export class ProfileComponent implements OnInit {
  profile: Profile;
 
  constructor(
    private navCtrl: NavController,
    private alertCtrl: AlertController
  ) {}
 
  ngOnInit(): void {
    this.profile = Meteor.user().profile || {
      name: '',
      picture: '/assets/ionicons/dist/svg/ios-contact.svg'
    };
  }
 
  done(): void {
    MeteorObservable.call('updateProfile', this.profile).subscribe({
      next: () => {
        this.navCtrl.push(TabsContainerComponent);
      },
      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, call updateProfile and redirect to TabsContainerComponent which is our main view if the action succeed.

If you'll take a look at the constructor's logic 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 assets dir; So let's add a symlink to ionicons in that dir:

public/assets$ ln -s ../../node_modules/ionicons

There's no component without a view:

5.21 Create a template for ProfileComponent client/imports/pages/auth/profile.component.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>

There's no good looking view without a stylesheet:

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;
    }
  }
}

There's no access to the component without a declaration inside AppModule:

5.23 Register ProfileComponent client/imports/app/app.module.ts
7
8
9
10
11
12
13
 
17
18
19
20
21
22
23
24
 
27
28
29
30
31
32
33
34
import {MessagesPage} from "../pages/chat/messages-page.component";
import {LoginComponent} from '../pages/auth/login.component';
import {VerificationComponent} from '../pages/auth/verification.component';
import {ProfileComponent} from '../pages/auth/profile.component';
 
@NgModule({
  // Components, Pipes, Directive
...some lines skipped...
    ChatsComponent,
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent
  ],
  // Entry Components
  entryComponents: [
...some lines skipped...
    ChatsComponent,
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent
  ],
  // Providers
  providers: [

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.24 Add senderId property to addMessage method server/imports/methods/methods.ts
24
25
26
27
28
29
30
31
32
 
37
38
39
40
41
42
43
    });
  },
  addMessage(chatId: string, content: string): void {
    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...
 
    Messages.collection.insert({
      chatId: chatId,
      senderId: this.userId,
      content: content,
      createdAt: new Date()
    });
5.25 Add it also to the model both/models/message.model.ts
1
2
3
4
5
6
7
export interface Message {
  _id?: string;
  chatId?: string;
  senderId?: string;
  content?: string;
  ownership?: string;
  createdAt?: Date;

We can determine message ownership inside the component:

5.26 Determine message ownership based on sender id client/imports/pages/chat/messages-page.component.ts
1
2
3
4
5
6
 
21
22
23
24
25
26
27
 
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import {Component, OnInit, OnDestroy} from "@angular/core";
import {NavParams} from "ionic-angular";
import {Meteor} from 'meteor/meteor';
import {Chat} from "../../../../both/models/chat.model";
import {Messages} from "../../../../both/collections/messages.collection";
import {Observable} from "rxjs";
...some lines skipped...
  private title: string;
  private picture: string;
  private messages: Observable<Message[]>;
  private senderId: string;
  private message = "";
  private autoScroller: MutationObserver;
 
...some lines skipped...
    this.selectedChat = <Chat>navParams.get('chat');
    this.title = this.selectedChat.title;
    this.picture = this.selectedChat.picture;
    this.senderId = Meteor.userId();
  }
 
  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:

5.27 Add options-popover to ChatsComponent client/imports/pages/chats/chats.component.ts
7
8
9
10
11
12
13
14
15
 
21
22
23
24
25
26
27
28
29
30
 
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import {Chats} from "../../../../both/collections/chats.collection";
import {Message} from "../../../../both/models/message.model";
import {Messages} from "../../../../both/collections/messages.collection";
import {NavController, PopoverController} from "ionic-angular";
import {MessagesPage} from "../chat/messages-page.component";
import {ChatsOptionsComponent} from '../chats/chats-options.component';
 
@Component({
  selector: "chats",
...some lines skipped...
export class ChatsComponent implements OnInit {
  chats: Observable<Chat[]>;
 
  constructor(
    private navCtrl: NavController,
    private popoverCtrl: PopoverController
    ) {}
 
  ngOnInit() {
    this.chats = Chats
...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});
  }
4
5
6
7
8
9
10
 
    <ion-buttons end>
      <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>
  </ion-navbar>
</ion-header>

It uses popover functionality from Ionic (see documentation).

As you can see, we used ChatsOptionsComponent.

Now let's implement the component for the chats options which will handle the profile editing and logging-out:

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
71
72
73
74
75
76
import {Component} from '@angular/core';
import {NavController, ViewController, AlertController} from 'ionic-angular';
import {Meteor} from 'meteor/meteor';
import {ProfileComponent} from '../auth/profile.component';
import {LoginComponent} from '../auth/login.component';
import template from './chats-options.component.html';
import style from "./chats-options.component.scss";
 
@Component({
  selector: 'chats-options',
  template,
  styles: [
    style
  ]
})
export class ChatsOptionsComponent {
  constructor(
    private navCtrl: NavController, 
    private viewCtrl: ViewController,
    private 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();
  }
}

Add a template and styles:

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>
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;
  }
}

Add it to AppModule:

5.32 Register ChatsOptionsComponent client/imports/app/app.module.ts
8
9
10
11
12
13
14
 
19
20
21
22
23
24
25
26
 
30
31
32
33
34
35
36
37
import {LoginComponent} from '../pages/auth/login.component';
import {VerificationComponent} from '../pages/auth/verification.component';
import {ProfileComponent} from '../pages/auth/profile.component';
import {ChatsOptionsComponent} from '../pages/chats/chats-options.component';
 
@NgModule({
  // Components, Pipes, Directive
...some lines skipped...
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent
  ],
  // Entry Components
  entryComponents: [
...some lines skipped...
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent
  ],
  // Providers
  providers: [

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 add the following scss file to the styles dir:

5.33 Add options-popover stylesheet client/styles/options-popover.scss
1
2
3
4
5
6
7
8
9
10
11
.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;
  }
}

Note that this style-sheet is not a component specific, so it has to be loaded as a global asset rather then provide it to the component during its initialization.

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