Fork me on GitHub

WhatsApp Clone with Meteor and Ionic 2 CLI

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

Chats Creation & Removal

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

Our next step is about adding the ability to create new chats. So far we had the chats list and the users feature, we just need to connect them.

We will open the new chat view using Ionic's modal dialog. The dialog is gonna pop up from the chats view once we click on the icon at the top right corner of the view. Let's implement the handler in the chats component first:

6.1 Add 'addChat' method to ChatsComponent src/pages/chats/chats.ts
2
3
4
5
6
7
8
 
12
13
14
15
16
17
18
19
20
21
22
 
36
37
38
39
40
41
42
43
44
45
46
import { Observable } from "rxjs";
import { Chat } from "api/models/whatsapp-models";
import { Chats, Messages } from "api/collections/whatsapp-collections";
import { NavController, PopoverController, ModalController } from "ionic-angular";
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
 
...some lines skipped...
export class ChatsPage implements OnInit {
  chats;
 
  constructor(
    public navCtrl: NavController,
    public popoverCtrl: PopoverController,
    public modalCtrl: ModalController
  ) {}
 
  ngOnInit() {
    this.chats = Chats
...some lines skipped...
      ).zone();
  }
 
  addChat(): void {
    const modal = this.modalCtrl.create(NewChatComponent);
    modal.present();
  }
 
  showOptions(): void {
    const popover = this.popoverCtrl.create(ChatsOptionsComponent, {}, {
      cssClass: 'options-popover'

And let's bind the event to the view:

6.2 Bind that method src/pages/chats/chats.html
4
5
6
7
8
9
10
      Chats
    </ion-title>
    <ion-buttons end>
      <button ion-button icon-only class="add-chat-button" (click)="addChat()">
        <ion-icon name="person-add"></ion-icon>
      </button>
      <button ion-button icon-only class="options-button" (click)="showOptions()">

The dialog should contain a list of all the users whose chat does not exist yet. Once we click on one of these users we should be demoted to the chats view with the new chat we've just created.

Since we wanna insert a new chat we need to create the corresponding method in the methods.ts file:

6.3 Define 'addChat' Method api/server/methods.ts
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
 
export function initMethods() {
  Meteor.methods({
    addChat(receiverId: string): void {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');
 
      check(receiverId, nonEmptyString);
 
      if (receiverId == this.userId) throw new Meteor.Error('illegal-receiver',
        'Receiver must be different than the current logged in user');
 
      const chatExists = !!Chats.collection.find({
        memberIds: {$all: [this.userId, receiverId]}
      }).count();
 
      if (chatExists) throw new Meteor.Error('chat-exists',
        'Chat already exists');
 
      const chat = {
        memberIds: [this.userId, receiverId]
      };
 
      Chats.insert(chat);
    },
    updateProfile(profile: Profile): void {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');

As you can see, a chat is inserted with an additional memberIds field. Let's update the chat model accordingly:

6.4 Add memberIds prop in Chat model api/models/whatsapp-models.d.ts
9
10
11
12
13
14
15
    title?: string;
    picture?: string;
    lastMessage?: Message;
    memberIds?: string[];
  }
 
  interface Message {

Now, in order to have access to the users collection, we need to wrap it with a MeteorObservable:

6.5 Create Observable collection from Meteor.users api/collections/whatsapp-collections.ts
1
2
3
4
5
6
import { MongoObservable } from "meteor-rxjs";
import { Meteor } from "meteor/meteor";
 
export const Chats = new MongoObservable.Collection("chats");
export const Messages = new MongoObservable.Collection("messages");
export const Users = MongoObservable.fromExisting(Meteor.users);

We're also required to create an interface to the user model so TypeScript will recognize it:

6.6 Create a User model api/models/whatsapp-models.d.ts
20
21
22
23
24
25
26
27
    ownership?: string;
    senderId?: string;
  }
 
  interface User extends Meteor.User {
    profile?: Profile;
  }
}

Now that we have the method ready we can go ahead and implement the new chat dialog:

6.7 Create NewChatComponent src/pages/new-chat/new-chat.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
71
72
73
74
import { Component, OnInit } from '@angular/core';
import { MeteorObservable, ObservableCursor } from 'meteor-rxjs';
import { NavController, ViewController, AlertController } from 'ionic-angular';
import { Chats, Users } from 'api/collections/whatsapp-collections';
import { User } from 'api/models/whatsapp-models';
 
@Component({
  selector: 'new-chat',
  templateUrl: 'new-chat.html'
})
export class NewChatComponent implements OnInit {
  users;
  senderId: string;
 
  constructor(
    public navCtrl: NavController,
    public viewCtrl: ViewController,
    public alertCtrl: AlertController
  ) {
    this.senderId = Meteor.userId();
  }
 
  ngOnInit() {
    MeteorObservable.autorun().zone().subscribe(() => {
      this.users = this.findUsers().zone();
    });
  }
 
  addChat(user): void {
    MeteorObservable.call('addChat', user._id).subscribe({
      next: () => {
        this.viewCtrl.dismiss();
      },
      error: (e: Error) => {
        this.viewCtrl.dismiss().then(() => {
          this.handleError(e);
        });
      }
    });
  }
 
  private findUsers(): ObservableCursor<User> {
    return Chats.find({
      memberIds: this.senderId
    }, {
      fields: {
        memberIds: 1
      }
    })
      .startWith([]) // empty result
      .mergeMap((chats) => {
        const recieverIds = chats
          .map(({memberIds}) => memberIds)
          .reduce((result, memberIds) => result.concat(memberIds), [])
          .concat(this.senderId);
 
        return Users.find({
          _id: {$nin: recieverIds}
        })
      });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}
6.8 Create NewChatComponent view src/pages/new-chat/new-chat.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<ion-header>
  <ion-toolbar color="whatsapp">
    <ion-title>New Chat</ion-title>
 
    <ion-buttons end>
      <button ion-button class="dismiss-button" (click)="viewCtrl.dismiss()"><ion-icon name="close"></ion-icon></button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>
 
<ion-content class="new-chat">
  <ion-list class="users">
    <button ion-item *ngFor="let user of users | async" class="user" (click)="addChat(user)">
      <img class="user-picture" [src]="user.profile.picture">
      <h2 class="user-name">{{user.profile.name}}</h2>
    </button>
  </ion-list>
</ion-content>
6.9 Create NewChatComponent view style src/pages/new-chat/new-chat.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.new-chat {
  .user-picture {
    border-radius: 50%;
    width: 50px;
    float: left;
  }
 
  .user-name {
    margin-left: 20px;
    margin-top: 25px;
    transform: translate(0, -50%);
    float: left;
  }
}
6.10 Register that component src/app/app.module.ts
9
10
11
12
13
14
15
 
20
21
22
23
24
25
26
27
 
36
37
38
39
40
41
42
43
import { VerificationComponent } from "../pages/verification/verification";
import { ProfileComponent } from "../pages/profile/profile";
import { ChatsOptionsComponent } from "../pages/chat-options/chat-options";
import { NewChatComponent } from "../pages/new-chat/new-chat";
 
@NgModule({
  declarations: [
...some lines skipped...
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent,
    NewChatComponent,
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent,
    NewChatComponent
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})

Thanks to our new-chat dialog, we can create chats dynamically with no need in initial fabrication. Let's replace the chats fabrication with users fabrication in the Meteor server:

6.11 Replace chats fabrication with users fabrication api/server/main.ts
2
3
4
5
6
7
8
 
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
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { initMethods } from "./methods";
import { Users } from "../collections/whatsapp-collections";
 
Meteor.startup(() => {
  initMethods();
...some lines skipped...
    SMS.twilio = Meteor.settings['twilio'];
  }
 
  if (Users.collection.find().count()) return;
 
  [{
    phone: '+972540000001',
    profile: {
      name: 'Ethan Gonzalez',
      picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg'
    }
  }, {
    phone: '+972540000002',
    profile: {
      name: 'Bryan Wallace',
      picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg'
    }
  }, {
    phone: '+972540000003',
    profile: {
      name: 'Avery Stewart',
      picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg'
    }
  }, {
    phone: '+972540000004',
    profile: {
      name: 'Katie Peterson',
      picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg'
    }
  }, {
    phone: '+972540000005',
    profile: {
      name: 'Ray Edwards',
      picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg'
    }
  }].forEach(user => {
    Accounts.createUserWithPhone(user);
  });
});

And let's add the missing import inside the ChatsPage:

6.12 Added missing import src/pages/chats/chats.ts
5
6
7
8
9
10
11
import { NavController, PopoverController, ModalController } from "ionic-angular";
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
import { NewChatComponent } from "../new-chat/new-chat";
 
@Component({
  templateUrl: 'chats.html'

Since we've changed the data fabrication method, the chat's title and picture are not hardcoded anymore, therefore they should be calculated in the components themselves. Let's calculate those fields in the chats component:

6.13 Add title and picture to chat src/pages/chats/chats.ts
1
2
3
4
5
6
7
 
12
13
14
15
16
17
18
 
21
22
23
24
25
26
27
28
 
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { Component, OnInit } from '@angular/core';
import { Observable } from "rxjs";
import { Chat } from "api/models/whatsapp-models";
import { Chats, Messages, Users } from "api/collections/whatsapp-collections";
import { NavController, PopoverController, ModalController } from "ionic-angular";
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
...some lines skipped...
})
export class ChatsPage implements OnInit {
  chats;
  senderId: string;
 
  constructor(
    public navCtrl: NavController,
...some lines skipped...
  ) {}
 
  ngOnInit() {
    this.senderId = Meteor.userId();
 
    this.chats = Chats
      .find({})
      .mergeMap((chats: Chat[]) =>
...some lines skipped...
              })
          )
        )
      ).map(chats => {
        chats.forEach(chat => {
          chat.title = '';
          chat.picture = '';
 
          const receiver = Users.findOne(chat.memberIds.find(memberId => memberId !== this.senderId));
          if (!receiver) return;
 
          chat.title = receiver.profile.name;
          chat.picture = receiver.profile.picture;
        });
 
        return chats;
      }).zone();
  }
 
  addChat(): void {

Now we want our changes to take effect. We will reset the database so next time we run our Meteor server the users will be fabricated. To reset the database, first make sure the Meteor server is stopped and then type the following command:

$ meteor reset

And once we start our server again it should go through the initialization method and fabricate the users.