Add buyer-seller chat into a marketplace with Angular 6

This is an older tutorial. Check out the Getting started guide for Angular.

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.


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:

After this tutorial's implementation:

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 an example Angular 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:

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. In the docs you can read more about a TalkJS Session.

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.

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

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

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

Chatbox

In this chapter, we're going to make sure that a user is able to send a vendor a message from his/her profile page, by the use of a Chatbox. You’ll notice that the code to do this is extremely similar to the code used for creating a Popup.

This is what a Chatbox looks like:

TalkService

Add the following method to the TalkService:

async createChatbox(otherApplicationUser: User) : Promise {
   const session = await this.currentSessionDeferred.promise;
   const conversationBuilder = await this.getOrCreateConversation(session, otherApplicationUser);
   return session.createChatbox(conversationBuilder);
}

User Profile Component

Navigate to the UserProfileComponent:

`src/app/users/components/user-profile/user-profile.component.ts`

Template

The first thing we’ll have to do is add a container for the Chatbox in the UserProfileComponent’s template.

Add the following highlighted code to the UserProfileComponent's template:

<div id="chat-container">
      <div class="row">
            <div class="col-sm-4"></div>
            <div class="col-sm-4 container-title">Chat</div>
            <div class="col-sm-4"></div>
      </div>
      <div id="talkjs-container">Loading chat with {{user?.username}}..</div>
  </div>

The code that we've just added is a html div element. This element is going to serve as the container for our TalkJS Chatbox, which is why we’re calling it ‘talkjs-container’.

Template Styling

We’ll have to add some styling to assure that our Chatbox is being shown in the center of the page and is of sufficient height.

Open the styling file for the UserProfileComponent's template: `user-profile.component.css`.

Add the following code:

#talkjs-container {
   height: 505px;
   margin-top: 1%;
   text-align: center;
}
Component

We’ll now have to add a property and a method in the UserProfileComponent to load the Chatbox in the template’s container.

Add the following property:

import * as Talk from "talkjs";

...
export class UserProfileComponent implements OnInit {
   private chatbox: Talk.Chatbox;
}

Add the following method:

private async loadChatbox(otherUser: User) {
   this.chatbox = await this.talkService.createChatbox(otherUser);
   this.chatbox.mount(document.getElementById('talkjs-container'));
}

Add the method call to the ngOnInit lifecycle hook:

ngOnInit() {
   this.userService.getUser(this.getUserId()).then(user => {
      this.user = user;
      this.loadChatbox(this.user);
   });
}

When our component is being destroyed, we should make sure that the TalkJS Chatbox and its event listeners are also being destroyed. Add the following body to the ngOnDestroy lifecycle hook:

ngOnDestroy() {
   if (this.chatbox) {
      this.chatbox.destroy();
   }
}

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

If you have successfully executed all steps, your TalkService, UserProfileComponent, UserProfileComponent’s template, and UserProfileComponent’s template styling 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;
   }

   async createChatbox(otherApplicationUser: User) : Promise {
      const session = await this.currentSessionDeferred.promise;
      const conversationBuilder = await this.getOrCreateConversation(session, otherApplicationUser);
      
      return session.createChatbox(conversationBuilder);
   }

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

UserProfileComponent:

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

import * as Talk from "talkjs";

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

@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
   private chatbox: Talk.Chatbox;
   user: User;

   constructor(
   private userService: UserService,
   private talkService: TalkService,
   private route: ActivatedRoute) { }

   ngOnInit() {
      this.userService.getUser(this.getUserId()).then(user => {
      this.user = user;
      this.loadChatbox(this.user);
      });
   }

   ngOnDestroy() {
      if (this.chatbox) {
         this.chatbox.destroy();
      }
   }

   private async loadChatbox(otherUser: User) {
      this.chatbox = await this.talkService.createChatbox(otherUser);
      this.chatbox.mount(document.getElementById('talkjs-container'));
   }

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

UserProfileComponent’s template:

<div *ngIf="user">
  <div id="personal-information-container">
      <div class="row">
          <div class="col-sm-4"></div>
          <div class="col-sm-4">
              <div class="card personal-information-card">
                  <img class="card-img-top personal-information-card-img-top img-fluid mx-auto d-block" src="{{user?.profilePictureUrl}}" alt="profile-picture">
                  <div class="card-block">
                      <h4 class="card-title personal-information-card-title">{{user?.username}}</h4>
                  </div>
              </div>
          </div>
          <div class="col-sm-4"></div>
      </div>
  </div>
  <hr class="divider" *ngIf="(user.products) && (user.products.length > 0)">
  <div id="owned-products-container" class="container" *ngIf="(user.products) && (user.products.length > 0)">
    <div class="row">
            <div class="col-sm-4"></div>
            <div class="col-sm-4 container-title">Products</div>
            <div class="col-sm-4"></div>
      </div>
      <div id="products-row" class="row">
        <div class="col-sm-3" *ngFor="let product of user.products">
            <div class="card owned-products-card">
                <img class="card-img-top owned-products-card-img-top img-fluid" src="{{product?.pictureUrl}}" alt="product-image">
                <div class="card-block">
                    <h4 class="card-title owned-products-card-title">{{product?.name}}</h4>
                </div>
            </div>
        </div>
      </div>
  </div>
  <hr class="divider">
  <div id="chat-container">
      <div class="row">
            <div class="col-sm-4"></div>
            <div class="col-sm-4 container-title">Chat</div>
            <div class="col-sm-4"></div>
      </div>
      <div id="talkjs-container">Loading chat with {{user?.username}}..</div>
  </div>
</div>

UserProfileComponent’s template styling:

#talkjs-container {
   height: 505px;
   margin-top: 1%;
   text-align: center;
}

Inbox

Finally, let’s make sure that our user is able to view and send messages in previous conversations by the use of the Inbox.

This is what an Inbox looks like:

TalkService

Add the following code to the TalkService:

async createInbox() : Promise {
   const session = await this.currentSession;

   return session.createInbox();
}

Inbox Component

It’s best practice to display the TalkJS Inbox on a separate page in your application. This means that you’ll have to add a new Component to your application.

You can take a look at how we added an InboxComponent by looking into the source code of this tutorial’s final product, the marketplace with chat functionalities.

Template

We’ll have to add a container for the Inbox in the InboxComponent's template.

Open the InboxComponent's template:

`src/app/chat-inbox/components/inbox/inbox.components.html`

Add the following code to the template:



Loading chats..

Template Styling

We’ll have to add some styling to assure that our Inbox is being shown in the center of the page and is of sufficient height.

Add the following code to the InboxComponent's styling file:

#talkjs-container {
   height: 505px;
   margin-top: 5%;
   text-align: center;
}
Component

We still have to add a method in the InboxComponent that will load our Inbox.

Add the following highlighted property and method:

import * as Talk from "talkjs";

export class InboxComponent implements OnInit {
   private inbox: Talk.Inbox;

   private async createInbox() {
      this.inbox = await this.talkService.createInbox();
      this.inbox.mount(document.getElementById('talkjs-container'));
   }
}

Add the method call in the ngOnInit lifecycle hook:

ngOnInit() {
   this.createInbox();
}

We should make sure to destroy the Inbox and its event handlers when the component gets destroyed:

ngOnDestroy() {
   if (this.inbox) {
      this.inbox.destroy();
   }
}

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

If you have successfully executed all steps, your TalkService, InboxComponent, InboxComponent's template, and InboxComponent’s template styling 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;
   }

   async createChatbox(otherApplicationUser: User) : Promise {
      const session = await this.currentSessionDeferred.promise;
      const conversationBuilder = await this.getOrCreateConversation(session, otherApplicationUser);
      
      return session.createChatbox(conversationBuilder);
   }

   async createInbox() : Promise {
      const session = await this.currentSessionDeferred.promise;
      return session.createInbox();
   }

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

InboxComponent

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

import * as Talk from "talkjs";

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

@Component({
selector: 'app-inbox',
templateUrl: './inbox.component.html',
styleUrls: ['./inbox.component.css']
})
export class InboxComponent implements OnInit {
   private inbox: Talk.Inbox;

   constructor(private talkService: TalkService) { }

   ngOnInit() {
      this.createInbox();
   }

   ngOnDestroy() {
      if (this.inbox) {
         this.inbox.destroy();
      }
   }

   private async createInbox() {
      this.inbox = await this.talkService.createInbox();
      this.inbox.mount(document.getElementById('talkjs-container'));
   }
}

InboxComponent's template:



Loading chats..

InboxComponent’s template styling:

#talkjs-container {
   height: 505px;
   margin-top: 5%;
   text-align: center;
}

Authentication

Before deploying your application to production, you need to ensure that authentication is enabled to prevent malicious users from hijacking accounts. This requires adding a few lines of code to your backend – see our Authentication docs for more information.


Finishing touches

Congratulations, you have implemented TalkJS into an existing application! However, if you want, you can add some finishing touches to make the user experience better.

Enabling file- & location sharing

In this chapter, we’re going to allow our users to share both files and their location in any chat.

TalkJS Dashboard

The first thing we have to do is create a custom configuration in the TalkJS dashboard.

Log into the TalkJS dashboard and navigate to the configurations section.

Create a new configuration by clicking on the plus button. You can give the configuration any name, we're going for ‘demo_default’.

We’re able to enable both file- and location sharing by enabling their checkboxes.

Enable the following checkboxes:

TalkService

To enable the configuration that we just created for all our users, all we have to do is add this configuration to the TalkService#createTalkUser method.

Add the following highlighted code to TalkService#createTalkUser:

async createTalkUser(applicationUser: User) : Promise {
   await Talk.ready;
   
   return new Talk.User({
      id: applicationUser.id,
      name: applicationUser.username,
      photoUrl: applicationUser.profilePictureUrl,
      configuration: 'demo_default'
   });
}

Make sure to use the configuration name that you chose yourself in the TalkJS dashboard.

You have now successfully enabled file- and location sharing in your application.

Enabling email- and SMS-notifications

Enabling email- and SMS-notifications is really easy within TalkJS. All you have to do is pass TalkJS the users' phone number and/or email address and TalkJS will handle the rest!

Add the following highlighted code to `TalkService#createTalkUser`:

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

   return new Talk.User({
      id: applicationUser.id,
      name: applicationUser.username,
      photoUrl: applicationUser.profilePictureUrl,
      email: 'youruser@youruseremail.com',
      phone: 'yourusersphone'
});
}

Read more about notifications.

Welcome message

We’re going to add personal welcome messages for each user in our application.

ChatPreferences

Navigate to the ChatPreferences model:

`src/app/shared/models/chat-preferences.model.ts`

Add a new property for the welcome message as follows:

export class ChatPreferences {
   chatButtonColorHex: string;
   chatWelcomeMessage: string;

   constructor(..., chatWelcomeMessage: string) {
      this.chatButtonColorHex = chatButtonColorHex;
      this.chatWelcomeMessage = chatWelcomeMessage;
   }
}
Mock users

Open the user mocks: `src/core/mocks/users.mock.ts`

Make sure to add a welcome message for each mock user as follows:

new User(4, 'John', '../../../assets/images/users/john.jpg', new ChatPreferences("#1D1F1E", 
"Hi! Any questions? Let me know how I can help"))
TalkService

Add the following highlighted code to TalkService#createTalkUser:

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

   return new Talk.User({
      id: applicationUser.id,
      name: applicationUser.username,
      photoUrl: applicationUser.profilePictureUrl,
      configuration: "demo_default",
      welcomeMessage: applicationUser.chatPreferences.chatWelcomeMessage
   });
}

You have now successfully added welcome messages to your application.

Destroying Popups

You may have noticed that if you open a Popup with a vendor and then navigate to the Inbox page, the Popup is still visible. If the user is at the Inbox page, there is no need to have any Popups open as these conversations can be opened through the Inbox itself.

We will, therefore, write some code that will make sure that any active Popups will be destroyed whenever the Inbox page is being visited by our user.

Let’s go ahead and open the TalkService.

We’ll have to save all the Popups that are being opened until they're destroyed.

To accomplish this, we’ll first have to add a local variable to the TalkService:

private loadedPopups: Talk.Popup[];

And then we’ll have to make sure the array is being initialized, by adding it’s initialization to the TalkService's constructor:

constructor(private authenticationService: AuthenticationService) {
   this.loadedPopups = [];
}

We should now make sure that every Popup that's being opened, is added to the list of loaded Popups.

Add the following highlighted code to the TalkService#createPopup method:

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 });
   this.loadedPopups.push(popup);

   return popup;
}

The last thing we should do in the TalkService now, is make a method that actually destroys all the loaded Popups.

Add the following method:

destroyAllLoadedPopups() {
   if (this.loadedPopups.length > 0) {
      this.loadedPopups.forEach(p => p.destroy());
      this.loadedPopups = [];
   }
}

Add the following method call to the `InboxComponent#createInbox`:

private async createInbox() {
   this.inbox = await this.talkService.createInbox();
   this.inbox.mount(document.getElementById('talkjs-container'));

   this.talkService.destroyAllLoadedPopups();
}

There are a lot more things you can customize about TalkJS, such as custom chat UI themes, different languages and so on.

Take a look at our documentation to read more about our customization possibilities.

If you have a question, don't hesitate to drop by our support chat.