Fork me on GitHub

WhatsApp Clone with Ionic 2 and Meteor CLI

Legacy Tutorial Version (Last Update: 22.11.2016)

Realtime Meteor Server

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

Now that we have the initial chats layout and its component, we will take it a step further by providing the chats data from a server instead of having it locally. In this step we will be implementing the API server and we will do so using Meteor with Mongo.

Collections

In Meteor, we keep data inside Mongo.Collections.

This collection is actually a reference to a MongoDB collection, and it is provided to us by a Meteor package called Minimongo, and it shares almost the same API as a native MongoDB collection.

We can also wrap it with RxJS' Observables using meteor-rxjs.

That package has been already installed, it's a part of the boilerplate.

Let's create a Collection of Chats:

3.1 Add the Chats collection both/collections/chats.collection.ts
1
2
3
4
import {Chat} from "../models/chat.model";
import {MongoObservable} from "meteor-rxjs";
 
export const Chats = new MongoObservable.Collection<Chat>('chats');

And also for Messages:

3.2 Add the Messages collection both/collections/messages.collection.ts
1
2
3
4
import {MongoObservable} from "meteor-rxjs";
import {Message} from "../models/message.model";
 
export const Messages = new MongoObservable.Collection<Message>('messages');

Data fixtures

Since we have Collections, we can now move on to fill them with data.

3.3 Added the stub data to the server server/imports/server-main/main.ts
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
import {Chats} from "../../../both/collections/chats.collection";
import {Messages} from "../../../both/collections/messages.collection";
import * as moment from "moment";
 
export class Main {
  start(): void {
    if (Chats.collection.find().count()) return;
 
    let chatId;
 
    chatId = Chats.collection.insert({
      title: 'Ethan Gonzalez',
      picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'You on your way?',
      createdAt: moment().subtract(1, 'hours').toDate()
    });
 
    chatId = Chats.collection.insert({
      title: 'Bryan Wallace',
      picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'Hey, it\'s me',
      createdAt: moment().subtract(2, 'hours').toDate()
    });
 
    chatId = Chats.collection.insert({
      title: 'Avery Stewart',
      picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'I should buy a boat',
      createdAt: moment().subtract(1, 'days').toDate()
    });
 
    chatId = Chats.collection.insert({
      title: 'Katie Peterson',
      picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'Look at my mukluks!',
      createdAt: moment().subtract(4, 'days').toDate()
    });
 
    chatId = Chats.collection.insert({
      title: 'Ray Edwards',
      picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'This is wicked good ice cream.',
      createdAt: moment().subtract(2, 'weeks').toDate()
    });
  }
}

Quick overview. We use .collection to get the actual Mongo.Collection instance, this way we avoid using Observables. At the beginning we check if Chats Collection is empty by using .count() operator. Then we provide few chats with one message each.

We also bundled Message with a Chat using chatId property.

This requires a small change in the model:

3.4 Add 'chatId' property to message model both/models/message.model.ts
1
2
3
4
5
6
export interface Message {
  _id?: string;
  chatId?: string;
  content?: string;
  createdAt?: Date;
}

UI

Since Meteor's API requires us to share some of the code in both client and server, we have to import all the collections on the client-side too.

We also want to provide that data to the component:

3.5 Added the chats with the last message using RxJS operators client/imports/pages/chats/chats.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
 
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
import {Component, OnInit} from "@angular/core";
import template from "./chats.component.html"
import {Observable} from "rxjs";
import {Chat} from "../../../../both/models/chat.model";
import * as moment from "moment";
import style from "./chats.component.scss";
import {Chats} from "../../../../both/collections/chats.collection";
import {Message} from "../../../../both/models/message.model";
import {Messages} from "../../../../both/collections/messages.collection";
 
@Component({
  selector: "chats",
...some lines skipped...
    style
  ]
})
export class ChatsComponent implements OnInit {
  chats: Observable<Chat[]>;
 
  constructor() {
 
  }
 
  ngOnInit() {
    this.chats = Chats
      .find({})
      .mergeMap<Chat[]>(chats =>
        Observable.combineLatest(
          ...chats.map(chat =>
 
            Messages.find({ chatId: chat._id }, { sort: { createdAt: -1 }, limit: 1 })
              .startWith(null)
              .map(messages => {
                if (messages) chat.lastMessage = messages[0];
                return chat;
              })
 
          )
        )
      ).zone();
  }
}

As you can see, we moved chats property initialization to ngOnInit, one of the Angular's lifehooks. It's being called when Component is initalized.

Here comes a quick lesson of RxJS.

Since Chats.find() returns an Observable we can take advantage of that and bundle it with Messages.find() to look for last messages of each chat. This way everything will work as a one body, one Observable.

So what's really going on there?

Find chats

First thing is to get all the chats by using Chats.find({}).

The result of it will be an array of Chat objects.

Let's use map operator to make a space for adding the last messages.

Chats.find({})
    .map(chats => {
        const chatsWithMessages = chats.map(chat => {
            chat.lastMessage = undefined;
            return chat;
        });

        return chatsWithMessages;
    })

Look for the last message

For each chat we need to find the last message. We can achieve this by calling Messages.find with proper selector and options.

Let's go through each element of the chats property to call Messages.find.

const chatsWithMessages = chats.map(chat => Chats.find(/* selector, options*/));

That returns an array of Observables.

We need to create a selector. We have to look for a message that is a part of required chat:

{
    chatId: chat._id
}

Okay, but we need only one, last message. Let's sort them by createdAt:

{
    sort: {
        createdAt: -1
    }
}

This way we get them sorted from newest to oldest.

We look for just one, so selector will look like this:

{
    sort: {
        createdAt: -1
    },
    limit: 1
}

Now we can add the last message to the chat.

Messages.find(/*...*/)
    .map(messages => {
        if (messages) chat.lastMessage = messages[0];
        return chat;
    })

Great! But what if there are no messages? Wouldn't it emit a value at all?

RxJS contains a operator called startWith. It allows to emit some value before Messages.find beings to emit messages. This way we avoid the waiting for non existing message.

The result:

const chatsWithMessages = chats.map(chat => {
    return Messages.find(/*...*/)
        .startWith(null)
        .map(messages => {
            if (messages) chat.lastMessage = messages[0];
            return chat;
        })
})

Combine those two

Last thing to do is to handle the array of Observables we created (chatsWithMessages).

Yet again, RxJS comes with a rescue. We will use combineLatest which takes few Observables and combines them into one Observable.

It works like this:

const source1 = /* an Observable */
const source2 = /* an Observable */

Observable.combineLatest(source1, source2);

This combination returns an array of both results (result). So the first item of that array will come from source1 (result[0]), second from source2 (result[1]).

Let's see how it applies to our example:

Observable.combineLatest(...chatsWithMessages);

We used ...array because Observable.combineLatest expects arguments, not a single one that with an array of Observables.

To merge that observable into Chats.find({}) we need to use mergeMap operator instead of map:

Chats.find({})
    .mergeMap(chats => Observable.combineLatest(...chatsWithMessages));

In Whatsapp we used chats.map(/*...*/) directly instead of creating another variables like we did with chatsWithMessages.

With all this, we have now Chats with their last messages available in the UI view.

{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/chats-page" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/messages-page"}}}