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.
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 namedis-typing
. This is done by creating a folder and copy-pasting all the required files for a basic project including the dependencies. Theblank
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 port8100
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 Screen
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:
- Invoke modal
- Initialize deepstream
- Handle new chat messages
- Render chat messages
- 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>
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.
Sample Code.
Hi Christian, can you please let me know how it will be implemented in Angularfire2 in IONIC3
Hi Christian, I followed your tutorial but I am simply getting a blank page.