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, so first let's add a button that opens this dialog to the chats list:
1
2
3
4
5
6
7
8
<ion-view view-title="Chats">
<ion-nav-buttons side="right">
<button ng-click="chats.showNewChatModal()" class="button button-clear button-positive button-icon ion-ios-compose-outline"></button>
</ion-nav-buttons>
<ion-content>
<ion-list>
<ion-item ng-repeat="chat in chats.data | orderBy:'-lastMessage.timestamp'"
This button calls a controller method, which we will implement now in the controller:
2
3
4
5
6
7
8
9
14
15
16
17
18
19
20
21
22
23
import { Controller } from 'angular-ecmascript/module-helpers';
export default class ChatsCtrl extends Controller {
static $inject = ['NewChat']
constructor() {
super(...arguments);
...some lines skipped...
});
}
showNewChatModal() {
this.NewChat.showModal();
}
remove(chat) {
this.data.remove(chat._id);
}
Note that we first create the modal dialog with a template, and then later we open it in the button function.
Inorder to open this modal, we will create a service that takes care of that:
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
import { Service } from 'angular-ecmascript/module-helpers';
export default class NewChatService extends Service {
static $inject = ['$rootScope', '$ionicModal']
static $name = 'NewChat'
constructor() {
super(...arguments);
this.templateUrl = 'templates/new-chat.html';
}
showModal() {
this.scope = this.$rootScope.$new();
this.$ionicModal.fromTemplateUrl(this.templateUrl, {
scope: this.scope
})
.then((modal) => {
this.modal = modal;
this.modal.show();
});
}
hideModal() {
this.scope.$destroy();
this.modal.remove();
}
}
12
13
14
15
16
17
18
33
34
35
36
37
38
39
import SettingsCtrl from './controllers/settings.controller';
import InputDirective from './directives/input.directive';
import CalendarFilter from './filters/calendar.filter';
import NewChatService from './services/new-chat.service';
import Routes from './routes';
const App = 'whatsapp';
...some lines skipped...
.load(SettingsCtrl)
.load(InputDirective)
.load(CalendarFilter)
.load(NewChatService)
.load(Routes);
Ionic.Platform.ready(() => {
Now let's add the view of this modal dialog, which is just a list of users:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<ion-modal-view ng-controller="NewChatCtrl as chat">
<ion-header-bar>
<h1 class="title">New Chat</h1>
<div class="buttons">
<button class="button button-clear button-positive" ng-click="chat.hideNewChatModal()">Cancel</button>
</div>
</ion-header-bar>
<ion-content>
<div class="list">
<a ng-repeat="user in chat.users" ng-click="chat.newChat(user._id)" class="item">
<h2>{{user.profile.name}}</h2>
<p>
Hey there! I am using meteor-Whatsapp with meteor.
</p>
</a>
</div>
</ion-content>
</ion-modal-view>
And now we will add the controller of this view, and use the NewChat
service:
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
import { Meteor } from 'meteor/meteor';
import { Chats } from 'api/collections';
import { Controller } from 'angular-ecmascript/module-helpers';
export default class NewChatCtrl extends Controller {
static $inject = ['$state', 'NewChat', '$ionicPopup', '$log']
constructor() {
super(...arguments);
this.helpers({
users() {
return Meteor.users.find({ _id: { $ne: this.currentUserId } });
}
});
}
newChat(userId) {
let chat = Chats.findOne({ userIds: { $all: [this.currentUserId, userId] } });
if (chat) {
this.hideNewChatModal();
return this.goToChat(chat._id);
}
this.callMethod('newChat', userId, (err, chatId) => {
this.hideNewChatModal();
if (err) return this.handleError(err);
this.goToChat(chatId);
});
}
hideNewChatModal() {
this.NewChat.hideModal();
}
goToChat(chatId) {
this.$state.go('tab.chat', { chatId });
}
handleError(err) {
this.$log.error('New chat creation error ', err);
this.$ionicPopup.alert({
title: err.reason || 'New chat creation failed',
template: 'Please try again',
okType: 'button-positive button-clear'
});
}
}
NewChatCtrl.$name = 'NewChatCtrl';
8
9
10
11
12
13
14
30
31
32
33
34
35
36
import ChatsCtrl from './controllers/chats.controller';
import ConfirmationCtrl from './controllers/confirmation.controller';
import LoginCtrl from './controllers/login.controller';
import NewChatCtrl from './controllers/new-chat.controller';
import ProfileCtrl from './controllers/profile.controller';
import SettingsCtrl from './controllers/settings.controller';
import InputDirective from './directives/input.directive';
...some lines skipped...
.load(ChatsCtrl)
.load(ConfirmationCtrl)
.load(LoginCtrl)
.load(NewChatCtrl)
.load(ProfileCtrl)
.load(SettingsCtrl)
.load(InputDirective)
The controller includes a server method for creating a chat which is not yet implemented, so let's create it:
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
}
return Meteor.users.update(this.userId, { $set: { 'profile.name': name } });
},
newChat(otherId) {
if (!this.userId) {
throw new Meteor.Error('not-logged-in',
'Must be logged to create a chat.');
}
check(otherId, String);
const otherUser = Meteor.users.findOne(otherId);
if (!otherUser) {
throw new Meteor.Error('user-not-exists',
'Chat\'s user not exists');
}
const chat = {
userIds: [this.userId, otherId],
createdAt: new Date()
};
const chatId = Chats.insert(chat);
return chatId;
}
});
We will also rewrite the logic of removeChat()
function in the ChatsCtrl
and we will call a server method instead (which we will explain why further in this tutorial):
19
20
21
22
23
24
25
}
remove(chat) {
this.callMethod('removeChat', chat._id);
}
}
And we will implement the method on the server:
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
const chatId = Chats.insert(chat);
return chatId;
},
removeChat(chatId) {
if (!this.userId) {
throw new Meteor.Error('not-logged-in',
'Must be logged to create a chat.');
}
check(chatId, String);
const chat = Chats.findOne(chatId);
if (!chat || !_.include(chat.userIds, this.userId)) {
throw new Meteor.Error('chat-not-exists',
'Chat not exists');
}
Messages.remove({ chatId: chatId });
return Chats.remove({ _id: chatId });
}
});
The next messages won't include the username, only the user id, so we need to change the logic of username display. We will add a filter that fetches the user object from the Users
collection according to the userId
property of the chat object:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { _ } from 'meteor/underscore';
import { Meteor } from 'meteor/meteor';
import { Filter } from 'angular-ecmascript/module-helpers';
export default class ChatNameFilter extends Filter {
static $name = 'chatName'
filter(chat) {
if (!chat) return;
let otherId = _.without(chat.userIds, Meteor.userId())[0];
let otherUser = Meteor.users.findOne(otherId);
let hasName = otherUser && otherUser.profile && otherUser.profile.name;
return hasName ? otherUser.profile.name : chat.name || 'NO NAME';
}
}
And we will also create the same logic for fetching the user's image:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { _ } from 'meteor/underscore';
import { Meteor } from 'meteor/meteor';
import { Filter } from 'angular-ecmascript/module-helpers';
export default class ChatPictureFilter extends Filter {
static $name = 'chatPicture'
filter(chat) {
if (!chat) return;
let otherId = _.without(chat.userIds, Meteor.userId())[0];
let otherUser = Meteor.users.findOne(otherId);
let hasPicture = otherUser && otherUser.profile && otherUser.profile.picture;
return hasPicture ? otherUser.profile.picture : chat.picture || '/img/user-default.svg';
}
}
And we will load our filters:
13
14
15
16
17
18
19
20
37
38
39
40
41
42
43
44
import SettingsCtrl from './controllers/settings.controller';
import InputDirective from './directives/input.directive';
import CalendarFilter from './filters/calendar.filter';
import ChatNameFilter from './filters/chat-name.filter';
import ChatPictureFilter from './filters/chat-picture.filter';
import NewChatService from './services/new-chat.service';
import Routes from './routes';
...some lines skipped...
.load(SettingsCtrl)
.load(InputDirective)
.load(CalendarFilter)
.load(ChatNameFilter)
.load(ChatPictureFilter)
.load(NewChatService)
.load(Routes);
And we will add the usage of these filters in the chats list view:
9
10
11
12
13
14
15
16
class="item-chat item-remove-animate item-avatar item-icon-right"
type="item-text-wrap"
href="#/tab/chats/{{ chat._id }}">
<img ng-src="{{ chat | chatPicture }}">
<h2>{{ chat | chatName }}</h2>
<p>{{ chat.lastMessage.text }}</p>
<span class="last-message-timestamp">{{ chat.lastMessage.timestamp | calendar }}</span>
<i class="icon ion-chevron-right icon-accessory"></i>
And in the chat view:
1
2
3
4
5
6
<ion-view title="{{ chat.data | chatName }}">
<ion-nav-buttons side="right">
<button class="button button-clear"><img class="header-picture" ng-src="{{ chat.data | chatPicture }}"></button>
</ion-nav-buttons>
<ion-content class="chat" delegate-handle="chatScroll">
Now we want to get rid of the current data we have, which is just a static data.
So let's stop our Meteor
's server and reset the whole app by running:
$ meteor reset
Let's add some users to the server instead of the old static data:
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
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
Meteor.startup(function() {
if (Meteor.users.find().count() != 0) return;
Accounts.createUserWithPhone({
phone: '+972501234567',
profile: {
name: 'My friend 1'
}
});
Accounts.createUserWithPhone({
phone: '+972501234568',
profile: {
name: 'My friend 2'
}
});
Accounts.createUserWithPhone({
phone: '+972501234569',
profile: {
name: 'My friend 3'
}
});
});
Run it again.
Cool! Now once we click a user a new chat should be created with it.
Our last part of this step is to remove Meteor
's package named insecure
.
This package provides the ability to run remove()
method from the client side in our collection. This is a behavior we do not want to use because removing data and creating data should be done in the server and only after certain validations, and this is the reason for implementing the removeChat()
method in the server.
Meteor
includes this package only for development purposes and it should be removed once our app is ready for production.
So remote this package by running this command:
$ meteor remove insecure