Nexmo Stitch is a conversation-centric product that enables communications across multiple channels including in-app messaging and in-app voice. In this tutorial we’ll enable chat in an Angular web application using the JavaScript SDK and the Stitch API so that users can communicate in our application.

This is what we’re trying to build:

end game

Before you begin

Before we begin you’ll need a few things:

  • A basic understanding of Angular

  • Node.js installed on your machine

  • Create a free Nexmo account - signup

  • Install the Nexmo CLI:

    $ npm install -g nexmo-cli@beta
    

    Setup the CLI to use your Nexmo API Key and API Secret. You can get these from the setting page in the Nexmo Dashboard.

    $ nexmo setup api_key api_secret
    
  • The middleware code from Github

Getting the middleware code from Github

First, we’re going to clone the middleware source code and install the dependencies for it. There is a Node.js application using Express that provides an level of abstraction between the Nexmo Stitch API and the Angular code we’re going to write.

{% raw %}

$ git clone git@github.com:Nexmo/stitch-demo.git
$ cd stitch-demo/
$ npm install

{% endraw %}

Running the middleware code from Github

Before we can run the code, we’ll need to create a Nexmo RTC application within the Nexmo platform to use within this code.

{% raw %}

$ nexmo app:create "My Conversation App" https://example.com/answer https://example.com/event --type=rtc --keyfile=private.key

{% endraw %}

The output of the above command will be something like this:

{% raw %}

Application created: aaaaaaaa-bbbb-cccc-dddd-0123456789ab
No existing config found. Writing to new file.
Credentials written to /path/to/your/local/folder/.nexmo-app
Private Key saved to: private.key

{% endraw %}

The first item is the Application ID which you should take a note of. We’ll refer to this as APP_ID later. The last value is a private key location. The private key is used to generate JWTs that are used to authenticate your interactions with Nexmo.

Now we’ll need to make a copy of example.env and call that .env, and update the values within with your Nexmo API_KEY and API_SECRET. We’ll also have to add the APP_ID we just generated and the path to your private key. After we’ve updated the values, we can run the code in debug mode with

{% raw %}

$ DEBUG=stitch-demo:* node ./bin/www

{% endraw %}

Create Users & Conversations

The app should be running on localhost:3000. Now that the app is running, we’re going to go ahead and create a couple of users and a conversation, and then we’ll add the users we created to the conversation.

We’ll create a couple of users by running this command twice, once with the username alice and then jamie

{% raw %}

curl --request POST \
  --url http://localhost:3000/api/users \
  --header 'content-type: application/json' \
  --data '{
	"username": "jamie",
	"admin": true
}'

{% endraw %}

The ouput should look similar to

{% raw %}

{"user":{"id":"USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab","href":"http://conversation.local/v1/users/USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab"},"user_jwt":"USER_JWT"}

{% endraw %}

We’ll make a note of the user id and refer to it late on as USER_ID. Now let’s create a conversation via the demo API

{% raw %}

curl --request POST \
  --url http://localhost:3000/api/conversations \
  --header 'content-type: application/json' \
  --data '{"displayName": "My Chat"}'

{% endraw %}

The ouput should look similar to

{% raw %}

{"id":"CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab","href":"http://conversation.local/v1/conversations/CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab"}

{% endraw %}

We’ll make a note of the conversation id and refer to it late on as CONVERSATION_ID. Now let’s join the users to the conversation. We’re going to run the following command twice and remember to replace the CONVERSATION_ID and USER_ID with IDs from the two previous steps every time you run this command.

{% raw %}

curl --request PUT \
  --url http://localhost:3000/api/conversations \
  --header 'content-type: application/json' \
  --data '{
	"conversationId": "CON-64aa2077-2728-4ab5-ae1b-531b32043d58",
	"userId": "USR-5fc698c7-eccf-4807-9075-da4b0cc3214d",
	"action": "join"
}'

{% endraw %}

Generate Angular App

Now that we have our middleware up and running, it’s time we created the Angular application. We are going to use the Angular CLI to generate the app so if you don’t have it installed, you’ll need to install that first:

{% raw %}

$ npm install -g @angular/cli

{% endraw %}

And then we’ll use it to generate a new application with routing. It may take a while for it to generate all the files and install the dependencies

{% raw %}

$ ng new nexmo-stitch-angular --routing

{% endraw %}

Add Material Design

After the previous command finished, we’ll add Angular Material and it’s dependencies to the project:

{% raw %}

$ npm install --save @angular/material @angular/cdk @angular/animations

{% endraw %}

We’ll also have to import the NgModule for each component we want to use in our application. In order to do that, we need to open src/app/app.module.ts in our editor and import the modules at the top

{% raw %}

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import {
  MatAutocompleteModule,
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatCheckboxModule,
  MatChipsModule,
  MatDatepickerModule,
  MatDialogModule,
  MatExpansionModule,
  MatGridListModule,
  MatIconModule,
  MatInputModule,
  MatListModule,
  MatMenuModule,
  MatNativeDateModule,
  MatPaginatorModule,
  MatProgressBarModule,
  MatProgressSpinnerModule,
  MatRadioModule,
  MatRippleModule,
  MatSelectModule,
  MatSidenavModule,
  MatSliderModule,
  MatSlideToggleModule,
  MatSnackBarModule,
  MatSortModule,
  MatTableModule,
  MatTabsModule,
  MatToolbarModule,
  MatTooltipModule,
  MatStepperModule
} from '@angular/material';

{% endraw %}

We’ll also need to update the @NgModules imports declaration to add the modules from above:

{% raw %}

@NgModule({
  ...
  imports: [
    imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    FormsModule,
    HttpClientModule,
    MatTabsModule,
    MatCardModule,
    MatGridListModule,
    MatButtonModule,
    MatInputModule,
    MatListModule,
    MatIconModule,
    MatSidenavModule,
    MatProgressSpinnerModule,
    MatTooltipModule,
    MatDialogModule
  ],
  ],
  ...
})

{% endraw %}

We’ll also want to add a theme to Material, so add this line to your style.css

{% raw %}

@import "~@angular/material/prebuilt-themes/indigo-pink.css";

{% endraw %}

If we want to use the official Material Design Icons, we’ll need to load the icon font in our index.html

{% raw %}

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

{% endraw %}

Polifill Nexmo Stitch SDK

Now that we’ve got our Angular application generated and all set-up with Material, we’ll install the Stitch JavaScript SDK and add it to the bottom of polyfills.ts

{% raw %}

$ npm install --save nexmo-conversation

{% endraw %}

{% raw %}

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
import 'nexmo-conversation';

{% endraw %}

Messaging Service

We’ll need to create an Angular Service to handle the data from our middleware. Let’s generate it using the Angular CLI

{% raw %}

$ ng g service messaging

{% endraw %}

Two new files were generated in our src/app folder, messaging.service.spec.ts and messaging.service.spec.ts. We’re going to update the messaging.service.spec.ts in order to add the ConversationClient from the Stitch SDK, instantiate a client and handle getting an user JSON Web Token from the middleware. We’re going to replace the boilerplate code with:

{% raw %}

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

declare var ConversationClient: any;
const GATEWAY_URL = "http://localhost:3000/api/";

@Injectable()
export class MessagingService {


  constructor(private http: HttpClient) {

  }

  initialize() {
    this.client = new ConversationClient(
      {
        debug: false
      }
    )
  }

  public client: any
  public app: any


  public getUserJwt(username: string): Promise<any> {
    return this.http.get(GATEWAY_URL + "jwt/" + username + "?admin=true").toPromise().then((response: any) => response.user_jwt)
  }
}

{% endraw %}

We need to update app.module.ts in order to import the MessagingService and register it as a provider

{% raw %}

import { MessagingService } from './messaging.service';

...
    providers: [MessagingService],
...

{% endraw %}

Login Component

Let’s start building some UI to our app. We’ll start with a LoginComponent, and we’ll generate that with the Angular CLI:

{% raw %}

$ ng g component login

{% endraw %}

That will generate a login folder inside the app folder, and 4 files, for the HTML, CSS and TypeScript code, as well as tests. Let’s replace the code in login.component.html with a UI for logging in. I chose a <mat-grid-list> with a <mat-card> inside of it, and a form with a login button that calls onLogin() when it’s submitted. The code looks like this:

{% raw %}

<mat-grid-list cols="4" rowHeight="100px">
  <mat-grid-tile colspan="1" rowspan="5"></mat-grid-tile>
  <mat-grid-tile colspan="2" rowspan="1"></mat-grid-tile>
  <mat-grid-tile colspan="1" rowspan="5"></mat-grid-tile>
  <mat-grid-tile colspan="2" rowspan="3">
    <mat-card class="mat-typography login">
      <h1>Login</h1>
      <form (ngSubmit)="onLogin()">
        <mat-form-field class="full-width">
          <input matInput placeholder="Username" name="username" [(ngModel)]="username">
        </mat-form-field>
        <br>
        <button type="submit" mat-raised-button color="primary" (click)="showSpinner = !showSpinner">Login
          <mat-spinner color="accent" mode="indeterminate" *ngIf="showSpinner"></mat-spinner>
        </button>
      </form>
    </mat-card>
  </mat-grid-tile>
</mat-grid-list>

{% endraw %}

Let’s add some CSS to it for the Material spinner, in the login.component.css file

{% raw %}

.login {
    text-align: center;
}

.mat-spinner {
    width: 20px !important;
    height: 20px !important;
    display: inline-block;
    margin-left: 10px;
}

/deep/ .mat-spinner svg{
    width: 20px !important;
    height: 20px !important;
}

{% endraw %}

We also need to update login.component.ts in order to add the MessagingService to it, and implement the onLogin() method. The method is going to take the username, make a request via the messaging service to the middleware we are running in order to get a user JWT and then use that to authenticate via the login method of the Stitch JavaScript SDK. The code looks like this:

{% raw %}

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

import { MessagingService } from '../messaging.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  username: string = ""

  constructor(private ms: MessagingService, private router: Router) { }

  ngOnInit() {
    this.ms.initialize()
  }

  onLogin() {
    this.ms.getUserJwt(this.username).then(this.authenticate.bind(this))
  }

  authenticate(userJwt: string) {
    this.ms.client.login(userJwt).then(app => {
      this.ms.app = app
      this.router.navigate(['/conversation']);
    })
  }
}

{% endraw %}

You’ve noticed I’ve imported the Router from Angular, injected it into our constructor and I’m using it at the end of the authentication flow to navigate to the next page, /conversation

Conversation Component

We haven’t actually created that component yet, so let’s go ahead and use the Angular CLI to create the conversation component

{% raw %}

$ ng g component conversation

{% endraw %}

We’re going to update the conversation.component.html file to use a material sidenav component, which lists the user conversations on the left and the conversation members on the right, leaving the middle for our main chat. We’ll add a header to the chat section to list the conversation name and member count, and add an input section at the bottom. We’ll leave the middle section for the actual conversation history to be displayed. We’ll build an empty shell for now and add to it later as we develop the ConversationComponent. The HMTL should look like this:

{% raw %}

<mat-sidenav-container class="container">
  <mat-sidenav mode="side" opened>
    <mat-card>
      <mat-tab-group>
        <mat-tab>
          <ng-template mat-tab-label>
            <mat-icon matListIcon>forum</mat-icon>
          </ng-template>
          <mat-list class="conversations">
              ...
          </mat-list>
        </mat-tab>
      </mat-tab-group>
    </mat-card>
  </mat-sidenav>
  <mat-sidenav position="end" mode="side" opened *ngIf="selectedConversation">
    <mat-card>
      <mat-list class="members">
          ...
      </mat-list>
    </mat-card>
  </mat-sidenav>
  <section class="empty-conversation" *ngIf="!selectedConversation">
    <h1 class="mat-display-1">Select a conversation from the left to start chatting</h1>
  </section>
  <section *ngIf="selectedConversation">
    <div class="mat-typography conversation-header">
        ...
    </div>
    <mat-divider></mat-divider>
    <mat-list dense class="conversation-history mat-typography">
      ...
    </mat-list>
    <div class="conversation-input">
      <mat-divider></mat-divider>
      <mat-form-field class="full-width">
        <input matInput placeholder="Start chatting..." name="text" [(ngModel)]="text">
        <mat-icon matSuffix (click)="">send</mat-icon>
      </mat-form-field>
    </div>
  </section>
</mat-sidenav-container>

{% endraw %}

We’re going to update the conversation.component.ts file with the necessary boilerplate for methonds we’re going to use later on.

{% raw %}

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';

import { MessagingService } from '../messaging.service';

@Component({
  selector: 'app-conversation',
  templateUrl: './conversation.component.html',
  styleUrls: ['./conversation.component.css']
})
export class ConversationComponent implements OnInit {

  constructor(private ms: MessagingService, private router: Router) { }

  buildConversationsArray(conversations) {
  }

  ngOnInit() {
  }

  selectConversation(conversationId: string) {
  }

  sendText(text: string) {
  }

  conversations: any
  selectedConversation: any
  text: string
  events: Array<any> = []
}

{% endraw %}

Let’s start by implementing ngOnInit() so that it checks if we have app data before trying to getConversations using the Stitch SDK. If there is no app data, then we’ll get redirected to the login screen.

{% raw %}

  ngOnInit() {
    if (!this.ms.app) {
      this.router.navigate(['/']);
    } else {
      this.ms.app.getConversations().then(conversations => {
        this.conversations = this.buildConversationsArray(conversations)
      })
    }
  }

{% endraw %}

We need to implement a helper method for building a conversations array out of the conversations dictionary the Stitch JavaScript SDK provides, so we can use it with *ngFor in the UI later on

{% raw %}

  buildConversationsArray(conversations) {
    let array = [];

    for (let conversation in conversations) {
      array.push(conversations[conversation]);
    }

    return array
  }

{% endraw %}

Let’s add a method for selecting a conversation from the list. We’ll need to take the ID from the view and pass it on to the controller, and then use the Stitch SDL to get data about the conversation. We’ll store this in a class property, so it’s available to the view later on. We’re also using Observable to creat an array from the conversation.events Map, so we can re-create chat history when the user comes back to the app. We’ll also add an event listener using the SDK to listen for text events and add those to the events history as well.

{% raw %}

selectConversation(conversationId: string) {
    this.ms.app.getConversation(conversationId).then(conversation => {
      this.selectedConversation = conversation

      Observable.from(conversation.events.values()).subscribe(
        event => {
          this.events.push(event)
        }
      )

      this.selectedConversation.on("text", (sender, message) => {
        this.events.push(message)
      })

      console.log("Selected Conversation", this.selectedConversation)
    }
    )
  }

{% endraw %}

Last but not least, let’s add a method that takes the input from the view and sends it to the Stitch API via the SDK.

{% raw %}

  sendText(text: string) {
    this.selectedConversation.sendText(text).then(() => this.text = "")
  }

{% endraw %}

Now that we’ve implemented all the methods we need, we can go back and start to flesh out the view some more, to use the data models we created in the controller. First, let’s update the conversations section in conversation.component.html

{% raw %}

...
          <mat-list class="conversations">
            <mat-list-item *ngFor="let conversation of conversations" (click)="selectConversation(conversation.id)">
              <mat-icon matListIcon>forum</mat-icon>
              <p>{{conversation.display_name}}</p>
            </mat-list-item>
          </mat-list>
...

{% endraw %}

Now let’s add the members section.

{% raw %}

...
      <mat-list class="members">
        <mat-list-item *ngFor="let member of selectedConversation.members | keys">
          <p>{{member.value.user.name}}</p>
        </mat-list-item>
      </mat-list>
...

{% endraw %}

We’re using a pipe called keys here to transform the members dictionary object we get from the SDK into an array, so we’ll need to create that using the Angular CLI and update the generated keys.pipe.ts file:

{% raw %}

$ ng g pipe keys

{% endraw %}

{% raw %}

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'keys'
})
export class KeysPipe implements PipeTransform {

  transform(value, args:string[]) : any {
    let keys = [];
    for (let key in value) {
      keys.push({key: key, value: value[key]});
    }
    return keys;
  }
}

{% endraw %}

Next, we’ll update the conversation-header section of the view to display the selected conversation name and member count

{% raw %}

...
    <div class="mat-typography conversation-header">
      <h2>
        <mat-icon>forum</mat-icon>
        {{selectedConversation.display_name}}</h2>
      <p>
        <mat-icon>account_circle</mat-icon>
        {{(selectedConversation.members | keys).length}} Members</p>
    </div>
...

{% endraw %}

We also need to update the conversation-history section in order to parse the events and recreate history in the chat. Events coming from the Stitch SDK have multiple types, so we’ll account for the some of them, like member:joined and text.

{% raw %}

...
    <mat-list dense class="conversation-history mat-typography">
      <mat-list-item *ngFor="let event of events; index as i" [dir]="event.from === selectedConversation.me.id ? 'rtl' : 'ltr'">
        <img *ngIf="event.type == 'text'" matListAvatar matTooltip="{{selectedConversation.members[event.from].user.name}}" src="https://randomuser.me/api/portraits/thumb/lego/{{i}}.jpg"
        />
        <p *ngIf="event.type == 'text'" [dir]="'ltr'">{{event.body.text}}</p>
        <p *ngIf="event.type == 'member:joined'" class="text-center">
          <b>{{selectedConversation.members[event.from].user.name}}</b> has joined the conversation</p>
      </mat-list-item>
    </mat-list>
...

{% endraw %}

We’ll need to update the conversation-input part, in order to be able to send messages into the conversation

{% raw %}

...
    <div class="conversation-input">
      <mat-divider></mat-divider>
      <mat-form-field class="full-width">
        <input matInput placeholder="Start chatting..." name="text" [(ngModel)]="text">
        <mat-icon matSuffix (click)="sendText(text)">send</mat-icon>
      </mat-form-field>
    </div>
...

{% endraw %}

Let’s add some CSS to it to make it full screen and fixed position. Add the following CSS to conversation.component.css:

{% raw %}

.container {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
}

.mat-drawer.mat-drawer-side {
    padding: 0 5px;
}

.empty-conversation {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
}

.conversation-header h2, .conversation-header p {
    align-items: center;
    display: flex;
}

.text-center {
    text-align: center;
    width: 100%;
}

.conversation-history.mat-list {
    height: calc(100% - 180px);
    overflow-x: scroll;
    position: absolute;
    width: 100%;
}

.conversation-history.mat-list p {
    margin: 0;
}

.empty-conversation h1 {
    margin: 0;
}

.conversations .mat-list-item {
    cursor: pointer;
}

.mat-card {
    height: 100%;
    padding: 0 24px;
    overflow: scroll;
}
.conversation-input {
    position: absolute;
    bottom: 0;
    width: 100%;
    background-color: #fafafa;
}

section .mat-list .mat-list-avatar{
    width: 25px;
    height: 25px;
}

.mat-list-avatar {
    margin: 0 5px;
}

.right {
    text-align: right;
}

.full-width {
    width: 100%;
}

.full-width .mat-icon {
    cursor: pointer;
}

{% endraw %}

Last but not least, we need to update the app routing module in app-routing.module.ts in order to have the correct routes displayed

{% raw %}

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ConversationComponent } from './conversation/conversation.component';


const routes: Routes = [
    {
        path: '',
        component: LoginComponent,
    },
    {
      path: 'conversation',
      component: ConversationComponent,
  }
];

@NgModule({
    imports: [
        RouterModule.forRoot(routes)
    ],
    exports: [
        RouterModule
    ],
    declarations: []
})
export class AppRoutingModule { }

{% endraw %}

We also need to replace the entire app.component.html with the <router-outlet> in order to display the router on the first page

{% raw %}

<router-outlet></router-outlet>

{% endraw %}

##Conclusion

After making the app detailed in this post, run the app to see it working.

{% raw %}

$ ng serve

{% endraw %}

The app will run at “http://localhost:4200”. I’d suggest opening the app in two separate tabs, logging in with both alice and jamie, and start talking to eachother! If you’d like to see the app in its final state, you can check out the source code for this app on our community github page. If you want to see a more advanced version of this code, you can check the stitch-demo middleware code you downloaded at the beginning of the blog post, it also contains an Angular Material front-end.

What’s next?

If you’d like to continue learning how to use the Nexmo Stitch SDK for JavaScript, check out our In-App Messaging quickstarts where we show you how to create a simple conversation, invite & chat with another user and use more event listeners to show chat history and when a user is typing. If you have more questions about using Stitch we encourage you to join the Nexmo community slack and check out our #stitch channel or email us directly at ea-support@nexmo.com.