Write a post

How to Implement "Is Typing" Feature in Ionic Chat App

Published Apr 24, 2017Last updated Apr 26, 2017
How to Implement "Is Typing" Feature in Ionic Chat App

Just like you, growing up I wondered what magic is behind the "is typing" notice that appears on most chat apps when a friend I'm texting starts typing. For fun’s sake, I decided to implement this feature with the easiest tools I could lay my hands on.

Demo Image

Ionic will be our platform for making the mobile chat app, while deepstream will serve as the tool for very fast realtime data transfer.

Let's get started.

Create An Ionic Project

Ionic project takes insignificant time to setup. This is as a result of the CLI tool provided by the Ionic team to help scaffolding new projects easy. First, you will need to install this CLI tool, then you can use the tool to generate a new project:

#1. Install CLI too
npm install -g ionic
#2. Scaffold new project
ionic start is-typing blank --v2
#3. Enter project directory
cd is-typing
#4. Start App
ionic serve -l
  • The first command installs Ionic using Node. It is installed globally with the g flag so as to have the CLI tools available in your system.
  • The start command creates a new project named is-typing. This is done by creating a folder and copy-pasting all the required files for a basic project including the dependencies. The blank option specifies which template we want to start with while --v2 tells the installer to scaffold with Ionic 2 not Ionic 1.
  • The serve command starts the app at port 8100

Installing deepstream

deepstreamHub is a realtime server that offers fast realtime data transfer. You can create an account, grab your connection URL from the dashboard and connect your app to it.

With an account created, you need a way to interact with the server. This is where deepstream's clients come in. Client SDKs are open sourced for you to easily interact with the server. You can install the JavaScript SDK via npm or include a script tag. npm is always better but for simplcity, let's just download and add the script source to our index.html:

<!--./www/index.html-->
<script src="deepstream.min.js"></script>

App Screens

We need to prepare two screens for our app -- a home page where the chat happens and a modal that is presented to new users to provide their credentials before joining the chat.

The home page qualifies to be a page but the modal can just be simple component. There is not much difference between these two, just the way they are treated by Ionic.

Modal Image

Before working on the home page which is where our chat lives, let's first give users identity by requesting their username and email via a modal. Password is not necessary, it's an open chat group.

Create a new folder, components in the src directory. Inside the new components folder, add a new username-modal component:

// ./src/components/username-modal.component.ts
import { Component } from '@angular/core';
import { ViewController } from 'ionic-angular';

@Component({
  selector: 'username-modal',
  templateUrl: 'username-modal.component.html'
})
export class UsernameModal {
  model = {
    username: '',
    email: ''
  }; 

  constructor(public viewCtrl: ViewController) {}

  dismiss() {
    this.viewCtrl.dismiss(this.model);
  }
}

It is up to the ViewController to manage the modal, which is why it is being injected in the constructor. When the dismiss method is invoked via the constructor, the modal is dismissed and model (being the form data) will be sent to the Home page component.

The template is comprised of basic form controls for the username and email as well as a button to invoke dismiss:

<ion-header>
  <ion-navbar>
    <ion-title>
      Join Chat Room
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-list>
    <ion-item>
        <ion-label>Username</ion-label>
        <ion-input type="text" [(ngModel)]="model.username"></ion-input>
    </ion-item>

    <ion-item>
        <ion-label>Avatar Email</ion-label>
        <ion-input type="email" [(ngModel)]="model.email"></ion-input>
    </ion-item>

  </ion-list>

  <div padding>
    <button block ion-button (click)="dismiss()">Join</button>
  </div>
</ion-content>

When your app reloads, nothing different happens, because the modal is not being invoked yet. We have handled dismissal but not initialization. Initialization can be taken care of by the parent component which is Home. Let's move our spotlight to the Home component.

Home Page Screen

The home page screen is expected to:

  1. Invoke modal
  2. Initialize deepstream
  3. Handle new chat messages
  4. Render chat messages
  5. Implement "is typing" feature

Let's start with invoking the modal.

Invoking Modal

In the previous section, we created a modal, but this modal cannot invoke itself. The Home Page component should:

// ./src/pages/home/home.ts
import { Component, OnInit } from '@angular/core';
import { NavController, ModalController } from 'ionic-angular';
import md5 from 'blueimp-md5';

import { UsernameModal } from '../../components/username-modal.component'

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage implements OnInit {
  
  user: any;

  constructor(
    public modalCtrl: ModalController
    ) {
  }

  ngOnInit() {
    this.presentModal()
  }

  presentModal() {
    const usernameModal = this.modalCtrl.create(UsernameModal);
    usernameModal.onDidDismiss(data => {
     // Update user property
     this.user = Object.assign(
       {}, 
       data, 
       {avatar: `https://s.gravatar.com/avatar/${md5(data.email.trim().toLowerCase())}?s=200.jpg`}
       )
   });
    usernameModal.present();
  }
}

The presentModal method creates a modal based off the UsernameModal.

Thereafter, an onDidDismiss event is attached to listen for when the dismiss method is called in the modal component. When that happens, we update the user property with whatever information comes in from the UsernameModal component. One other interesting thing here is that we are fetching the avatar using Gravatar based on the hashed email.

After setting up the event, the present method is called to present this modal.

In our case, we expect no button click to invoke this modal, we just want to invoke it once the app is launched. Therefore, we execute the method in the ngOnInit lifecycle method.

The ModalController exposes APIs for interacting with our UserModal which is why it is being injected above.

Initialize deepstream

Next up, we need to setup deepstream client to enable us communicate with our realtime server. First we need to declare deepstream as global so Typescript doesn't shout at us with errors:

// ./src/pages/home/home.ts
...
declare var deepstream;

Then you can connect to the server using the app url on your dashboard:

// ./src/pages/home/home.ts
export class HomePage implements OnInit {
  
  user: any;
  client: any;

  constructor(
    public modalCtrl: ModalController
    ) {
  }

  ngOnInit() {
    this.presentModal()
    this.client = deepstream('<APP-URL-HERE>');
    this.client.login()
    this.client.on('error', (err) => { console.log(err) })
  }

  presentModal() {
    ...
  }

}

The login method opens the connection to the server. Errors could occur during the lifecycle of this connection so it becomes important to handle these errors. We are doing so by listening to the error event and logging to the console.

New Chat Messages

Let's create a form to create new chat messages and use deepstream events to handle the new message updates:

import moment from 'moment'


@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage implements OnInit {
  
  user: any;
  client: any;
  model: any = {
    text: 'Hi :)',
    time: null
  };

  // ... other members of the class
  
  send() {
  this.model.time = moment().format('h:mm a');
    const payload = Object.assign({}, this.user, this.model);
    this.client.event.emit('chat:new', payload);
    this.model.text = '';
  }
  
}

The idea is: when a send button is clicked in the view, we emit a deepstream event called chat:new with the user, text and time as payload. The time is formatted using the most popular time library, moment.

The text input is emptied after the event is emitted to make room for a new message.

Let's see the template implementation:

<ion-header>
  <ion-navbar>
    <ion-title>
      {{user?.username}}
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <!--
  List of messages will be here.
  See next section
  -->
  <ion-grid>
    <ion-row class="msg-box">
      <ion-col col-9>
        <ion-item>
          <ion-input type="text" placeholder="Text..." [(ngModel)]="model.text"></ion-input>
        </ion-item>
      </ion-col>
      <ion-col col-3>
        <button block ion-button (click)="send()">Send</button>
      </ion-col>
    </ion-row>
  </ion-grid>
</ion-content>

The text property of the model is bound to the input, likewise the send method to the send button.

Let's now see what happens to the new messages after emitting them.

Render Chat Messages

We can render the list of chat messages by subscribing to the emitted event and updating a UI property based on this event:

export class HomePage implements OnInit {
  
  user: any;
  client: any;
  chats: any = [];
  model: any = {
    text: 'Hi :)',
    time: null
  };

  constructor(
    public navCtrl: NavController,
    public modalCtrl: ModalController
    ) {
  }

  ngOnInit() {
    ...
    this.client.event.subscribe('chat:new', (payload) => {
      this.chats.push(payload);
    })
  }

  send() {
    this.model.time = moment().format('h:mm a');
    const payload = Object.assign({}, this.user, this.model);
    this.client.event.emit('chat:new', payload);
    this.model.text = '';
  }
}

We added a chats property which is an array. Then inside the ngOnInit lifecycle method, we subscribe to the chat:new event where we push chats to the chats array when they are emitted.

You can iterate over the chat list and bind them to the view as follows:

<ion-header>
  <ion-navbar>
    <ion-title>
      {{user?.username}}
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-list class="cards">
    <ion-card *ngFor="let chat of chats">

      <ion-item>
        <ion-avatar item-left>
          <img src="{{chat.avatar}}">
        </ion-avatar>
        <h2>{{chat.username}}</h2>
        <p>{{chat.time}}</p>
      </ion-item>

      <ion-card-content>
        {{chat.text}}
      </ion-card-content>

    </ion-card>
  </ion-list>

  <!--
  Chat message box is here
  -->
</ion-content>

"is typing" Feature

The trick behind this feature is to emit realtime events on keystrokes and update the UI with the "who is typing" text. This text will stick around forever even when the user has stopped time so you can use setTimeout to remove the content from the view after a given period of time.

First let's split the model.text binding to property and events binding:

<ion-input type="text" placeholder="Text..." [ngModel]="model.text" (keypress)="onChange($event)"></ion-input>

This way we can have control over the keypress event:

export class HomePage implements OnInit {

  typing = '';

  ngOnInit() {
   ...
    
    // Handle is typing event
    this.client.event.subscribe('chat:typing', (payload) => {
      if(payload.username !== this.user.username) {
        this.typing = payload.username + ' is typing...'
        setTimeout(() => {
          this.typing = ''
        }, 2000)
      }
    })
  }

  onChange(e) {
    this.model.text = e.target.value;
    this.client.event.emit('chat:typing', this.user);
  }

}

When the user A (e.g. Ada) starts typing, we tell every other users that Ada is typing. We don’t tell Ada that she is typing, because it would be unnecessary. We do this by comparing the usernames using the if logic.

After 2 seconds, we reset the typing property back to an empty string.

Let's bind the text to our view:

<p>{{typing}}</p>

App Screenshot

Conclusion

Hopefully you had fun trying to build his chat app with the "who is typing" feature. A lot of web concepts are being demystified these days, and Ionic is doing a great job for mobile developers, just as deepstreamHub is doing a fantastic job for realtime engineers. Feel free to contact me or make comments on your views about the implementation in this article.

Discover and read more posts from Christian Nwamba
get started
Enjoy this post?

Leave a like and comment for Christian

5
1
Parse Dashboard on Heroku in 3 Steps
5 Steps to Authenticating Node.js with JWT
How does one keep up with all the new JS libs/frameworks coming out