Verbal communication is an essential part of human life. Today, technology brings the possibility of much richer communication channel then standard text-chat to our browsers. With CometChat you can build your audio and Video chat application on your website. Today you will integrate CometChat with Angular application.

There is a companion repository created for this post, available on the GitHub.

Getting started

First things first. In the catalog where you want your project to be located, type in the following Angular CLI command, which creates Angular project boilerplate:

ng new comet-chat-typing-indicator --style scss --routing true

And install @cometchat-pro/chat library from npm:

npm install @cometchat-pro/chat

Customize the global stylesheet by editing src/styles.scss file and place there following content:

html {
    height: 100%;
}

body {
    background: #1B46D5;
    font-family: 'Roboto', sans-serif;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

Now navigate into just created file, and replace the content of the src/app/app.component.html file with following HTML syntax:

<router-outlet></router-outlet>

Add some styling to the src/app/app.component.scss file:

:host {
    margin: 0 auto;
    background: white;
    width: calc(100% - 20px);
    height: 500px;
    max-width: 680px;
    display: block;
    border-radius: 6px;
}

Now it’s time to store your application ID and API Key in the environments/environment.ts file:

export const environment = {
 production: false,
 appId: 'Your app ID',
 apiKey: 'Your API Key'
};

The application is going to have two main components: LoginComponent and ChatComponent.

ChatComponent is the place where a logged in user will be brought to display a group chat window. If the user is not authenticated, he needs to provide login and password in the LoginComponent. Information about user authentication will be provided by the CometChatService. Generate them using the following commands:

ng g c Login
ng g c Chat
ng g s CometChat

Let’s start from the implementation of the CometChatService functionalities. Replace the content of the src/app/comet-chat.service.ts file with the following code:

import { Injectable } from '@angular/core';
import { CometChat } from "@cometchat-pro/chat"
import { environment } from 'src/environments/environment';
import { Observable, ReplaySubject, Subject, from } from 'rxjs';
import { filter, flatMap, tap } from 'rxjs/operators';
@Injectable({
 providedIn: 'root'
})
export class CometChatService {
 private initialized: Subject<boolean> = new ReplaySubject();
 private signedIn$: Subject<string> = new ReplaySubject();
 private _signedIn: boolean = false;
 constructor() {
   CometChat.init(environment.appId).then(_ => {
     console.log('Comet Chat initialized.');
     this.initialized.next(true);
   }, error => {
     console.log('Initialization error: ' + error);
   });
  }
 public login(uid: string): Observable<any> {
   uid = uid.toLowerCase();
   return this.initialized.pipe(filter(v => v), flatMap(() => {
     return from(CometChat.login(uid, environment.apiKey)).pipe(tap((user: any) => {
       this.signedIn$.next(uid);
       this._signedIn = true;
     }));
   }));
  } 
 public getSignedIn(): Observable<string> {
   return this.signedIn$;
 }
 public isSignedIn(): boolean {
  return this._signedIn;
 }
}

With this basic functionality of the CometChatService (authenticating user and checking if he is authenticated) we can build up an AuthenticationGuard to protect the ChatComponent against unauthorized access. Generate the AuthenticationGuard:

ng g g auth --implements CanActivate

Now, you can implement the Authentication Guard which will check if user is authenticated every time he is trying to navigate into the application. Replace the content of the src/app/auth.guard.ts file with following code:

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { CometChatService } from './comet-chat.service';
@Injectable({
 providedIn: 'root'
})
export class AuthGuard implements CanActivate {
 constructor(private chat: CometChatService, private router: Router) {}
 canActivate(): boolean {
   if (!this.chat.isSignedIn()) {
     this.router.navigate(['login']);
   }
   
   return this.chat.isSignedIn();
  }
}

Now it’s more than ready to use it in the app navigation. Place following content in the src/app/app-routing.module.ts:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ChatComponent } from './chat/chat.component';
import { AuthGuard } from './auth.guard';
const routes: Routes = [
  {path: '', redirectTo: 'chat', pathMatch: 'full'},
  {path: 'login', component: LoginComponent },
  {path: 'chat', component: ChatComponent, canActivate: [AuthGuard]}
]
@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Ok, let’s give the user ability to login. Paste following code representing login form, to the src/login/login.component.ts file:

import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { CometChatService } from '../comet-chat.service';
import { Router } from '@angular/router';
@Component({
 selector: 'app-login',
 templateUrl: './login.component.html',
 styleUrls: ['./login.component.scss']
})
export class LoginComponent {
 public loginForm = new FormGroup({
   uid: new FormControl('', Validators.required)
 });
 constructor(private chat: CometChatService, private router: Router) {}
  public signIn(): void {
   this.chat.login(this.loginForm.value['uid']).subscribe(signedUser => {
     this.router.navigate(['chat']);
   });
  }
}

And following HTML template in the src/app/login/login.component.html

<h1>Welcome Back</h1>
<p>To access this demo, you can use one of the following four users: superhero1, superhero2, superhero3, or superhero4</p>
<form [formGroup]="loginForm" (ngSubmit)="signIn()">
    <input type="text" formControlName="uid" placeholder="Username" />
    <button type="submit" [disabled]="!loginForm.valid">Log In</button>
</form>

You can also add some styling, to make it prettier. Paste following CSS into src/app/login/login.component.scss

:host {
    color: #2D313F;
    padding: 15px;
    display: block;
}
input {
    border: 1px solid #BBBEBE;
    width: 50%;
    height: 100%;
    border-radius: 6px 6px 6px 6px;
    font-size: 16px;
    padding: 10px 29px;
    outline: none;
    box-shadow: none;
    color: #2D313F;
}
input::placeholder {
    color: #BBBEBE;
}
button {
    background: #1B46D5;
    border-radius: 6px 6px 6px 6px;
    font-size: 16px;
    padding: 10px 29px;
    color: #ffffff;
    margin-left: 10px;
    text-transform: uppercase;
}
button:hover {
    cursor: pointer;
}

The last thing to do in this step is to add a new import to the src/app/app.module.ts, in the imports array add ReactiveFormsModule, imported from the @angular/forms library:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { ChatComponent } from './chat/chat.component';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    ChatComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now, after navigating to the localhost:4200, the login page should be displayed:

If you haven't been coding along and want to catch up to this step using the companion repository on GitHub, execute the following commands:

git clone https://github.com/maciejtreder/comet-chat-angular.git
cd comet-chat-angular
git checkout step1
npm install

Implementing the chat functionality

Let’s start the implementation of the main functionality. First thing first, add a new fields to the src/app/comet-chat.service.ts:

 private _whoIsTypingArr: string[] = [];
 private messages$: Subject<any> = new ReplaySubject();
 private actualImage: string;
 private whoIsTyping$: Subject<string[]> = new BehaviorSubject([]);

Remember to add BehaviorSubject to the import statement from the rxjs library.

It’s time to add some CometChat listeners. We will do that at the moment when user is logging into the application. Replace the login method with following new implementation:

  public login(uid: string): Observable<any> {
    uid = uid.toLowerCase();
    return this.initialized.pipe(filter(v => v), flatMap(() => {
      return from(CometChat.login(uid, environment.apiKey)).pipe(tap((user: any) => {
        this.signedIn$.next(uid);
        this._signedIn = true;
        this.actualImage = user.avatar;
 
        CometChat.addMessageListener('messageListener', new CometChat.MessageListener({
          onTextMessageReceived: message => {
            this.messages$.next({name: message.sender.name, image: message.sender.avatar, message: message.text, arrived: uid !== message.sender.uid});
          },
          onTypingStarted: (who) => {
            if(this._whoIsTypingArr.indexOf(who.sender.name) > -1)
             return;
            this._whoIsTypingArr.push(who.sender.name);
            this.whoIsTyping$.next(this._whoIsTypingArr);
           },
          onTypingEnded: (who) => {
            this._whoIsTypingArr.splice(this._whoIsTypingArr.findIndex(val => val === who.sender.name), 1);
            this.whoIsTyping$.next(this._whoIsTypingArr);
           }
        }));
      }));
    }));
   } 

Implement methods which will be responsible for sending message to the group and retrieve messages in the component. For the purpose of that method you will use the CometChat library:

 public sendMessage(content: string): void {
  this.messages$.next({image: this.actualImage, message: content, arrived: false});
   
  let message = new CometChat.TextMessage('supergroup', content, CometChat.MESSAGE_TYPE.TEXT, CometChat.RECEIVER_TYPE.GROUP);
   CometChat.sendMessage(message).catch(console.log);
 }

 public getMessages(): Observable<any> {
   return this.messages$;
 }

Add also methods which will initialize typing, finish typing and publish to the component list of actually typing users:

 public startTyping(): void {
  CometChat.startTyping(new CometChat.TypingIndicator('supergroup', CometChat.RECEIVER_TYPE.GROUP, {}));
 }
 public endTyping(): void {
  CometChat.endTyping(new CometChat.TypingIndicator('supergroup', CometChat.RECEIVER_TYPE.GROUP, {}));
 }
 public getTypingIndicator(): Observable<any> {
   return this.whoIsTyping$;
 }

Now you can consume the new implementation of the CometChatService in the ChatComponent and nested components. Replace the code inside src/app/chat/chat.component.ts with the following code:

import { Component } from '@angular/core';
import { CometChatService } from '../comet-chat.service';
import { Observable } from 'rxjs';
@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  styleUrls: ['./chat.component.scss']
})
export class ChatComponent {
  public loggedIn: Observable<string> = this.chatService.getSignedIn();
  constructor(private chatService: CometChatService) {}
}

Adjust the template, in the src/app/chat/chat.component.html:

<div>You are logged in as: {{loggedIn | async}}</div>
<app-chat-window></app-chat-window>
<app-message-box></app-message-box>

And add some styles in the src/app/chat/chat.component.scss:

a:hover {
    cursor: pointer;
    text-decoration: none;
}
a {
    text-decoration: underline;
}
div {
    padding: 0 28px;
    font-size: 15px;
    line-height: 18px;
    display: flex;
    align-items: center;
    height: 61px;
    color: #2D313F;
    border-radius: 6px 6px 0 0;
    background: #FFFFFE;
}

As you can see, we introduced two new components MessageBoxComponent and ChatWindowComponent. Generate them with the CLI by using following command:

ng g c chatWindow
ng g c messageBox

Start from implementing the MessageBoxComponent it would be responsible for sending typed message to the supergroup of superheroes. Replace the code inside src/app/message-box/message-box.component.ts with following

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { CometChatService } from '../comet-chat.service';
import { tap } from 'rxjs/operators';
import { Subject, BehaviorSubject } from 'rxjs';
@Component({
  selector: 'app-message-box',
  templateUrl: './message-box.component.html',
  styleUrls: ['./message-box.component.scss']
})
export class MessageBoxComponent implements OnInit {
  public messageForm = new FormGroup({
    message: new FormControl('', Validators.required)
  });
  public touched: Subject<boolean> = new BehaviorSubject(false);
  constructor(private chatService: CometChatService) { }
  public send():void {
    this.chatService.sendMessage(this.messageForm.value['message']);
    this.chatService.endTyping();
    this.messageForm.reset();
  }

  ngOnInit() {
    this.messageForm.valueChanges.pipe(
      tap(() => this.chatService.startTyping()),
    ).subscribe();
  }
}

Whenever user will type something into the form described here, the startTyping method on the CometChatService will be fired and inform other users that something is going on.

Create template in src/app/message-box/message-box.component.html:

<form [formGroup]="messageForm" (ngSubmit)="send()">
    <input [ngClass]="{'inactive': !(touched | async)}" type="text" formControlName="message" (focus)="onFocus()" placeholder="Type something" />
</form>

And add styling to the src/app/message-box/message-box.component.scss:

form {
    height: 60px;
    width: 100%;
    display: flex;
}
input {
    width: 100%;
    height: 100%;
    border: 0;
    border-radius: 0 0 6px 6px;
    font-size: 16px;
    padding: 0 29px;
    outline: none;
    box-shadow: none;
    color: #2D313F;
}
input::placeholder {
    color: #BBBEBE;
}

Now add the last piece to our puzzles. Edit content of the src/app/chat-window.component.ts and place following code inside:

import { Component, ViewChild, ElementRef } from '@angular/core';
import { CometChatService } from '../comet-chat.service';
import { Observable } from 'rxjs';
import { scan, map, filter, tap } from 'rxjs/operators';
@Component({
  selector: 'app-chat-window',
  templateUrl: './chat-window.component.html',
  styleUrls: ['./chat-window.component.scss']
})
export class ChatWindowComponent {
  public typingIndicator: Observable<boolean> = this.chatService.getTypingIndicator().pipe(map(val => val.length > 0));
  public who: Observable<string> = this.chatService.getTypingIndicator().pipe(filter(val => val.length > 0), map(val => {
    switch(val.length) {
      case 1: return `${val[0]} is typing`;
      case 2: return `${val[0]} and ${val[1]} are typing`;
      default: return `Many poeple are typing`;
    }
  }));
  public messages: Observable<any[]> = this.chatService.getMessages().pipe(
    scan<any>((acc, curr) => [...acc, curr], [])
  );
  @ViewChild('conversation', { static: true })
  private conversationContainer: ElementRef;
  constructor(private chatService: CometChatService) {
  }
  public ngOnInit(): void {
    this.messages.pipe(tap(() => {
      this.conversationContainer.nativeElement.scrollTop = this.conversationContainer.nativeElement.scrollHeight;
    })).subscribe();
  }
}

Couple things went on here. First of all, you are retrieving the Observable with information about who is typing from the CometChatService ; later, basing on the number of typing users you are creating a new observable called who - that’s what you are going to display in the template. Last but not least observable is messages which is stream of messages sent by you and other users. For purpose of scrolling the chat window to the recent messages you are going to use conversationContainer the JavaScript scrollTop method is used inside the ngOnInit method (an Angular lifecycle hook).

Implement template which uses all of those methods and variables. Change content of the src/app/chat-window.component.html to the following:

<div class="conversation" #conversation>
    <div *ngFor="let message of messages | async" [ngClass]="{'external': message.arrived, 'internal': !message.arrived}">
            <img [src]=message.image />
            <p><span *ngIf="message.name && message.arrived">{{message.name}}</span>{{message.message}}</p>
        </div>
</div>
<div *ngIf="typingIndicator | async" class="typingIndicator">
        <span>{{who | async}}
                <div class="container">
                        <span class="dot"></span>
                        <span class="dot"></span>
                        <span class="dot"></span>
                </div>
        </span>
</div>

And finally, make the final touch by adjusting css in the src/app/chat-window.component.scss:

:host {
    position: relative;
    height: 385px;
    display: block;
    padding: 0 15px 0;
    background: #F8F9FB;
}
div.conversation {
  overflow: auto;
  height: 350px;
  padding-bottom: 35px;
    div {
      display: flex;
      flex-direction: row;
      align-items: flex-end;
    }
    div + div {
      margin-top: 20px;
    }
    div:first-child {
      margin-top: 15px;
    }
    div:last-child {
      margin-bottom: 40px;
    }
    p {
      span {
        display: block;
        font-weight: bold;
        font-size: 12px;
        color: #1B47DB;
      }
      padding: 15px;
      border-radius: 12px;
      font-size: 14px;
      line-height: 19px;
      margin: 0;
      max-width: 250px;
    }
    .internal p {
      background: #1B47DB;
      color: white;
      margin-right: 15px;
    }
    .external p {
      background: #ffffff;
      box-shadow: 0px 1px 2px rgba(189, 204, 215, 0.544307);
      margin-left: 15px;
    }
    .internal {
      flex-direction: row-reverse;
    }
}
img {
    height: 50px;
}
div.typingIndicator {
    height: 35px;
    color: #575A65;
    font-size: 13px;
    display: flex;
    align-items: center;
    position: absolute;
    bottom: 0;
    width: 100%;
    background: #F8F9FB;
    margin-left: -15px;
    padding-left: 20px;
    box-sizing: border-box;
}
.container {
  display: inline-block;
  position: relative;
  top: -1px;
  left: 3px;
}
.dot {
  height: 4px;
  width: 4px;
  border-radius: 100%;
  display: inline-block;
  background-color: #5C5E69;
  animation: 1.2s typing-dot ease-in-out infinite;
}
.dot:nth-of-type(2) {
  animation-delay: 0.15s;
  margin-left: 3px;
}
.dot:nth-of-type(3) {
  animation-delay: 0.25s;
  margin-left: 3px;
}
@keyframes typing-dot {
  15% {
    transform: translateY(-35%);
    opacity: 0.5;
  }
  30% {
    transform: translateY(0%);
    opacity: 1;
  }
}
@media (max-width: 450px) {
  img {
    display: none;
  }
  div.conversation .external p,
  div.conversation .internal p {
    margin-left: 0;
    margin-right: 0;
  }
}

Now, after navigating to localhost:4200 in two separate windows, authorizing into two different accounts, you should be able to perform a real-life test of the typing indicator:

If you haven't been coding along and want to catch up to this step using the companion repository on GitHub, execute the following commands in the directory where you’d like to create the project:

git clone https://github.com/maciejtreder/comet-chat-angular.git
cd comet-chat-angular
git checkout step2
npm install

Conclusion

Today you brought one of the most important part of human communication - voice and view - to the web! You learned how to embed CometChat in an Angular application.