In this step we gonna take care of the app's security and encapsulation, since we don't want the users to do whatever they want, and we don't want them to be able to see content which is irrelevant for them.
We gonna start by removing a Meteor package named insecure
.
This package provides the client with the ability to run collection mutation methods. This is a behavior we are not interested in since removing data and creating data should be done in the server and only after certain validations.
Meteor includes this package by default only for development purposes and it should be removed once our app is ready for production.
So let's remove this package by running this command:
$ meteor remove insecure
With that we're able to add ability to remove chats:
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Chats.insert(chat);
},
removeChat(chatId: string): void {
if (!this.userId) throw new Meteor.Error('unauthorized',
'User must be logged-in to remove chat');
check(chatId, nonEmptyString);
const chatExists = !!Chats.collection.find(chatId).count();
if (!chatExists) throw new Meteor.Error('chat-not-exists',
'Chat doesn\'t exist');
Messages.remove({chatId});
Chats.remove(chatId);
},
updateProfile(profile: Profile): void {
if (!this.userId) throw new Meteor.Error('unauthorized',
'User must be logged-in to create a new chat');
Now that we have a dedicated method in the server, we can go ahead and take advantage of in in our app. In the messages page we have two buttons in the navigation bar, one is for sending attachments and one to open the options menu. The options menu is gonna be a pop-over, the same as in the chats page. Let's implement its component, which is gonna be called MessagesOptionsComponent
:
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 { NavParams, NavController, ViewController, AlertController } from 'ionic-angular';
import { MeteorObservable } from 'meteor-rxjs';
import { TabsPage } from "../tabs/tabs";
@Component({
selector: 'messages-options',
templateUrl: 'messages-options.html'
})
export class MessagesOptionsComponent {
constructor(
public navCtrl: NavController,
public viewCtrl: ViewController,
public alertCtrl: AlertController,
public params: NavParams
) {}
remove(): void {
const alert = this.alertCtrl.create({
title: 'Remove',
message: 'Are you sure you would like to proceed?',
buttons: [
{
text: 'Cancel',
role: 'cancel'
},
{
text: 'Yes',
handler: () => {
this.handleRemove(alert);
return false;
}
}
]
});
this.viewCtrl.dismiss().then(() => {
alert.present();
});
}
private handleRemove(alert): void {
MeteorObservable.call('removeChat', this.params.get('chat')._id).subscribe({
next: () => {
alert.dismiss().then(() => {
this.navCtrl.setRoot(TabsPage, {}, {
animate: true
});
});
},
error: (e: Error) => {
alert.dismiss().then(() => {
if (e) return this.handleError(e);
this.navCtrl.setRoot(TabsPage, {}, {
animate: true
});
});
}
});
}
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
<ion-content class="chats-options-page-content">
<ion-list class="options">
<button ion-item class="option option-remove" (click)="remove()">
<ion-icon name="remove" class="option-icon"></ion-icon>
<div class="option-name">Remove</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;
}
}
10
11
12
13
14
15
16
23
24
25
26
27
28
29
39
40
41
42
43
44
45
46
import { ProfileComponent } from "../pages/profile/profile";
import { ChatsOptionsComponent } from "../pages/chat-options/chat-options";
import { NewChatComponent } from "../pages/new-chat/new-chat";
import { MessagesOptionsComponent } from "../pages/messages-options/messages-options";
@NgModule({
declarations: [
...some lines skipped...
ProfileComponent,
ChatsOptionsComponent,
NewChatComponent,
MessagesOptionsComponent
],
imports: [
IonicModule.forRoot(MyApp),
...some lines skipped...
VerificationComponent,
ProfileComponent,
ChatsOptionsComponent,
NewChatComponent,
MessagesOptionsComponent
],
providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
Now we can go ahead and implement the method in the messages page for showing this popover:
1
2
3
4
5
6
7
8
9
10
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { Component, OnInit, OnDestroy, ElementRef } from "@angular/core";
import { NavParams, PopoverController } from "ionic-angular";
import { Chat, Message } from "api/models/whatsapp-models";
import { Messages } from "api/collections/whatsapp-collections";
import { Observable } from "rxjs";
import { MeteorObservable } from "meteor-rxjs";
import { MessagesOptionsComponent } from "../messages-options/messages-options";
@Component({
selector: "messages-page",
...some lines skipped...
autoScroller: MutationObserver;
senderId: string;
constructor(navParams: NavParams, element: ElementRef, public popoverCtrl: PopoverController) {
this.selectedChat = <Chat>navParams.get('chat');
this.title = this.selectedChat.title;
this.picture = this.selectedChat.picture;
this.senderId = Meteor.userId();
}
showOptions(): void {
const popover = this.popoverCtrl.create(MessagesOptionsComponent, {
chat: this.selectedChat
}, {
cssClass: 'options-popover'
});
popover.present();
}
private get messagesPageContent(): Element {
And last but not least, let's update our view and bind the event to its handler:
8
9
10
11
12
13
14
<ion-buttons end>
<button ion-button icon-only class="attach-button"><ion-icon name="attach"></ion-icon></button>
<button ion-button icon-only class="settings-button" (click)="showOptions()"><ion-icon name="more"></ion-icon></button>
</ion-buttons>
</ion-navbar>
</ion-header>
Now let's use the chat removal method in the chats view once we slide a chat item to the right and press the remove
button:
2
3
4
5
6
7
8
17
18
19
20
21
22
23
24
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { Observable } from "rxjs";
import { Chat } from "api/models/whatsapp-models";
import { Chats, Messages, Users } from "api/collections/whatsapp-collections";
import { NavController, PopoverController, ModalController, AlertController } from "ionic-angular";
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
import { NewChatComponent } from "../new-chat/new-chat";
...some lines skipped...
constructor(
public navCtrl: NavController,
public popoverCtrl: PopoverController,
public modalCtrl: ModalController,
public alertCtrl: AlertController
) {}
ngOnInit() {
...some lines skipped...
}
removeChat(chat: Chat): void {
MeteorObservable.call('removeChat', chat._id).subscribe({
complete: () => {
this.chats = this.chats.zone();
},
error: (e: Error) => {
if (e) 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();
}
}
Right now all the chats are published to all the clients which is not very good for privacy. Let's fix that.
First thing we need to do in order to stop all the automatic publication of information is to remove the autopublish
package from the Meteor server:
$ meteor remove autopublish
Now we need to explicitly define our publications. Let's start by sending the users' information:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { Users, Messages, Chats } from '../collections/whatsapp-collections';
import { User, Message, Chat } from 'api/models/whatsapp-models';
export function initPublications() {
Meteor.publish('users', function(): Mongo.Cursor<User> {
if (!this.userId) return;
return Users.collection.find({}, {
fields: {
profile: 1
}
});
});
}
And add the messages:
14
15
16
17
18
19
20
21
22
23
24
});
});
Meteor.publish('messages', function(chatId: string): Mongo.Cursor<Message> {
if (!this.userId) return;
if (!chatId) return;
return Messages.collection.find({chatId});
});
}
We will now add the publish-composite package which will help us implement joined collection publications.
$ meteor add reywood:publish-composite
And we will install its typings declarations:
$ npm install --save @types/meteor-publish-composite
And import them:
21
22
23
24
25
26
27
"@ionic/storage": "1.1.6",
"@types/meteor": "^1.3.31",
"@types/meteor-accounts-phone": "0.0.5",
"@types/meteor-publish-composite": "0.0.32",
"@types/underscore": "^1.7.36",
"accounts-base-client-side": "^0.1.1",
"accounts-phone": "0.0.1",
13
14
15
16
17
18
19
*/
/// <reference types="meteor-typings" />
/// <reference types="@types/meteor-accounts-phone" />
/// <reference types="@types/meteor-publish-composite" />
/// <reference types="@types/underscore" />
/// <reference path="../api/models/whatsapp-models.d.ts" />
declare module '*';
Now we will use Meteor.publishComposite
from the package we installed and create a publication of Chats
:
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
return Messages.collection.find({chatId});
});
Meteor.publishComposite('chats', function() {
if (!this.userId) return;
return {
find: () => {
return Chats.collection.find({memberIds: this.userId});
},
children: [
{
find: (chat) => {
return Messages.collection.find({chatId: chat._id}, {
sort: {createdAt: -1},
limit: 1
});
}
},
{
find: (chat) => {
return Users.collection.find({
_id: {$in: chat.memberIds}
}, {
fields: {profile: 1}
});
}
}
]
};
});
}
The chats publication is a composite publication which is made of several nodes. First we gonna find all the relevant chats for the current user logged in. After we have the chats, we gonna return the following cursor for each chat document we found. First we gonna return all the last messages, and second we gonna return all the users we're currently chatting with.
Those publications are still not visible by server, we need to import and run the init method:
2
3
4
5
6
7
8
9
10
11
12
13
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { initMethods } from "./methods";
import { initPublications } from "./publications";
import { Users } from "../collections/whatsapp-collections";
Meteor.startup(() => {
initMethods();
initPublications();
if (Meteor.settings) {
Object.assign(Accounts._options, Meteor.settings['accounts-phone']);
Let's add the subscription for the chats publication in the chats component:
6
7
8
9
10
11
12
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 { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
import { NewChatComponent } from "../new-chat/new-chat";
import { MeteorObservable } from 'meteor-rxjs';
@Component({
templateUrl: 'chats.html'
...some lines skipped...
ngOnInit() {
this.senderId = Meteor.userId();
MeteorObservable.subscribe('chats').subscribe(() => {
MeteorObservable.autorun().subscribe(() => {
if (this.chats) {
this.chats.unsubscribe();
this.chats = undefined;
}
this.chats = Chats
.find({})
.mergeMap((chats: Chat[]) =>
Observable.combineLatest(
...chats.map((chat: Chat) =>
Messages
.find({chatId: chat._id})
.startWith(null)
.map(messages => {
if (messages) chat.lastMessage = messages[0];
return chat;
})
)
)
).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 {
The users publication publishes all the users' profiles, and we need to use it in the new chat dialog whenever we wanna create a new chat.
Let's subscribe to the users publication in the new chat component:
21
22
23
24
25
26
27
28
29
30
}
ngOnInit() {
MeteorObservable.subscribe('users').subscribe(() => {
MeteorObservable.autorun().subscribe(() => {
this.users = this.findUsers().zone();
});
});
}
The messages publication is responsible for bringing all the relevant messages for a certain chat. This publication is actually parameterized and it requires us to pass a chat id during subscription.
Let's subscribe to the messages publication in the messages component, and pass the current active chat id provided to us by the nav params:
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
}
ngOnInit() {
MeteorObservable.subscribe('messages', this.selectedChat._id).subscribe(() => {
MeteorObservable.autorun().subscribe(() => {
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;
});
});
});
this.autoScroller = this.autoScroll();
{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/chats-mutations" next_ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/summary"}}}