The Ultimate Guide For Building RTA Part 3

The Ultimate Guide For Building RTA Part 3

  • 2016-11-11
  • 1007

Previously on The Ultimate Guide For Building RTA Part 2 I explained how to make a more advanced use of the LoopBack SDK Builder by authenticating users and creating rooms within our {N} Chat Application.

Today we are going continue with this series by creating the room section in which we will allow users to send/receive messages and to add other users in specific rooms.

Important to Notice

During the previous weeks the community and I have been putting a lot of effort on the LoopBack SDK Builder which by now is much more mature, although it still is on beta; minor issues may appear, please add a github issue if you find something.

If you started to implement my previous post during the week I published it; I need to warn you that it has been updated to implement the latest version of the [email protected] please consider in updating your project since major changes has been released.

Project Description

For this part of the series; we are going to create a room section for users to be able to send and to receive messages from other accounts and devices, also to add other users to conversations and in order to start giving a better look and feel we are going to make some tweaks in what we already have implemented.

Although by the end of this tutorial we will have a pretty good looking app I still don’t think we can finish the project example in this post, but now I’m pretty sure I will be adding just 1 more post for this series; since I want to complete this by adding a couple of animations a way to logout and some polish, so we finish with a pretty good shaped application.

Sections to create in this post:

  • Chat Room
  • Add Members

Pending section/feature

  • Log out
  • Animations
  • Polish

Requirements

GitHub Example Code Repository

Native Chat Application Example

Update Components

During the time I have been writing this series; I have also been fixing bugs and making improvements for both the LoopBack SDK Builder and for LoopBack Component PubSub.

For this reason I recommend you to keep updating these components, I have been trying to make these changes as transparent as I can, but… There are a couple of configurations to modify if and only if you have been following this series from the very first weeks.

$ cd /native-chat-api
$ npm install --save-dev @mean-expert/loopback-sdk-builder
$ npm install --save [email protected]

Update API package.json

If you are already creating your sdk using the lb-sdk command, you can go to the next section.

For those who are following this series from day 1; you may be using the deprecated lb-ng command to generate the sdk, therefore you will need to update the package json to start using the lb-sdk command instead.

Previously you would select -l to select nativescript2 or angular2 as these defined as 2 different libraries, but since the base code between these 2 are shared in at least 90%, I merged these into 1 generator and added the ability to define drivers instead.

You would need to select the driver by using the -d [ng4web | nativescript2] flag.

For more details, please check the documentation in the following link

After you update the builder, just re-generate it by running the $ npm run build:sdk command.

Update API Server Configuration

Enable http context by changing the following property

native-chat-api/server/config.json

{
    "context": {
      "enableHttpContext": true
    }
}

That is all!!! It was not that bad, most of the updates were at low level and no apis were affected or modified, so your application should keep running without any other modification.

Update App Structure

In part 2 we improved our application structure by using the Angular 2 Style Guide. This time we are going to continue with that spirit and since we are going to create a room component which at the end; is part of the +rooms component we already created, we will need to update our component structure as follows:

├─ native-chat-app
| ├─ app
| | ├─ +rooms
| | | ├─ room
| | | | ├─ room.component.css
| | | | ├─ room.component.html
| | | | ├─ room.component.ts
| | | ├─ room-list
| | | | ├─ room-list.component.html
| | | | ├─ room-list.component.ts
| | | ├─ index.ts

Migrate RoomsComponent

We already built a section to create and list rooms, lets just organize our application and move what we had in rooms.component.ts to room-list/room-list.component.ts.

After you moved all of the code, you will need to make some adjustments and I will explain these below:

import { Component } from [email protected]/core';
import { Router } from [email protected]/router-deprecated';
import { Subject } from 'rxjs/Subject';
import { ObservableArray } from 'data/observable-array';
import {
  LoopBackConfig,
  Room,
  AccountApi,
  RoomInterface,
  BASE_URL,
  API_VERSION
} from '../../shared';

@Component({
  selector: 'room-list',
  templateUrl: '+rooms/room-list/room-list.component.html',
  providers: []
})

export class RoomsComponent {

  private rooms: ObservableArray<Room>;
  private room: RoomInterface = new Room();
  private subscriptions = new Array();

  constructor(private _account: AccountApi,  private _router: Router) {
      LoopBackConfig.setBaseURL(BASE_URL);
      LoopBackConfig.setApiVersion(API_VERSION);
  }

  ngAfterViewInit() {
      this.getRooms();
  }

  onOnDestroy() {
    this.subscriptions.forEach(subscription => subscription.unsubscribe());
  }

  private addRoom(): void {
    this.subscriptions.push(
      this._account.createRooms(this._account.getCurrentId(), this.room)
                   .subscribe(()  => this.room = new Room(), err => alert(err))
    );
  }

  private getRooms(): void {
    this.subscriptions.push(
      this._account.getRooms(this._account.getCurrentId()).subscribe((rooms: Array<Room>) => {
          this.rooms = new ObservableArray<Room>(rooms);
      })
    ); 
    this.subscriptions.push(
      this._account.onCreateRooms(this._account.getCurrentId())
                   .subscribe((room: Room) => this.onRoom(room))
    );
    this.subscriptions.push(
      this._account.onLinkRooms(this._account.getCurrentId())
                   .subscribe((room: Room) => this.onRoom(room))
    );
  }

  private onRoom(room: Room) { this.rooms.push(room); }

  private join(room: RoomInterface) {
    this._router.navigate([ 'RoomComponent', room ]); 
  }
}

While creating this example I was able to quickly figure out that you can easily create memory leaks with your application.

First of all and if you already haven’t noticed, I’m now importing a core library from NativeScript named ObservableArray and changed the type of rooms from Array<Room> to ObservableArray<Room>

import { ObservableArray } from 'data/observable-array';

....

private rooms: ObservableArray<Room>;

This will allow us to take advantage from the ListView component; by using the ObservableArray the UI will be refreshed whenever the array updates, otherwise you will run into issues updating the rendered data.

You could simply use an *ngFor but believe me, that would be a -nasty- memory leak. This because all of the elements -the ones you see and the ones you don’t see- will be loaded in memory.

The ListView component will load in memory only those elements you are actually viewing in the screen, and will load/unload elements on demand while users scroll the window.

Unsubscribe to Observables

Another potential risk is not unsubscribing from our observables and for this reason we are now implementing the ngOnDestroy lifecycle hook, so we can later unsubscribe when we move off the component.

If you don’t implement this, you will easily find out that your application will slow down really quick and eventually die just by changing the sections several times.

Account.onLinkRooms

I’m not sure if you already noticed but a new implementation is the this._account.onLinkRooms.

So… If you remember; the onCreateRooms event will fire every time you create a room in every device you are connected.

But… What happens when other users create their own rooms and then adds you as member?

That is when the onlinkRooms event handler comes in action, because you need to listen when someone else adds you to another room.

Create Room Component

Now we need to work in the room component that will allow users to send/receive messages but also to add new members to a conversation.

import { Component } from [email protected]/core';
import { Router, RouteParams } from [email protected]/router-deprecated';
import { ObservableArray } from 'data/observable-array';
import {
  LoopBackConfig,
  Room,
  RoomApi,
  RoomInterface,
  Account,
  AccountApi,
  AccountInterface,
  Message,
  MessageInterface,
  BASE_URL,
  API_VERSION
} from '../../shared';

@Component({
  selector   : 'room',
  templateUrl: '+rooms/room/room.component.html',
  styleUrls  : ['+rooms/room/room.component.css'],
  providers  : []
})

export class RoomComponent {

  private loggedId: number | string;
  private room    : RoomInterface = new Room();
  private member  : AccountInterface = new Account();
  private message : MessageInterface = new Message();
  private messages: ObservableArray<Message>;
  private members : ObservableArray<Account>;
  private subscriptions = new Array();

  constructor(
    private _account: AccountApi,
    private _room   : RoomApi,
    private _params : RouteParams,
    private _router : Router
  ) {
    LoopBackConfig.setBaseURL(BASE_URL);
    LoopBackConfig.setApiVersion(API_VERSION);
    this.loggedId = this._account.getCurrentId();
  }

  ngAfterViewInit() {
    this.getRoom(this._params.get('id'));
  }

  onOnDestroy() {
    this.subscriptions.forEach(subscription => subscription.unsubscribe());
  }

  getRoom(id: any): void {
    this.subscriptions.push(
      this._room.findById(id, {
        include: {
          relation: 'accounts',
          scope: { order: 'id DESC' }
        }
      }).subscribe((room: Room) => {
        this.room = room;
        this.getMessages();
        this.subscribe();
        this.members  = new ObservableArray<Account>(this.room['accounts']);
      }, err => alert(err))
    );
  }

  subscribe(): void {
    this.subscriptions.push(
      this._room.onCreateMessages(this.room.id)
                .subscribe((message: Message) => this.messages.unshift(message))
    );

    this.subscriptions.push(
      this._room.onLinkAccounts(this.room.id)
                .subscribe((account: Account) => this.members.unshift(account))
    );
  }

  sendMessage(): void {
    this.message.accountId = this._account.getCurrentId();
    let subscription = this._room.createMessages(this.room.id, this.message)
                .subscribe(
                  () => this.message = new Message(),
                  err => alert(err),
                  () => { subscription.unsubscribe() }
                );
  }

  getMessages(): void {
    this.subscriptions.push(
      this._room.getMessages(this.room.id, {
        order: 'id DESC',
        include: 'account'
      }).subscribe((messages: Array<Message>) => {
        this.messages = new ObservableArray<Message>(messages);
      })
    );
  }

  addMember(): void {
    this.subscriptions.push(
      this._account.findOne({ where: this.member }).subscribe(
        (member: AccountInterface) => this.linkMember(member), 
        err => alert('Member not found')
      )
    );
  }

  linkMember(member: AccountInterface): void {
    this.subscriptions.push(
      this._room.linkAccounts(this.room.id, member.id)
                .subscribe(
                  res => (this.member = new Account()), 
                  err => alert(err)
                )
    );
  }
}

Ok, this is a long one… But since we are going to use a TabView for this component we will need to handle both logics within the RoomComponent.

Sure… We could create 2 more components and implement these within the RoomComponent so we would keep the logic separated.

But for demo reasons I needed to keep in one component; otherwise this would be a much more overwhelming post… If you feel like you want to go beyond; you may think in the possibility of creating a MessagesComponent and a MembersComponent.

Properties

As I described in Part 2 we get from the generated sdk a set of models, interfaces and api services that allows you to save huge amounts of time, so you can start fully typing your components right away and take advantage of what typescript offers.

I have to be honest, when I migrated from plain JavaScript I thought, Oh Jesus!!! Now I need spend time all this time to write a bunch of declaration files.

I’m pretty sure most of you had that feeling… But Hey!!! Why write these if can be automatically generated!!!

private room    : RoomInterface = new Room();
private member  : AccountInterface = new Account();
private message : MessageInterface = new Message();
private messages: ObservableArray<Message>;
private members : ObservableArray<Account>;

And believe me… If you stop using plain JavaScript you will avoid a huge amount of bugs while developing your applications and not during execution.

Also use the dependency injection pattern as possible.

Something important I found during this series is that we may want to wait angular to finish loading but also to finish rendering in order to start doing our calls, this will allow a more fluid application.

ngAfterViewInit() {
  this.getRoom(this._params.get('id'));
}

getRoom(id: any): void {
  this.subscriptions.push(
    this._room.findById(id, {
      include: {
        relation: 'accounts',
        scope: { order: 'id DESC' }
      }
    }).subscribe((room: Room) => {
      this.room = room;
      this.getMessages();
      this.subscribe();
      this.members  = new ObservableArray<Account>(this.room['accounts']);
    }, err => alert(err))
  );
}

So… After the view has been loaded we can now load our persisted room by the given id and we absolutely want to get all the accounts that are already related with this room.

That can be achieved by using the include filter.

Once we get the room and its members we add references for these so we can bind them into the UI.

After we get everything that is already persisted its a good moment to subscribe for new messages and new room members.

subscribe(): void {
  this.subscriptions.push(
    this._room.onCreateMessages(this.room.id)
              .subscribe((message: Message) => this.messages.unshift(message))
  );

  this.subscriptions.push(
    this._room.onLinkAccounts(this.room.id)
              .subscribe((account: Account) => this.members.unshift(account))
  );
}

Please note that I’m saving reference of the Disposal instance returned from every subscription, so we can later unsubscribe when leaving the room:

onOnDestroy() {
  this.subscriptions.forEach(subscription => subscription.unsubscribe());
}

Other than that, the rest of the methods are self explanatory except for the linkMember one; that may be obvious at high level, but not at the low level:

addMember(): void {
  this.subscriptions.push(
    this._account.findOne({ where: this.member }).subscribe(
      (member: AccountInterface) => this.linkMember(member), 
      err => alert('Member not found')
    )
  );
}

linkMember(member: AccountInterface): void {
  this.subscriptions.push(
    this._room.linkAccounts(this.room.id, member.id)
              .subscribe(
                res => (this.member = new Account()), 
                err => alert(err)
              )
  );

After we verify if the given user exists, we link the user as member of the room; obviously it creates a relationship between the given user account and the current room.

But what happens in the background?

Remember that we subscribed to an event method named Account.onLinkRooms within our RoomsComponent?

Well, when you link a room with a user account using the Room.linkAccounts 3 things happen in the background:

  • The API creates a relationship between Room and Account.
  • The API notifies the account on Account.onLinkRooms(id: userId) event.
  • The API notifies the room on Room.onLinkAccounts(id: roomId) event.

So… Why would we want the API to do all of these 3 processes?

Just remember; this is a real-time application… If you add a third person to your room, you -and the rest of the members- will want to see the new account to appear in the member list, but also… If you are the third person that just has been added to a room, you will want the room to be displayed in the list of rooms.

Fortunately the loopback-component-pubsub and the loopback-sdk-builder will notify everyone who is subscribed to these events.

this.subscriptions.push(
  this._room.onLinkAccounts(this.room.id)
            .subscribe((account: Account) => this.members.unshift(account))
);

and

this.subscriptions.push(
  this._account.onLinkRooms(this._account.getCurrentId())
               .subscribe((room: Room) => this.onRoom(room))
);

Include Messages Account

Now that you know that many things happen in the background when interacting with the loopback-component-pubsub you need to know that not all of the times the component will be able -by itself- to publish everything you need.

But… Why? Simply because the server doesn’t know specifically what do you need, it assumes you need something because you subscribed to a loopback-sdk-builder event handler.

What happens when you want to include related information?

Ok… I know that may sounds abstract, and it is; but if you analyze the way we got the room by the giving id, we included the related accounts by using the include filter.

That does work when you are actually calling the server like:

this._room.findById(id, {
  include: {
    relation: 'accounts',
    scope: { order: 'id DESC' }
  }
});

When someone sends a message within our chat room, the server will publish that message to all clients subscribed to Room.onCreateMessages(id: roomId) but, that is all…

The server will only publish the message and not the related data.

But… What does that mean? Well, if you think about it for a moment; you will be able to display a list of messages but you won’t be able to display who actually wrote these messages.

So… In order to get the complete information, we will need to modify our mixing configuration as follows:

native-chat-api/common/models/room.json

"mixins": {
  "PubSub": {
    "filters": {
      "__create__messages": {
        "include": "account"
      }
    }
  }
}

Wonderful!!! We just have configured the server to include the account who created a message whenever it happens.

I know this is complex and its low level, but most of the times you won’t need to modify this configurations, just remember that if you run into a situation when the server is not publishing everything you need, then configure the model mixing to do so.

Create Room View

Now that we covered the logic of our room component we need to create the view; for this I want to use a tab view as follows:

<ActionBar *ngIf="room" title="{{ room.name }}">
  <NavigationButton text="Go Back" android.systemIcon="ic_menu_back" (tap)="_router.navigate(['RoomsComponent'])"></NavigationButton>
</ActionBar>
<TabView selectedIndex="1" selectedColor="#FF0000">
  <StackLayout *tabItem="{ title: 'Messages' }">
    <GridLayout rows="auto, *" class="small-spacing">
      <GridLayout row="0" columns="*, auto">
        <TextField
          [(ngModel)]="message.text"
          hint="Enter a message..."
          returnKeyType="done"
          (returnPress)="sendMessage()"
          col="0"
          class="text-box">
        </TextField>
      </GridLayout>
      <ScrollView row="1">
        <ListView [items]="messages" row="1">
          <template let-item="item">
            <StackLayout [className]="loggedId == item.account.id ? 'message-bubble current' : 'message-bubble remote'" >
              <Label text="{{ item.account.email + ' says:' }}" class="message-user"></Label>
              <Label [text]="item.text"></Label>
            </StackLayout>
          </template>
        </ListView>
     </ScrollView>
    </GridLayout>
  </StackLayout>
  <StackLayout *tabItem="{ title: 'Members' }">
    <GridLayout rows="auto, *" class="small-spacing">
      <GridLayout row="0" columns="*">
        <TextField
          [(ngModel)]="member.email"
          hint="Add member by email"
          col="0"
          keyboardType= "email"
          returnKeyType="done"
          (returnPress)="addMember()"
          class="text-box">
        </TextField>
      </GridLayout>
      <ListView [items]="members" row="1">
        <template let-item="item">
          <StackLayout>
            <Label [text]="item.email" class="medium-spacing"></Label>
          </StackLayout>
        </template>
      </ListView>
    </GridLayout>
  </StackLayout>
</TabView>

Style Room Component

.message-bubble {
  border-radius: 10;
  padding: 10;
}

.message-user {
  font-weight: bold;
}

.current {
  background-color: #FCF6DB;
}

.remote{
  background-color: #E6F3F6;
}

Complete Barrel

Since we moved everything related to rooms within our +room directory, which contains more than 1 component, we need to create a barrel and export these components within the +rooms/index.ts file:

export * from './room/room.component';
export * from './room-list/room-list.component';

Update App Component

Thanks to the fact that we modified our +rooms component but also we created another component, now we need to update our app component as follows:

import { Component } from "@angular/core";
import { Account, AccountApi } from './shared';
import { Router, RouteConfig } from "@angular/router-deprecated";
import { NS_ROUTER_DIRECTIVES, NS_ROUTER_PROVIDERS} from "nativescript-angular/router";
import { SignComponent } from "./+sign";
import { RoomsComponent, RoomComponent } from "./+rooms";

@Component({
    selector: "my-app",
    directives: [ NS_ROUTER_DIRECTIVES ],
    providers: [ NS_ROUTER_PROVIDERS ],
    template: "<page-router-outlet></page-router-outlet>"
})

@RouteConfig([
  { path: "/sign", component: SignComponent, name: "SignComponent", useAsDefault: true },
  { path: "/rooms", component: RoomsComponent, name: "RoomsComponent"  },
  { path: "/rooms/:id", component: RoomComponent, name: "RoomComponent"  }
])

export class AppComponent {
    constructor(private _router: Router, private _account: AccountApi) {
        this._router.subscribe(() => {
            if (!this._account.isAuthenticated())
            this._router.navigate(['SignComponent'])
        })
    }
}

For this component we just modified the way we are importing the RoomsComponent and created a new route in order to be able to navigate to the room section.

Styling App Component

For this part of the tutorial we want a more appealing application so we just need to update our main app styles as follow:

@import url("~/platform.css");

Page {
  background-color: white;
  font-size: 15;
}

ActionBar {
  padding-top: 20;
  background-color: #000B31;
  color: white;
}

TextField {
  padding: 15;
  font-size: 16;
}

Button {
  padding: 20 auto;
}

.small-spacing {
  margin: 10;
}

.medium-spacing {
  margin: 15;
}

.text-box {
  font-size: 16;
  background-color: #f6f6f6;
  border-radius: 10;
  margin-bottom: 10;
}

.primary-btn {
  background-color: #E43850;
  color: white;
}

.text-white {
  color: white;
}

Test Application

I know this journey was a little overwhelming, it had to be in some point; but I hope you have been enjoying this series and I really think the result application is starting to be much more interesting.

So, if everything went well, you will need just to start your API and build your application.

Terminal 1:

$ cd native-chat-api
$ node .
RTC server listening at ws://0.0.0.0:3000/
Web server listening at: http://0.0.0.0:3000
Browse your REST API at http://0.0.0.0:3000/explorer

Terminal 2:

$ cd native-chat-app
$ tns run android

Now, when the application loads you can login/register, create new rooms, send/receive messages and add members of a conversation.

Application Tour

What is next?

In my next blog posts I will continue with this tutorial by adding some animations and gestures for our application I will add a logout button and we may do some tweaks to our final application.

Suggest

Mastering Node.js: Nodejs Development from Scratch

Node.js Tutorial: From Zero to Hero with Nodejs

Create a new Angular 2 application with Angular CLI

Angular 2 Tutorial : GuildRunner sandbox Angular 2 application

Angular 2 Tutorial - The Architecture of an Angular 2 Application