Fork me on GitHub

WhatsApp Clone with Meteor CLI and Ionic

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 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:

5.1 Add new chat button client/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:

5.2 Add new chat button logic client/scripts/controllers/chats.controller.js
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:

5.3 Add new chat modal view client/templates/new-chat.html
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:

5.4 Create new chat service client/scripts/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
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'];
5.5 Load new chat service client/scripts/lib/app.js
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'];
5.7 Load new chat controller client/scripts/lib/app.js
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:

5.8 Add new chat method lib/methods.js
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:

5.10 Add remove chat method lib/methods.js
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:

5.11 Create chat name filter client/scripts/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 {
  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';
5.12 Load chat name filter client/scripts/lib/app.js
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:

5.13 Create chat picture filter client/scripts/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 {
  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';
5.14 Load chat picture filter client/scripts/lib/app.js
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:

5.15 Apply chat filters in chats view client/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:

5.16 Apply chat filters in chat view client/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">
    <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:

5.17 Create users with phone data stub 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! 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