Fork me on GitHub

WhatsApp Clone with Meteor and Ionic CLI

Create and remove chats

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, so first let's add a button that opens this dialog to the chats list:

6.1 Add chat creation button to chats view www/templates/chats.html
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:

6.2 Add new chat modal method to chats controller src/controllers/chats.controller.js
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:

6.3 Add new chat service src/services/new-chat.service.js
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();
  }
}
6.4 Load new chat service src/app.js
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:

6.5 Add new chat view www/templates/new-chat.html
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:

6.6 Add new chat controller src/controllers/new-chat.controller.js
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';
6.7 Load new chat controller src/app.js
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:

6.8 Add new chat method to api api/server/methods.js
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):

6.9 Replace manual chat removal with method invokation in chats controller src/controllers/chats.controller.js
19
20
21
22
23
24
25
  }
 
  remove(chat) {
    this.callMethod('removeChat', chat._id);
  }
}
 

And we will implement the method on the server:

6.10 Add chat removal method to api api/server/methods.js
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:

6.11 Add chat name filter src/filters/chat-name.filter.js
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:

6.12 Add chat picture filter src/filters/chat-picture.filter.js
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:

6.13 Load chat name and picture filters src/app.js
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:

6.14 Apply chat name and picture filters in chats view www/templates/chats.html
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:

6.15 Apply chat name and picture filters in chat view www/templates/chat.html
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:

6.16 Create initial users with phone data stubs api/server/bootstrap.js
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