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 (see documentation). 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 client/imports/pages/chats/chats.component.ts
7
8
9
10
11
12
13
14
15
16
 
24
25
26
27
28
29
30
31
 
47
48
49
50
51
52
53
54
55
56
57
import {Chats} from "../../../../both/collections/chats.collection";
import {Message} from "../../../../both/models/message.model";
import {Messages} from "../../../../both/collections/messages.collection";
import {NavController, PopoverController, ModalController} from "ionic-angular";
import {MessagesPage} from "../chat/messages-page.component";
import {ChatsOptionsComponent} from '../chats/chats-options.component';
import {NewChatComponent} from './new-chat.component';
 
@Component({
  selector: "chats",
...some lines skipped...
 
  constructor(
    private navCtrl: NavController,
    private popoverCtrl: PopoverController,
    private modalCtrl: ModalController
    ) {}
 
  ngOnInit() {
...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:

3
4
5
6
7
8
9
    <ion-title>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()"><ion-icon name="more"></ion-icon></button>
    </ion-buttons>
  </ion-navbar>

The dialog should contain a list of all the users whom 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 server/imports/methods/methods.ts
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
      $set: {profile}
    });
  },
  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);
  },
  addMessage(chatId: string, content: string): 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 both/models/chat.model.ts
2
3
4
5
6
7
8
 
export interface Chat {
  _id?: string;
  memberIds?: string[];
  title?: string;
  picture?: string;
  lastMessage?: Message;

We're going to use Meteor.users so let's create a Observable Collection and call it Users:

6.5 Create Observable collection from Meteor.users both/collections/users.collection.ts
1
2
3
4
5
import {Meteor} from 'meteor/meteor';
import {MongoObservable} from "meteor-rxjs";
import {User} from "../models/user.model";
 
export const Users = MongoObservable.fromExisting<User>(Meteor.users);
6.6 Create a User model both/models/user.model.ts
1
2
3
4
5
6
7
import { Meteor } from 'meteor/meteor';
 
import { Profile } from '../models/profile.model';
 
export interface User extends Meteor.User {
  profile?: Profile;
}

We used fromExisting() method which does exactly what the name says.

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

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
77
78
79
80
81
82
83
84
import {Component, OnInit} from [email protected]/core';
import {MeteorObservable, ObservableCursor} from 'meteor-rxjs';
import {NavController, ViewController, AlertController} from 'ionic-angular';
import {Meteor} from 'meteor/meteor';
import {Observable} from 'rxjs/Observable';
import {Chats} from '../../../../both/collections/chats.collection';
import {Users} from '../../../../both/collections/users.collection';
import {User} from '../../../../both/models/user.model';
import template from './new-chat.component.html';
import style from "./new-chat.component.scss";
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/startWith';
 
@Component({
  selector: 'new-chat',
  template,
  styles: [
    style
  ]
})
export class NewChatComponent implements OnInit {
  users: Observable<User>;
  private senderId: string;
 
  constructor(
    private navCtrl: NavController, 
    private viewCtrl: ViewController,
    private 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(): Observable<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();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<ion-header>
  <ion-toolbar whatsapp>
    <ion-title>New Chat</ion-title>
 
    <ion-buttons end>
      <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>
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 client/imports/app/app.module.ts
9
10
11
12
13
14
15
 
21
22
23
24
25
26
27
28
 
33
34
35
36
37
38
39
40
import {VerificationComponent} from '../pages/auth/verification.component';
import {ProfileComponent} from '../pages/auth/profile.component';
import {ChatsOptionsComponent} from '../pages/chats/chats-options.component';
import {NewChatComponent} from '../pages/chats/new-chat.component';
 
@NgModule({
  // Components, Pipes, Directive
...some lines skipped...
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent,
    NewChatComponent
  ],
  // Entry Components
  entryComponents: [
...some lines skipped...
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent,
    NewChatComponent
  ],
  // Providers
  providers: [

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 server/imports/server-main/main.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
import {Chats} from "../../../both/collections/chats.collection";
import {Messages} from "../../../both/collections/messages.collection";
import {Users} from '../../../both/collections/users.collection';
import {Accounts} from 'meteor/accounts-base';
 
export class Main {
  start(): void {
    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);
    });
  }
}

Since we 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.12 Add title and picture to chat client/imports/pages/chats/chats.component.ts
1
2
3
4
5
6
7
 
22
23
24
25
26
27
28
 
31
32
33
34
35
36
37
 
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import {Component, OnInit} from "@angular/core";
import template from "./chats.component.html"
import {Observable} from "rxjs";
import {Meteor} from 'meteor/meteor';
import {Chat} from "../../../../both/models/chat.model";
import * as moment from "moment";
import style from "./chats.component.scss";
...some lines skipped...
})
export class ChatsComponent implements OnInit {
  chats: Observable<Chat[]>;
  senderId: string;
 
  constructor(
    private navCtrl: NavController,
...some lines skipped...
    ) {}
 
  ngOnInit() {
    this.senderId = Meteor.userId();
    this.chats = Chats
      .find({})
      .mergeMap<Chat[]>(chats =>
...some lines skipped...
 
          )
        )
      ).map(chats => {
        chats.forEach(chat => {
          const receiver = Meteor.users.findOne(chat.memberIds.find(memberId => memberId !== this.senderId))
 
          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.