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:
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:
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:
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:
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
:
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:
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:
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();
}
}
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>
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;
}
}
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:
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
:
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:
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.