Our next step is about adding new chats. So far we have the chats list implemented and user management, we just need to connect the two.
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:
12
13
14
15
16
17
18
19
20
21
22
23
24
25
});
}
showNewChatModal() {
this.NewChat.showModal();
}
remove(chat) {
Chats.remove(chat._id);
}
}
ChatsCtrl.$name = 'ChatsCtrl';
ChatsCtrl.$inject = ['NewChat'];
Note that we first create the modal dialog with a template, and later on we will implement the logic of showing it on screen.
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
<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 in order to open this modal, we will create a service that takes care of it:
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
import { Service } from 'angular-ecmascript/module-helpers';
import newChatTemplateUrl from '../../templates/new-chat.html';
export default class NewChatService extends Service {
constructor() {
super(...arguments);
this.templateUrl = newChatTemplateUrl;
}
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();
}
}
NewChatService.$name = 'NewChat';
NewChatService.$inject = ['$rootScope', '$ionicModal'];
19
20
21
22
23
24
25
41
42
43
44
45
46
47
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);
// Startup
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
import { Meteor } from 'meteor/meteor';
import { Controller } from 'angular-ecmascript/module-helpers';
import { Chats } from '../../../lib/collections';
export default class NewChatCtrl extends Controller {
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';
NewChatCtrl.$inject = ['$state', 'NewChat', '$ionicPopup', '$log'];
15
16
17
18
19
20
21
38
39
40
41
42
43
44
import ChatCtrl from '../controllers/chat.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(ChatCtrl)
.load(ConfirmationCtrl)
.load(LoginCtrl)
.load(NewChatCtrl)
.load(ProfileCtrl)
.load(SettingsCtrl)
.load(InputDirective)
It includes the users collection and a function for creating a new chat. This function is not yet implemented in the server, so let's create it:
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
}
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 will be explained why further in this tutorial):
17
18
19
20
21
22
23
}
remove(chat) {
this.callMethod('removeChat', chat._id);
}
}
And we will implement the method on the server:
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const chatId = Chats.insert(chat);
return chatId;
},
removeChat(chatId) {
if (!this.userId) {
throw new Meteor.Error('not-logged-in',
'Must be logged to remove 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 {
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';
}
}
ChatNameFilter.$name = 'chatName';
20
21
22
23
24
25
26
44
45
46
47
48
49
50
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 NewChatService from '../services/new-chat.service';
import Routes from '../routes';
...some lines skipped...
.load(SettingsCtrl)
.load(InputDirective)
.load(CalendarFilter)
.load(ChatNameFilter)
.load(NewChatService)
.load(Routes);
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 {
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 || '/user-default.svg';
};
}
ChatPictureFilter.$name = 'chatPicture';
21
22
23
24
25
26
27
46
47
48
49
50
51
52
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(InputDirective)
.load(CalendarFilter)
.load(ChatNameFilter)
.load(ChatPictureFilter)
.load(NewChatService)
.load(Routes);
And we will add the usage of this filter in the chats list:
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">
<div class="message-list">
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! and now clicking a user will open a chat with that user.
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 remove this package by running this command:
$ meteor remove insecure