loading...
TalkJS

Add chat into your Angular app with TalkJS - Part 1

sarah_chima profile image Sarah Chima Updated on ・11 min read

This tutorial will show you how you can implement a buyer-seller chat for online marketplaces, as well as a user to user chat, through the use of TalkJS into any Angular 6 application. We will show you how to implement TalkJS into an already existing application to give more context to the implementation. We’ll talk more about this existing application further throughout this tutorial.

This is the first part of a two-part tutorial. In this part, we'll see how we can add a chat popup to an existing application. In the next part, we'll learn how to add a chatbox and inbox to the application.

First things first

Prerequisites

Angular version
The code in this tutorial has been written in Angular CLI version 6.1.5. Make sure to follow this tutorial with Angular CLI v6.1.5 or above.

NodeJS version
Make sure you're working with NodeJS v8.11.4 or above.

The marketplace

Our marketplace is an application that realizes a simplified use case of a marketplace. In this marketplace, users are able to log in and view product listings:

Before this tutorial's implementation:

Before adding chat

After this tutorial's implementation:

Alt Text

Click on an image to open its example application live.

This tutorial is meant to demonstrate how to add chat functionality to any Angular 6 application, by the use of TalkJS. You can perform the steps in this tutorial on our example marketplace, but you should also be able to do them inside your own Angular app right away.

Source code for both marketplace applications can be found on our GitHub repo.

Chat functionalities will be added to the following pages within our marketplace: user profile, product page & inbox page.

Starting the application
Start the application into which you're going to add chat functionalities.

If you're adding chat functionalities to our marketplace application, you can clone the project from its GitHub repository.

Start the application:

npm install
npm start

If the marketplace has started successfully, navigate to http://localhost:4200/ in your browser to see the application.

Let’s get started

Install the TalkJS JavaScript SDK

The very first thing that we should do is install the TalkJS JavaScript SDK into our project:

npm install talkjs --save

Create TalkService

In order to maintain modularity within our application, all TalkJS logic will have to be executed in a separate service.

Create a file in src/app/core/services called talk.service.ts and fill it with the following code:

import { Injectable } from "@angular/core";

import * as Talk from 'talkjs';

@Injectable({
providedIn: 'root'
})

export class TalkService { }

To ensure that our TalkService runs as a singleton instance throughout our entire application, we're providing it in the root of our application. Make sure to not add this service to the list of providers for any module, as this will cause our service to not run as a single singleton instance anymore. You can read more about singleton services here.

Active Session

The first thing you should do for TalkJS to properly work within your application is to start a Session for the current logged in user. As long as you have an active Session running, your user will be able to receive desktop notifications. We will, therefore, make sure that the Session is running on each page of our application, even on ones on which our user is not able to read or write messages. You can read more about a TalkJS Session here.

Authentication

Whenever a user logs into our application, we should make sure that we’re creating a Session for this user.

Navigate to the LoginComponent:
src/app/core/authentication/components/login/login.component.ts

After our user has successfully logged in, we should start a Session. We’ll have to call the TalkService#createCurrentSession method — which we’ll create in just a moment — in our login function.

In order for us to be able to call a function in the TalkService, we should first inject the singleton instance of the TalkService into our LoginComponent, through the use of dependency injection.

We’ll pass it as a parameter into our LoginComponent's constructor:

constructor(..., private talkService: TalkService)

Make sure to import the TalkService into the LoginComponent:

import { TalkService } from 'src/app/core/services/talk.service';

Call the TalkService#createCurrentSession method in the login method:

login(credentials) {
   this.authenticationService.login(credentials.username).then(response => {
   if (response) {
       this.toastrService.success('Successful login');
       this.router.navigate(['home']);

       this.talkService.createCurrentSession();
   } else {
     this.toastrService.error('Incorrect credentials');
   }
 });
}

TalkService Methods

App ID
In order for TalkJS to work within your application, your application should have an App ID, which you can find in the TalkJS dashboard.

Create an account — for free while in a test environment — at TalkJS.

Then, go to the TalkJS dashboard and look for your App ID.

Save the App ID as a private constant in the TalkService:

private static APP_ID = 'YOUR_APP_ID';

Current User
In order for the TalkService to create a Session, it needs to know the application's current user. Our AuthenticationService contains a method to retrieve the current user.

Inject the AuthenticationService into the TalkService:

constructor(private authenticationService: AuthenticationService)

TalkJS User
We’ll need an instance of the TalkJS User class to create a Session. Create a method that converts an instance of our application's User class to an instance of the TalkJS User class:

private currentTalkUser: Talk.User;

async createTalkUser(applicationUser: User) : Promise {
   await Talk.ready;

   return new Talk.User({
      id: applicationUser.id,
      name: applicationUser.username,
      photoUrl: applicationUser.profilePictureUrl
   });
}

TalkJS's SDK is loaded asynchronously. By working with asynchronous methods as well, we're making sure that all TalkJS-related code is non-blocking within our application and that we're following the I/O standards (I/O methods being asynchronous).

At first, we're waiting for TalkJS’s SDK to be loaded, which we do by calling:

await Talk.ready

Then, we’re creating a new instance of the User class, filling it with our current user’s data.

Session Creation
Add the following method to create the actual Session:

async createCurrentSession() {
   await Talk.ready;

   const currentUser = await this.authenticationService.getCurrentUser();
   const currentTalkUser = await this.createTalkUser(currentUser);
   const session = new Talk.Session({
      appId: TalkService.APP_ID,
      me: currentTalkUser
      });
   this.currentTalkUser = currentTalkUser;
   this.currentSessionDeferred.resolve(session);
}

I’ll explain what's going on here, step by step.

As you can see, this method is an asynchronous method as well. We need to wait for TalkJS to be ready before we can create the Session.

We should then make sure to convert our application’s current User instance to the TalkJS User instance, by first retrieving our application’s current User and then converting it:

const currentUser = await this.authenticationService.getCurrentUser();
const currentTalkUser = await this.createTalkUser(currentUser);

After retrieving and converting our current User, we’re creating the actual Session:

const session = new Talk.Session({
   appId: TalkService.APP_ID,
   me: currentTalkUser
});

Session Retrieval
Whenever our user is already logged into our application and visits a Component that has to make use of our Session, there’s a possibility that our Session is still being created, while the Component is already trying to use the Session. This can cause all sorts of issues so we’re going to fix this by making sure that the application is able to wait for the Session to be active.

What we want to achieve is that we’re able to call code similar to:

await currentSession;

Without having to poll for the currentSession until it is active. This means we need to create a promise called currentSession that resolves when the session has loaded.

A common way to create a promise is by using a Deferred, which is a little object that lets you return a promise and resolve it later. The example code includes a helper class for this.

We’ll create it upon construction:

private currentSessionDeferred = new Deferred();

When we create the session, we’ll resolve the currentSessionDeferred with the session value:

this.currentSessionDeferred.resolve(session);

We can then await the current session like this anywhere else in the TalkService:

await this.currentSessionDeferred.promise;

Ok great! Your TalkService should look like:

import { Injectable } from "@angular/core";
import * as Talk from 'talkjs';
import { User } from "src/app/shared/models/user.model";
import { AuthenticationService } from "src/app/core/services/authentication.service";
import { Deferred } from "src/app/shared/utils/deffered.util";

@Injectable({
providedIn: 'root'
})
export class TalkService {
   private static APP_ID = 'YOUR_APP_ID';
   private currentTalkUser: Talk.User;
   private currentSessionDeferred = new Deferred()

   constructor(private authenticationService: AuthenticationService) { }

   async createCurrentSession() {
      await Talk.ready;

      const currentUser = await this.authenticationService.getCurrentUser();
      const currentTalkUser = await this.createTalkUser(currentUser);
      const session = new Talk.Session({
         appId: TalkService.APP_ID,
         me: currentTalkUser
      });
      this.currentTalkUser = currentTalkUser;
      this.currentSessionDeferred.resolve(session);
   }

   async createTalkUser(applicationUser: User) : Promise {
   await Talk.ready;

   return new Talk.User({
      id: applicationUser.id,
      name: applicationUser.username,
      photoUrl: applicationUser.profilePictureUrl
      });
   }
}

Core Module
There is one more step that we should do in order to finish this section.

The CoreModule is the heart of our application. It is the first module that is being loaded by the application after the AppModule. Our application’s architecture has been designed in such a way that all other modules but the CoreModule and AppModule, are being lazily loaded — they're only loaded whenever needed.

We also need to consider the scenario in which a user is already logged in when they load the application. As of now, our application only starts a Session whenever our user logs in. This means that with the aforementioned scenario, our user is logged in while there is no active Session running. As you're aware, it is important that there's always an active Session running when our user is logged in. We should, therefore, make sure that a Session will be created for the already logged in user in this scenario. We’re able to do this by making sure that if the CoreModule starts, the Session will be created as well.

Navigate to the CoreModule in src/app/core/core.module.ts and add the following highlighted line:

constructor (
@Optional() @SkipSelf() parentModule: CoreModule,
private productService: ProductService,
private talkService: TalkService) {
   if (parentModule) {
      throw new Error('CoreModule is already loaded. Import only in AppModule');
   }

   this.talkService.createCurrentSession();
}

Make sure to also inject the TalkService into the CoreModule.

Chat Popup

In this section, we’re going to make sure that our user is able to open a chat with the vendor of a product, by the use of a Chat Popup.

This is what a Chat Popup looks like:

Preloading

Navigate to the product page of a motorcycle.

The first thing we should do is make sure that the chat between our user and the product’s vendor, is ready before our user actually tries to open this chat.

We’re going to do this by preloading the chat whenever the product page is being loaded.

TalkService
Add the following method to the TalkService:

async createPopup(otherApplicationUser: User, keepOpen: boolean) : Promise {
   const session = await this.currentSessionDeferred.promise;
   const conversationBuilder = await this.getOrCreateConversation(session, otherApplicationUser);
   const popup = session.createPopup(conversationBuilder, { keepOpen: keepOpen });

   return popup;
}

What this method does is retrieve the currentSession and create a TalkJS ConversationBuilder instance by calling the TalkService#getOrCreateConversation method, which we’re going to add in just a moment.

The Session has a method that creates and returns a TalkJS Popup instance. We're calling this method and return its created Popup instance. The keepOpen PopupOption determines whether the Popup should stay open if the user navigates to a different page in your application. You can read more about it here.

Add the missing TalkService#getOrCreateConversation method:

private async getOrCreateConversation(session: Talk.Session, otherApplicationUser: User) {
   const otherTalkUser = await this.createTalkUser(otherApplicationUser);
   const conversationBuilder = session.getOrCreateConversation(Talk.oneOnOneId(this.currentTalkUser, otherTalkUser));

   conversationBuilder.setParticipant(this.currentTalkUser);
   conversationBuilder.setParticipant(otherTalkUser);

   return conversationBuilder;
}

The Session#getOrCreateConversation method requires a conversationId. TalkJS has a function called Talk#oneOnOneId which generates an id between two TalkJS User instances, which will always be the same for the two given users, no matter in which order you pass the users as its parameters. You can read more about the function here.

We're using the Talk#oneOnOneId method to generate the needed conversationId.

Product Page Component
Navigate to the ProductPageComponent:
src/app/products/components/product-page/product-page.component.ts

We’ll first have to add a local variable for the Popup that we’re going to preload and display. Add:

private chatPopup: Talk.Popup;

Make sure to import the TalkJS SDK and inject our TalkService into this Component.

Add the preloading method:

private async preloadChatPopup(vendor: User) {
   this.chatPopup = await this.talkService.createPopup(vendor, false);
   this.chatPopup.mount({ show: false });
}

This method asynchronously waits for the Popup to be created, to then assign it to a local variable and call the popup#mount method on the created Popup. The popup#mount method gets called with the show property being false, which means that the Popup is being mounted — this is needed to be able to show the Popup later on — but not shown afterward.

Call the preloading method in the ngOnInit lifecycle hook:

ngOnInit() {
   this.productService.getProduct(this.getProductId()).then(product => {
   this.product = product;

   this.preloadChatPopup(product.vendor);
   });
}

Displaying

Create a button which, when clicked, calls the ProductPageComponent#showChatPopup method. You can look at how we added the button in the final source of the marketplace application.

Add the display method to our ProductPageComponent:

showChatPopup() {
   this.chatPopup.show();
}

We have now successfully added the TalkJS Popup to our application.

If you have successfully executed all steps, your TalkService, ProductPageComponent and ProductPageComponent's template should look like:

TalkService

import { Injectable } from "@angular/core";
import * as Talk from 'talkjs';
import { User } from "src/app/shared/models/user.model";
import { AuthenticationService } from "src/app/core/services/authentication.service";
import { Deferred } from "src/app/shared/utils/deffered.util";

@Injectable({
providedIn: 'root'
})
export class TalkService {
   private static APP_ID = 'YOUR_APP_ID';
   private currentTalkUser: Talk.User;
   private currentSessionDeferred = new Deferred()

   constructor(private authenticationService: AuthenticationService) { }

   async createCurrentSession() {
      await Talk.ready;

      const currentUser = await this.authenticationService.getCurrentUser();
      const currentTalkUser = await this.createTalkUser(currentUser);
      const session = new Talk.Session({
         appId: TalkService.APP_ID,
         me: currentTalkUser
      });

      this.currentTalkUser = currentTalkUser;
      this.currentSessionDeferred.resolve(session);
   }

   async createTalkUser(applicationUser: User) : Promise {
      await Talk.ready;

      return new Talk.User({
         id: applicationUser.id,
         name: applicationUser.username,
         photoUrl: applicationUser.profilePictureUrl
      });
   }

   async createPopup(otherApplicationUser: User, keepOpen: boolean) : Promise {
      const session = await this.currentSessionDeferred.promise;
      const conversationBuilder = await this.getOrCreateConversation(session, otherApplicationUser);
      const popup = session.createPopup(conversationBuilder, { keepOpen: keepOpen });

      return popup;
   }

   private async getOrCreateConversation(session: Talk.Session, otherApplicationUser: User) {
      const otherTalkUser = await this.createTalkUser(otherApplicationUser);
      const conversationBuilder = session.getOrCreateConversation(Talk.oneOnOneId(this.currentTalkUser, otherTalkUser));

      conversationBuilder.setParticipant(this.currentTalkUser);
      conversationBuilder.setParticipant(otherTalkUser);

      return conversationBuilder;
   }
}

ProductPageComponent:

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

import * as Talk from 'talkjs';

import { Product } from 'src/app/shared/models/product.model';
import { ProductService } from 'src/app/core/services/product.service';
import { User } from 'src/app/shared/models/user.model';
import { TalkService } from 'src/app/core/services/talk.service';

@Component({
selector: 'app-product-page',
templateUrl: './product-page.component.html',
styleUrls: ['./product-page.component.css']
})

export class ProductPageComponent implements OnInit {
   product: Product;
   private chatPopup: Talk.Popup;

   constructor(
   private productService: ProductService,
   private talkService: TalkService,
   private route: ActivatedRoute,
   private router: Router) { }

   ngOnInit() {
      this.productService.getProduct(this.getProductId()).then(product => {
      this.product = product;

      this.preloadChatPopup(product.vendor);
      });
   }

   goToVendorPage(vendor: User) {
      this.router.navigate(['users/' + vendor.id]);
   }

   showChatPopup() {
      this.chatPopup.show();
   }

   private async preloadChatPopup(vendor: User) {
      this.chatPopup = await this.talkService.createPopup(vendor, false);
      this.chatPopup.mount({ show: false });
   }

   private getProductId() {
      return Number(this.route.snapshot.paramMap.get('id'));
   }
}

So far, we've learnt how we can add a chat popup to an Angular marketplace application. In the next part of this tutorial, we will learn how to add a chatbox and inbox to the application.

Posted on by:

sarah_chima profile

Sarah Chima

@sarah_chima

A Frontend web developer interested in making the web accessible for everyone.

TalkJS

Build an integrated chat feature in hours instead of months.

Discussion

markdown guide