We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Add typing indicators and file attachments to your Angular 7 chat app

  • Ayooluwa Isaiah
December 14th, 2018
You will need Node 6+ installed on your machine. Some prior experience with Angular will be helpful.

This tutorial is a continuation of the How to build a chatroom with Angular 7 and Chatkit one, so you need to complete that first before moving on to this one. You can clone this GitHub repository and follow the instructions in the README file to get set up.

For the most part, aside from being able to send text messages, users also expect to be able to share images, documents and other files in a chat session. Typing indicators are also commonplace in most mainstream chat applications, and users might expect yours to have that too. Chatkit makes it really easy to incorporate these two features as you’ll see.

Update the CSS

I’ve made a few changes to client/src/app/app.component.css to account for the new features we’ll be adding to the application. Before you continue, change the contents of app.component.css to look like this:

    // client/src/app/app.component.css

    html {
      box-sizing: border-box;
    }

    *, *::before, *::after {
      box-sizing: inherit;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
    }

    .App {
      width: 100%;
      max-width: 960px;
      height: 90vh;
      margin: 0 auto;
      display: flex;
      border: 1px solid #ccc;
      margin-top: 30px;
      color: #333;
    }

    ul {
      list-style: none;
    }

    .sidebar {
      flex-basis: 30%;
      flex-shrink: 0;
      background-color: #300d4f;
      color: #fff;
      padding: 5px 10px;
    }

    .sidebar section {
      margin-bottom: 20px;
    }

    .sidebar h2 {
      margin-bottom: 10px;
    }

    .user-list li {
      margin-bottom: 10px;
      font-size: 16px;
      display: flex;
      align-items: center;
    }

    .presence {
      display: inline-block;
      width: 20px;
      height: 20px;
      background-color: #fff;
      margin-right: 10px;
      border-radius: 50%;
    }

    .presence.online {
      background-color: green;
    }

    .chat-window {
      display: flex;
      flex-grow: 1;
      flex-direction: column;
      justify-content: space-between;
    }

    .chat-window > * {
      padding: 10px 20px;
    }

    .chat-header, .chat-footer {
      display: flex;
      align-items: center;
    }

    .chat-header {
      border-bottom: 1px solid #ccc;
    }

    .chat-session {
      height: calc(90vh - 170px);
      overflow-y: auto;
    }

    .message-list {
      display: flex;
      flex-direction: column;
      justify-content: flex-end;
    }

    .user-message {
      margin-top: 10px;
    }

    .user-message span {
      display: block;
    }

    .user-id {
      font-weight: bold;
      margin-bottom: 3px;
    }

    .image-attachment {
      max-width: 400px;
      margin-bottom: 5px;
    }

    .chat-footer {
      display: flex;
      flex-direction: column;
    }

    .chat-footer {
      border-top: 1px solid #ccc;
    }

    .message-form {
      display: flex;
      flex-direction: column;
      width: 100%;
    }

    .message-form > div {
      display: flex;
    }

    .chat-footer input {
      padding: 10px;
    }

    .message-input {
      flex-grow: 1;
    }

    .send-message {
      background-color: #300d4f;
      color: #fff;
      border: 1px solid #300d4f;
      padding: 10px;
    }

    .is-typing {
      text-align: left;
    }

Add realtime typing indicators

Typing indicators are a staple amongst all the most common chat applications out there, but they can be difficult to build on your own. Adding support for typing indicators with Chatkit is effortless in comparison.

Let’s start by updating the app.component.html file to look like this:

    // client/src/app/app.component.html

    <div class="App">
      <aside class="sidebar">
        <section *ngIf="!currentUser" class="join-chat">
          <h2>Join Chat</h2>
          <form (ngSubmit)="addUser()">
            <input placeholder="Enter your username" type="text" name="username" [(ngModel)]="username" />
          </form>
        </section>
        <section class="online-members">
          <h2>Room Users</h2>
          <ul class="user-list">
            <li *ngFor="let user of users">
              <span class="presence {{ user.presence.state }}"></span>
              <span>{{ user.name }}</span>
            </li>
          </ul>
        </section>
      </aside>
      <main class="chat-window">
        <header class="chat-header">
          <h1>Chat</h1>
          <span class="participants"></span>
        </header>
        <section class="chat-session">
          <ul class="message-list">
            <li class="user-message" *ngFor="let message of messages">
              <span class="user-id">{{ message.senderId }}</span>
              <span>{{ message.text }}</span>
            </li>
          </ul>
        </section>
        <footer class="chat-footer">
          <div class="is-typing" *ngIf="usersWhoAreTyping.length > 0">
            {{ usersWhoAreTyping.slice(0, 2).join(' and ') }} is typing
          </div>
          <form class="message-form" (ngSubmit)='sendMessage()'>
            <input placeholder="Type a message. Hit Enter to send" type="text" name="message" [(ngModel)]="message" />
          </form>
        </footer>
      </main>
    </div>

The piece of code that we’ve added above allows us to show a message above the message input field if a user is typing. The logic that controls this feature can be found in app.component.ts as shown below:

    // client/src/app/app.component.ts

    import { Component } from '@angular/core';
    import Chatkit from '@pusher/chatkit-client';
    import axios from 'axios';

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

    export class AppComponent {
      title = 'Angular Chatroom';
      messages = [];
      users = [];
      currentUser: any;
      currentRoom = <any>{};
      usersWhoAreTyping = [];

      _username: string = '';
      get username(): string {
        return this._username;
      }

      set username(value: string) {
        this._username = value;
      }

      _message: string = '';
      get message(): string {
        return this._message;
      }

      set message(value: string) {
        this.sendTypingEvent();
        this._message = value;
      }

      sendMessage() {
        const { message, currentUser } = this;
        currentUser.sendMessage({
          text: message,
          roomId: '<your room id>',
        });
        this.message = '';
      }

      sendTypingEvent() {
        const { currentUser, currentRoom } = this;
        currentUser
        .isTypingIn({ roomId: currentRoom.id });
      }

      addUser() {
        const { username } = this;
        axios.post('http://localhost:5200/users', { username })
          .then(() => {
            const tokenProvider = new Chatkit.TokenProvider({
              url: 'http://localhost:5200/authenticate'
            });
            const chatManager = new Chatkit.ChatManager({
              instanceLocator: '<your instance locator>',
              userId: username,
              tokenProvider
            });

            return chatManager
              .connect()
              .then(currentUser => {
                currentUser.subscribeToRoom({
                  roomId: '<your room id>',
                  messageLimit: 100,
                  hooks: {
                    onMessage: message => {
                      this.messages.push(message);
                    },
                    onPresenceChanged: (state, user) => {
                      this.users = currentUser.users.sort((a) => {
                        if (a.presence.state === 'online') return -1;
                        return 1;
                      });
                    },
                    onUserStartedTyping: user => {
                      this.usersWhoAreTyping.push(user.name);
                    },
                    onUserStoppedTyping: user => {
                      this.usersWhoAreTyping = this.usersWhoAreTyping.filter(username => username !== user.name);
                    }
                  },
                })
                .then(currentRoom => {
                  this.currentRoom = currentRoom;
                });

                this.currentUser = currentUser;
                this.users = currentUser.users;
              });
          })
            .catch(error => console.error(error))
      }
    }

When a user is typing, we call the sendTypingEvent() method which invokes currentUser.userIsTyping. Then we listen for the userStartedTyping and userStoppedTyping events which allows us to update the usersWhoAreTyping array and show a message in the chatroom indicating which users are typing.

Don’t forget to update <your room id> and <your instance locator> with the appropriate credentials from your Chatkit instance before running the code. Here’s how it looks like in practice:

Send file attachments

We can easily configure our chat app to support attachments with Chatkit. In this case, I’ll show you how to add image attachments to this application in a few simple steps.

Update your app.component.html file to look like this:

    // client/src/app/app.component.html

    <div class="App">
      <aside class="sidebar">
        <section  *ngIf="!currentUser" class="join-chat">
          <h2>Join Chat</h2>
          <form (ngSubmit)="addUser()">
            <input placeholder="Enter your username" type="text" name="username" [(ngModel)]="username" />
          </form>
        </section>
        <section class="online-members">
          <h2>Room Users</h2>
          <ul class="user-list">
            <li *ngFor="let user of users">
              <span class="presence {{ user.presence.state }}"></span>
              <span>{{ user.name }}</span>
            </li>
          </ul>
        </section>
      </aside>

      <main class="chat-window">
        <header class="chat-header">
          <h1>Chat</h1>
          <span class="participants"></span>
        </header>
        <section class="chat-session">
          <ul class="message-list">
            <li class="user-message" *ngFor="let message of messages">
              <span class="user-id">{{ message.senderId }}</span>
              <img *ngIf="message.attachment"
                class="image-attachment"
                src="{{ message.attachment.link }}"
                alt="{{ message.attachment.name }}"
              />
              <span>{{ message.text }}</span>
            </li>
          </ul>
        </section>
        <footer class="chat-footer">
          <div class="is-typing" *ngIf="usersWhoAreTyping.length > 0">
            {{ usersWhoAreTyping.slice(0, 2).join(' and ') }} is typing
          </div>
          <form class="message-form" #form (ngSubmit)='sendMessage()'>
            <div>
              <input class="message-input" placeholder="Type a message. Hit Enter to send" type="text" name="message" [(ngModel)]="message" />
              <button class="send-message" type="submit">Send</button>
            </div>

            <input
            class="file-input"
              type="file"
              name="attachment"
              accept="image/png, image/jpeg"
              (change)="fileChangedHandler($event)"
            />
          </form>
        </footer>
      </main>
    </div>

We’ve added a new file input tag that allows us to pick a JPEG or PNG image from the file system, and a send button to submit the form. Also notice the new img tag that has been added to conditionally render an image if one is present in the message.

The next step it to tie up the logic in app.component.ts:

    // client/src/app/app.component.ts

    import { Component, ViewChild } from '@angular/core';
    import Chatkit from '@pusher/chatkit-client';
    import axios from 'axios';

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

    export class AppComponent {
      title = 'Angular Chatroom';
      messages = [];
      users = [];
      currentUser: any;
      currentRoom = <any>{};
      usersWhoAreTyping = [];
      attachment = null;

      @ViewChild('form') form;

      _username: string = '';
      get username(): string {
        return this._username;
      }

      set username(value: string) {
        this._username = value;
      }

      _message: string = '';
      get message(): string {
        return this._message;
      }

      set message(value: string) {
        this.sendTypingEvent();
        this._message = value;
      }

      reset() {
        this.form.nativeElement.reset()
      }

      fileChangedHandler(event) {
        const file = event.target.files[0];
        this.attachment = file;
      }

      sendMessage() {
        const { message, currentUser, attachment } = this;

        if (message.trim() === '') return;

        const messageObj = <any>{
          text: message,
          roomId: '<your room id>',
        };

        if (attachment) {
          messageObj.attachment = {
            file: attachment,
            name: attachment.name,
          };
        }

        currentUser.sendMessage(messageObj);
        this.reset();
        this.attachment = null;
      }

      sendTypingEvent() {
        const { currentUser, currentRoom } = this;
        currentUser
        .isTypingIn({ roomId: currentRoom.id });
      }

      addUser() {
        const { username } = this;
        axios.post('http://localhost:5200/users', { username })
          .then(() => {
            const tokenProvider = new Chatkit.TokenProvider({
              url: 'http://localhost:5200/authenticate'
            });
            const chatManager = new Chatkit.ChatManager({
              instanceLocator: '<your instance locator>',
              userId: username,
              tokenProvider
            });
            return chatManager
              .connect()
              .then(currentUser => {
                currentUser.subscribeToRoom({
                  roomId: '<your room id>',
                  messageLimit: 100,
                  hooks: {
                    onMessage: message => {
                      this.messages.push(message);
                    },
                    onPresenceChanged: (state, user) => {
                      this.users = currentUser.users.sort((a) => {
                        if (a.presence.state === 'online') return -1;
                        return 1;
                      });
                    },
                    onUserStartedTyping: user => {
                      this.usersWhoAreTyping.push(user.name);
                    },
                    onUserStoppedTyping: user => {
                      this.usersWhoAreTyping = this.usersWhoAreTyping.filter(username => username !== user.name);
                    }
                  },
                })
                .then(currentRoom => {
                  this.currentRoom = currentRoom;
                });

                this.currentUser = currentUser;
                this.users = currentUser.users;
              });
          })
            .catch(error => console.error(error))
      }
    }

Here, we are leveraging Angular’s ViewChild decorator. ViewChild allows us to query our view to get an element or directive of our choice. In this case, we want a ViewChild for our message form.

In the AppComponent class, we’ve set a ViewChild property to our form’s template variable (@ViewChild('form') form)so that we can call this.form.nativeElement.reset() once the Send button is clicked which has the effect of resetting the text and file inputs.

To send an attachment, we only need to grab the file and append it to the message object via the attachment property. The name property will be used as the name of the file that is stored by the Chatkit servers.

If you pasted the code above into your editor, remember to change <your room id> and <your instance locator> as appropriate.

At this point, you should be able to send image attachments seamlessly. Note that attachments must be accompanied by a message at this time.

Wrap up

In this tutorial, we explored two additional features supported by Chatkit and added them to our application without writing a ton of code. Chatkit has a small but powerful API that manages all our chat data for us so that all we have to do is take that data and render it for the user. To look at the other features Chatkit it has to offer, feel free to visit its documentation page. Also, don't forget to grab the complete source code in this GitHub repository.

Clone the project repository
  • Angular
  • JavaScript
  • Node.js
  • Chat
  • Chatkit

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.