https://d226lax1qjow5r.cloudfront.net/blog/blogposts/build-a-chat-application-with-angular-material-and-vonage/angular_chat-app.png

Crea una aplicación de chat con Angular Material y Vonage

Publicado el November 6, 2020

Tiempo de lectura: 26 minutos

Nota: Es posible que algunas de las herramientas o métodos descritos en este artículo ya no reciban soporte o no estén actualizados. Para obtener contenido actualizado o soporte, consulta nuestras últimas publicaciones o contáctanos en el Slack de la comunidad de Vonage

En este tutorial, habilitaremos el chat en una aplicación web Angular utilizando el SDK de JavaScript y la Conversation API para que los usuarios puedan comunicarse en nuestra aplicación. Si deseas ver el código fuente, se encuentra en nuestra página de Vonage Community GitHub de Vonage.

Esto es lo que intentamos construir:

Gif showing the end goal of this chat application

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Antes de empezar

Antes de empezar, necesitarás algunas cosas:

  • Conocimientos básicos de Angular

  • Node.js instalado en su máquina.

  • El código del middleware desde GitHub.

  • La CLI de Vonage. Instálalo de la siguiente manera:

npm install -g @vonage/cli

Configura la CLI para usar tu clave y secreto de API de Vonage:

vonage auth set –api-key=’VONAGE_API_KEY –api-secret=’VONAGE_API_SECRET’

Obtenga el código del middleware en GitHub

En primer lugar, vamos a clonar el código fuente del middleware e instalar sus dependencias. Vamos a escribir una aplicación Node.js usando Express que proporciona un nivel de abstracción entre la API de Vonage y el código Angular:

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

Ejecutar el código Middleware desde GitHub

Antes de poder ejecutar el código, necesitaremos crear una aplicación RTC dentro de la plataforma de Vonage para usar dentro de este código:

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

La salida del comando anterior será algo parecido a esto:

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

El primer elemento es el ID de Applications, del que debe tomar nota (nos referiremos a él como APP_ID más adelante). El último valor es la ubicación de la clave privada. La clave privada se utiliza para generar JWT que autentican tus interacciones con Vonage.

Ahora, tendremos que hacer una copia de example.env y llamarla .envy actualizar los valores dentro de tu Vonage API_KEY y API_SECRET. También tendremos que añadir el APP_ID que acabamos de generar la ruta a tu clave privada. Después de haber actualizado los valores, podemos ejecutar el código en modo de depuración con:

npm run debug

Crear usuarios y conversaciones

La aplicación debe ejecutarse en localhost:3000. Ahora que la aplicación se está ejecutando, vamos a seguir adelante y crear un par de usuarios y una conversación, y luego vamos a añadir los usuarios que hemos creado a la conversación.

Crearemos un par de usuarios ejecutando este comando dos veces, una con el nombre de usuario alice y luego con jamie:

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

La salida debería ser similar a:

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

Anotaremos el ID de usuario y nos referiremos a él más adelante como USER_ID. Ahora, vamos a crear una conversación a través de la API de demostración:

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

La salida debería ser similar a:

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

Anotaremos el ID de la conversación y nos referiremos a ella más adelante como CONVERSATION_ID. Ahora, vamos a unir a los usuarios en la conversación. Vamos a ejecutar el siguiente comando dos veces-recuerda sustituir los caracteres CONVERSATION_ID y USER_ID por los IDs de los dos pasos anteriores cada vez que ejecutes este comando:

curl --request PUT \ --url http://localhost:3000/api/conversations \ --header 'content-type: application/json' \ --data '{ "conversationId": "CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab", "userId": "USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab", "action": "join" }'

Generar aplicación Angular

Ahora que tenemos nuestro middleware en funcionamiento, es el momento de crear la aplicación Angular. Vamos a utilizar el Angular CLI para generar la aplicación, así que si no lo tienes instalado, necesitarás instalarlo primero:

npm install -g @angular/cli

A continuación, lo utilizaremos para generar una nueva aplicación con enrutamiento. Puede tardar un poco en generar todos los archivos e instalar las dependencias:

ng new nexmo-stitch-angular --routing

Añadir Material Design

Una vez finalizado el comando anterior, añadiremos Material Angular y sus dependencias al proyecto:

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

También tendremos que importar el NgModule para cada componente que queramos utilizar en nuestra aplicación. Para ello, tenemos que abrir src/app/app.module.ts en nuestro editor e importar los módulos de la parte superior:

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';

También tendremos que actualizar la declaración @NgModules para añadir los módulos anteriores:

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

También vamos a querer añadir un tema a Material, por lo que añadir esta línea a su style.css:

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

Si queremos utilizar los Material Design Iconstendremos que cargar la fuente de los iconos en nuestro archivo index.html:

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

Polyfill Vonage Client SDK para JavaScript

Ahora que tenemos nuestra aplicación Angular generada y todo configurado con Material, instalaremos el Vonage Client SDK para JavaScript y lo añadiremos al final de polyfills.ts:

npm install --save nexmo-conversation
/**********************************************************
 * APPLICATION IMPORTS
 */
import 'nexmo-conversation';

Servicio de mensajería

Necesitaremos crear un Servicio Angular para manejar los datos de nuestro middleware. Vamos a generarlo usando el CLI de Angular:

ng g service messaging

En nuestra carpeta src/app carpeta, messaging.service.spec.ts y messaging.service.spec.ts. Vamos a actualizar el archivo messaging.service.spec.ts para agregar el ConversationClient del Vonage In-App SDK, instanciar un cliente y manejar la obtención de un usuario Token Web JSON del middleware. Vamos a reemplazar el código repetitivo con:

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)
  }
}

Debemos actualizar app.module.ts para importar el MessagingService y registrarlo como proveedor:

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

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

Componente de inicio de sesión

Empecemos a construir algo de UI para nuestra aplicación. Empezaremos con un LoginComponent, y lo generaremos con el CLI de Angular:

ng g component login

Esto generará una carpeta login dentro de la carpeta app y cuatro archivos para el código HTML, CSS y TypeScript, así como las pruebas. Reemplacemos el código en login.component.html con una interfaz de usuario para iniciar sesión. Elegí un <mat-grid-list> con un <mat-card> dentro de él, y un formulario con un botón de inicio de sesión que llama a onLogin() cuando se envía. El código se ve así:

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

Vamos a añadirle algo de CSS para el spinner Material en el archivo login.component.css archivo:

.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;
}

También tenemos que actualizar login.component.ts para añadirle el MessagingService e implementar el método onLogin() método. El método tomará el nombre de usuario, realizará una solicitud a través del servicio de mensajería al middleware que estamos ejecutando para obtener un JWT de usuario y luego lo utilizará para autenticar a través del método login del SDK de Vonage In-App JavaScript. El código es el siguiente:

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']);
    })
  }
}

Fíjate que he importado el Router de Angular, lo he inyectado en nuestro constructor, y lo estoy usando al final del flujo de autenticación para navegar a la siguiente página, /conversation.

Componente de conversación

Todavía no hemos creado el componente conversation así que usemos la CLI de Angular para crearlo:

ng g component conversation

Vamos a actualizar el archivo conversation.component.html para utilizar un material sidenav que lista las conversaciones de los usuarios a la izquierda y los miembros de la conversación a la derecha, dejando el centro para nuestro chat principal. Vamos a añadir una cabecera a la sección de chat para listar el nombre de la conversación y el número de miembros, y una sección de entrada se añadirá en la parte inferior. Dejaremos la sección central para mostrar el historial de conversaciones. Construiremos un shell vacío por ahora y lo añadiremos más tarde a medida que desarrollemos el módulo ConversationComponent. El HMTL debería tener este aspecto:

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

Vamos a actualizar el archivo conversation.component.ts con el boilerplate necesario para los métodos que vamos a utilizar más adelante:

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> = []
}

Empecemos implementando ngOnInit() para que compruebe si tenemos datos de la aplicación antes de intentar getConversations utilizar el SDK In-App. Si no hay datos de la aplicación, entonces seremos redirigidos a la pantalla de inicio de sesión:

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

Necesitamos implementar un método de ayuda para construir una matriz de conversaciones a partir del diccionario de conversaciones que proporciona el SDK de JavaScript In-App para que podamos utilizarlo con *ngFor en la interfaz de usuario:

buildConversationsArray(conversations) {
    let array = [];

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

    return array
  }

Agreguemos un método para seleccionar una conversación de la lista. Necesitaremos tomar el ID de la vista, pasarlo al controlador y luego usar el SDK de Vonage para obtener datos sobre la conversación. Almacenaremos esto en una propiedad de la clase para que esté disponible para ver más adelante. También usaremos Observable para crear un array a partir de conversation.events Map para que podamos recrear el historial de chat cuando el usuario regrese a la aplicación. También añadiremos un listener de eventos usando el SDK para escuchar eventos y añadirlos a la lista de eventos. text y añadirlos al historial de eventos:

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)
    }
    )
  }

Por último, agreguemos un método que tome la entrada de la vista y la envíe a la API de Vonage en la aplicación a través del SDK:

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

Ahora que hemos implementado todos los métodos que necesitamos, podemos volver atrás y dar más cuerpo a la vista para utilizar los modelos de datos que hemos creado en el controlador. En primer lugar, vamos a actualizar la sección conversations en conversation.component.html:

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

Ahora vamos a añadir la sección de miembros:

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

Estamos utilizando una tubería llamada keys para transformar el objeto members que obtenemos del SDK en un array, así que tendremos que crearlo usando la CLI de Angular y actualizar el archivo keys.pipe.ts generado:

ng g pipe keys
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;
  }
}

A continuación, actualizaremos la sección conversation-header de la vista para mostrar el nombre de la conversación seleccionada y el número de miembros:

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

También tenemos que actualizar la sección conversation-history para analizar los eventos y recrear el historial en el chat. Los eventos procedentes del SDK de la aplicación tienen varios tipos, por lo que tendremos en cuenta algunos de ellos, como member:joined y text:

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

Tendremos que actualizar la parte conversation-input para poder enviar mensajes a la conversación:

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

Vamos a añadirle algo de CSS para que sea una pantalla completa y de posición fija. Añade el siguiente CSS a conversation.component.css:

.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;
}

Por último, pero no menos importante, tenemos que actualizar el módulo de enrutamiento de la aplicación en app-routing.module.ts para que se muestren las rutas correctas:

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 { }

También tenemos que sustituir todo el app.component.html por el <router-outlet> para mostrar el router en la primera página:

<router-outlet></router-outlet>

Ejecute su aplicación

Después de hacer la aplicación detallada en este post, ejecute la aplicación para ver que funciona:

ng serve

La aplicación se ejecutará en "http://localhost:4200". Yo sugeriría abrir la aplicación en dos pestañas separadas, iniciar sesión con ambos alice y jamie y ¡empezad a hablar entre vosotros! Si quieres ver la aplicación en su estado final, puedes consultar el código fuente de esta aplicación en nuestra página GitHub de la comunidad. Si quieres ver una versión más avanzada de este código, puedes consultar el stitch-demo que descargaste al principio de la entrada del blog. También contiene un front-end Angular Material.

¿Y ahora qué?

Si deseas continuar aprendiendo a usar Vonage Client SDK para JavaScript, consulta nuestro inicio rápido, donde te mostramos cómo hacerlo:

Compartir:

https://a.storyblok.com/f/270183/384x384/dabe7c5397/laka.png
Alex LakatosAntiguos alumnos de Vonage

Alex Lakatos es JavaScript Developer Advocate para Nexmo. En su tiempo libre es voluntario en Mozilla como Tech Speaker y Reps Mentor. Desarrollador de JavaScript en la web abierta, ha estado empujando sus límites todos los días. Cuando no está programando en Londres, le gusta viajar por el mundo, así que es probable que te lo encuentres en la sala de espera de un aeropuerto.