Build a chat app with Ionic featuring sentiment analysis
You will need Node and npm installed on your machine. A basic understanding of Angular, Node and Ionic will be helpful.
Introduction
Sentiment Analysis examines the problem of studying texts, uploaded by users on microblogging platforms or electronic businesses. It is based on the opinions they have about a product, service or idea. Using sentiment analysis, we can suggest emojis to be used as replies to messages based on the context of the received message.
Using Ionic, you can create a mobile application using web technologies and use a wide array of existing components. Using Pusher, we can enable realtime messaging in the chat using pusher’s pub/sub pattern.
We’ll be building a realtime chat application using Pusher, Ionic and the Sentiment library for emoji suggestions based on the context of messages received.
Here’s a demo of the final product:
Prerequisites
To follow this tutorial a basic understanding of Angular, Ionic and Node.js is required. Please ensure that you have Node and npm installed before you begin.
We’ll be using these tools to build out our application:
We’ll be sending messages to the server and using Pusher’s pub/sub pattern, we’ll listen to and receive messages in realtime. To make use of Pusher you’ll have to create an account here.
After account creation, visit the dashboard. Click Create new Channels app, fill out the details, click Create my app, and make a note of the details on the App Keys tab.
Let’s build!
Setup and folder structure
We’ll initialize our project using the Ionic CLI (command line interface). First, install the CLI by running npm install -g ionic
in your terminal. NPM is a package manager used for installing packages. It will be available on your PC if you have Node installed.
To create a new Ionic project called chat-app
using the CLI, open a terminal and run:
ionic start chat-app blank
The command is simply telling the CLI to create a new project called chat-app
without a template.
Follow the prompt and integrate your app with Cordova to target IOS and Android.
Type Y to integrate Cordova into the application. The next prompt will ask if you want to integrate Ionic pro into the application. If you have a pro account type Y and N if you don’t.
The Ionic team provides three ready made starter templates. You can check out the rest of the templates here
Open the newly created folder, your folder structure should look something like this:
chat-app/
resources/
node_modules/
src/
app/
app.component.html
app.module.ts
app.scss
...
Open a terminal inside the project folder and start the application by running ionic serve
. A browser window should pop up and you should see a page like this.
Installing dependencies
Next, run the following commands in the root folder of the project to install dependencies.
// install depencies required to build the server
npm install express body-parser dotenv pusher sentiment uuid
// front-end dependencies
npm install pusher-js @types/pusher-js
Building our server
Now that we have our application running, let’s build out our server.
To do this we’ll make user of Express. Express is a fast, unopinionated, minimalist web framework for Node.js. We’ll use this to receive requests from our Angular application.
Create a file called server.js
in the root of the project and update it with the code snippet below:
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: process.env.PUSHER_CLUSTER,
encrypted: true,
});
const app = express();
const port = process.env.PORT || 4000;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
We referenced three packages in the snippet above, body-parser
, pusher
and dotenv
. Let’s get into what each one does.
- body-parser is a package used to parse incoming request bodies in a middleware before your handlers, available under the
req.body
property. - dotenv is a zero-dependency module that loads environment variables from a
.env
file into[process.env](https://nodejs.org/docs/latest/api/process.html#process_process_env)
. This package is used so sensitive information like theappId
andsecret
aren’t added to our codebase directly. - The dotenv package will load the variables provided in our
.env
file into our environment.
The dotenv
package should always be initialized very early in the application at the top of the file. This is because we need the environment variables available throughout the application.
The calls to our endpoint will be coming in from a different origin, therefore we need to make sure we include the CORS headers (Access-Control-Allow-Origin
). If you are unfamiliar with the concept of CORS headers, you can find more information here.
Also, you’ll notice that we installed Pusher library as a dependency. Visit the Pusher website to create a Pusher account if you haven’t done so already
Create a .env
file to load the variables we’ll be needing into the Node environment. The file should be in the root folder of your project. Open the file and update it with the code below.
// .env
PUSHER_APP_ID=<APP_ID>
PUSHER_KEY=<PUSHER_KEY>
PUSHER_SECRET=<PUSHER_SECRET>
PUSHER_CLUSTER=<PUSHER_CLUSTER>
P.S: Please ensure you replace the placeholder values above with your Pusher
appId
,key
,secret
andcluster
.
This is a standard Node application configuration, nothing specific to our app.
Sending messages
To enable users send and receive messages, we’ll create a route to handle incoming requests. Update your server.js
file with the code below.
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
...
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
app.post('/messages', (req, res) => {
const { body } = req;
const { text, id } = body;
const data = {
text,
id,
timeStamp: new Date(),
};
pusher.trigger('chat', 'message', data);
res.json(data);
});
...
- We created a
POST /messages
route which, when hit, triggers a Pusher event. - We used object destructuring to get the body of the request, we also got the
text
andid
in the request body sent by the user. - The
data
object will contain thetext
andid
sent by the user accompanied by a timestamp. - The trigger is achieved using the
trigger
method which takes the trigger identifier(chat
), an event name (message
), and a payload(data
). - The payload can be any value, but in this case, we have a JS object.
- The response will contain the
data
object.
Sentiment analysis
Sentiment analysis refers to the use of natural language processing, text analysis, computational linguistics, and biometrics to systematically identify, extract, quantify, and study effective states and subjective information. - Wikipedia
You can read up a bit about sentiment analysis using the following links below:
Using sentiment analysis, we’ll analyse the messages sent to determine the attitude of the sender. With the data gotten from the analysis, we’ll determine the emojis to suggest to the user.
We’ll use the Sentiment JavaScript library for analysis. The next step is to update our POST /messages
route to include analysis of the messages being sent in. Update your server.js
with the code below.
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const Sentiment = require('sentiment');
const sentiment = new Sentiment();
...
app.post('/messages', (req, res) => {
const { body } = req;
const { text, id } = body;
const result = sentiment.analyze(text);
const comparative = result.comparative;
const tone =
comparative >= 0 ? (comparative >= 1 ? 'positive' : 'neutral') : 'negative';
const data = {
text,
id,
timeStamp: new Date(),
sentiment: {
tone,
score: result.score,
},
};
pusher.trigger('chat', 'message', data);
res.json(data);
});
...
- Include the Sentiment library in the project
result
: analyze the message sent in by the user to determine the context of the message.comparative
: this is the comparative score gotten after analyzing the message. This score is used to determine if a message ispositive
,negative
orneutral
.tone
: this variable is the context of the message gotten after analysis. This will benegative
if the comparative score is below0
,neutral
if the score is above0
but below1
. The tone ispositive
if the comparative score is1
and above.- A new object(
sentiment
) property is added to the response data containing the message’s tone and score.
You can now start the server by running node server.js
in a terminal in the root folder of the project.
Chat view
Let’s build out our chat interface. We’ll create a chat
component to hold the chat interface. Create a folder called components
in the src/
directory. This folder will hold all our components.
In the components
folder, create a folder named chat
, then proceed to create three files in the chat folder. chat.ts
, chat.scss
and chat.html
.
Now let’s go ahead and update the newly created chat component files. Open the chat.html
file and update it with the code snippet below:
// src/components/chat/chat.html
<div class="main">
<div class="chat-box">
<div class="message-area">
<div class="message" *ngFor="let message of messages" [ngClass]="getClasses(message.type)">
<p>{{message.text}}</p>
</div>
</div>
<div class="emo-area">
<!-- emoji-panel component comes here -->
</div>
<div class="input-area">
<form (submit)="sendMessage()" name="messageForm" #messageForm="ngForm">
<ion-input type="text" name="message" id="message" [(ngModel)]="message" placeholder="Say something nice..."></ion-input>
<button>
<ion-icon name="send"></ion-icon>
</button>
</form>
</div>
</div>
</div>
In the code snippet above:
- We loop through the available
messages
in the.message-area
. - We have a form containing an
ion-input
element and a submit button. - We are making use of the ionicons library.
Open the chat.ts
file and update with the code below:
// src/components/chat/chat.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { v4 } from 'uuid';
interface Message {
id: string;
text: string;
timeStamp: Date;
type: string;
}
@Component({
selector: 'chat',
templateUrl: 'chat.html',
})
export class ChatComponent implements OnInit {
constructor(private http: HttpClient) {}
messages: Array<Message> = [];
message: string = '';
lastMessageId;
sendMessage() {
if (this.message !== '') {
// Assign an id to each outgoing message. It aids in the process of differentiating between outgoing and incoming messages
this.lastMessageId = v4();
const data = {
id: this.lastMessageId,
text: this.message,
};
this.http
.post(`http://localhost:4000/messages`, data)
.subscribe((res: Message) => {
const message = {
...res,
// The message type is added to distinguish between incoming and outgoing messages. It also aids with styling of each message type
type: 'outgoing',
};
this.messages = this.messages.concat(message);
this.message = '';
});
}
}
// This method adds classes to the element based on the message type
getClasses(messageType) {
return {
incoming: messageType === 'incoming',
outgoing: messageType === 'outgoing',
};
}
ngOnInit() {
}
}
sendMessage
: this method uses the native HttpClient
to make requests to the server. The POST
method takes a URL and the request body
as parameters. We then append the data returned to the array of messages. We make use of a package called [uuid](https://www.npmjs.com/package/uuid)
to give each message a unique id
.
getClasses
: this method generates classes for a message element based on the messageType
.
To make use of the HttpClient
service, we’ll need to import the HttpClientModule
and HttpClient
into the app.module.ts
file. Also, we’ll need to register our newly created component, we’ll add it to the declarations array.
// src/app/app.module.ts
...
import { ChatComponent } from '../components/chat/chat';
import { HttpClientModule, HttpClient } from '@angular/common/http';
@NgModule({
declarations: [
MyApp,
HomePage,
ChatComponent,
],
imports: [BrowserModule, IonicModule.forRoot(MyApp), HttpClientModule],
...
providers: [
StatusBar,
SplashScreen,
{ provide: ErrorHandler, useClass: IonicErrorHandler },
HttpClient,
],
})
export class AppModule {}
...
Styling
Open the chat.scss
file and update it with the styles below:
// src/components/chat/chat.scss
.main {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
.chat-box {
width: 100%;
height: 100%;
position: relative;
background: #f9fbfc;
.message-area {
max-height: 90%;
height: 90%;
overflow: auto;
padding: 15px 10px;
.message {
p {
color: #8a898b;
font-size: 13px;
font-weight: bold;
margin: 0px;
max-width: 95%;
min-width: 55%;
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
padding: 10px 15px 10px 7px;
margin: 5px 0;
background: white;
}
}
.message.incoming {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
p {
color: white;
border-radius: 0 11px 11px 11px;
background: #B9C0E9;
}
}
.message.outgoing {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
p {
border-radius: 11px 11px 0 11px;
}
}
}
.emo-area {
position: absolute;
bottom: 50px;
left: 0;
width: 100%;
padding: 3px 10px;
}
}
}
This first SCSS snippet styles the .chat-area
, including how messages should look. The next snippet will style the input area and the send button. The styles below should be nested within the .main
style.
// src/components/chat/chat.scss
.input-area {
position: absolute;
bottom: 1px;
left: 0;
width: 100%;
height: 50px;
background: white;
form {
display: flex;
height: 100%;
ion-input {
width: 82%;
border: none;
padding: 5px 10px;
color: #8a898b;
font-size: 14px;
font-weight: bold;
font-family: 'Titillium Web', sans-serif;
background: inherit;
&:focus {
outline: none;
}
}
button {
width: 18%;
border: none;
color: #8a898b;
opacity: 0.9;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: inherit;
ion-icon {
font-size: 3rem;
}
}
}
}
Let’s include the chat
component in the home page. In the pages
directory, you’ll find the home
folder, open the home.html
file in the home folder and replace the content with the snippet below:
// src/pages/home/home.html
<ion-header>
<ion-navbar color="light">
<ion-title>Chat</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<chat></chat>
</ion-content>
Visit http://localhost:8100 in your browser to view the chat interface. It should be similar to the screenshot below:
Introducing Pusher and sending messages
So far we have an application that allows users send messages but the messages aren’t delivered in realtime. To solve this problem, we’ll include the Pusher library.
Let’s create a Pusher service to be used application wide. The Angular CLI can aid in the service creation. Open a terminal in your project’s root folder and run the following command.
ionic generate provider pusher
This command simply tells the CLI to generate a provider named pusher
. Now open the pusher.ts
file in the src/providers/pusher
directory and update it with the code snippet below:
// src/providers/pusher/pusher.ts
import { Injectable } from '@angular/core';
import Pusher from 'pusher-js';
@Injectable()
export class PusherProvider {
constructor() {
var pusher = new Pusher('<PUSHER_KEY>', {
cluster: '<PUSHER_CLUSTER>',
encrypted: true,
});
this.channel = pusher.subscribe('chat');
}
channel;
public init() {
return this.channel;
}
}
- First, we initialize Pusher in the constructor.
- The
init
method returns the Pusher property we created.
Note: Ensure you replace the
PUSHER_KEY
andPUSHER_CLUSTER
string with your actual Pusher credentials.
We’ll make use of this service in our component, by binding to the message event and appending the returned message into the list of messages. This will be done in the ngOnInit
lifecycle.
// src/components/chat/chat.ts
...
import { v4 } from 'uuid';
import { PusherProvider } from '../../providers/pusher/pusher';
...
// Include the PusherProvider in the component's constructor
constructor(private http: HttpClient, private pusher: PusherProvider){}
...
ngOnInit() {
const channel = this.pusher.init();
channel.bind('message', (data) => {
if (data.id !== this.lastMessageId) {
const message: Message = {
...data,
type: 'incoming',
};
this.messages = this.messages.concat(message);
}
});
}
}
Emoji suggestions
To display emoji suggestions during a chat session, we’ll make use of the sentiment
param being sent from the server as a response for each message request. The data being sent from the server should be similar to the snippet below.
{
id: '83d3dd57-6cf0-42dc-aa5b-2d997a562b7c',
text: 'i love pusher',
timeStamp: '2018-04-27T15:04:24.574Z'
sentiment: {
tone: 'positive',
score: 3
}
}
Create an emoji
component that will hold the emoji section. This component will handle the display of emojis based on the tone of each message received. Create a folder emoji-panel
in the components
directory and in that directory, create three files, emoji-panel.ts
, emoji-panel.scss
and emoji-panel.html
Replace the contents of the emoji-panel.html
in the src/components/emoji-panel
directory with the code snippet below.
// src/components/emoji-panel/emoji-panel.html
<div class="emojis" [hidden]="!showEmojis" [attr.aria-hidden]="!showEmojis">
<div class="emoji-list positive" *ngIf="result.tone === 'positive'">
<span class="emoji" *ngFor="let emoji of emojiList.positive; let i = index;" (click)="onClick('positive', i)">
{{codePoint(emoji)}}
</span>
</div>
<div class="emoji-list neutral" *ngIf="result.tone === 'neutral'">
<span class="emoji" *ngFor="let emoji of emojiList.neutral; let j = index;" (click)="onClick('neutral', j)">
{{codePoint(emoji)}}
</span>
</div>
<div class="emoji-list negative" *ngIf="result.tone === 'negative'">
<span class="emoji" *ngFor="let emoji of emojiList.negative; let k = index;" (click)="onClick('negative', k)">
{{codePoint(emoji)}}
</span>
</div>
</div>
attr.aria-hidden
: here we set the accessibility attribute of the element to either true
or false
based on the showEmojis
variable.
Update the emoji-panel.ts
with code below:
// src/components/emoji-panel/emoji-panel.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'emoji-panel',
templateUrl: 'emoji-panel.html',
})
export class EmojiPanelComponent implements OnInit {
constructor() {}
@Input() result = {};
@Input() showEmojis: boolean = false;
@Output() onEmojiSelect: EventEmitter<string> = new EventEmitter();
emojiList = {
positive: [128512, 128513, 128536, 128516],
neutral: [128528, 128529, 128566, 129300],
negative: [128543, 128577, 128546, 128542],
};
codePoint(emojiCodePoint) {
return String.fromCodePoint(emojiCodePoint);
}
onClick(reaction, index) {
const emoji = this.emojiList[reaction][index];
this.onEmojiSelect.emit(emoji);
}
ngOnInit() {}
}
emojiList
: this is an object containing a list of unicode characters for each emoji that’ll be used. There’s a list for each message tone.
codePoint
: this method returns an emoji from the codepoint passed in. It uses String.fromCodePoint introduced in ES2015.
showEmojis
: an input variable from the parent component(chat
) to determine the visibility of the emoji panel
onClick
: this method takes to parameters. The reaction
param is used to select the list of emojis to check for the provided index
. The selected emoji is then emitted to the parent component.
Add the following styles to the emoji-panel.scss
file.
// /src/components/emoji-panel/emoji-panel.scss
.emojis {
&[aria-hidden='true'] {
animation: slideOutDown 0.7s;
}
&[aria-hidden='false'] {
animation: slideInUp 0.7s;
}
.emoji-list {
display: flex;
.emoji {
margin: 0 5px;
cursor: pointer;
}
}
}
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideOutDown {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(0, 100%, 0);
}
}
After creating the emoji-panel
component, the next step is to register it in the app.module.ts
file and then add it to our chat
component. Update the app.module.ts
file and the chat component to include the emoji-panel
.
// src/app/app.module.ts
...
import { PusherProvider } from '../providers/pusher/pusher';
import { EmojiPanelComponent } from '../components/emoji-panel/emoji-panel';
@NgModule({
declarations: [
MyApp,
HomePage,
ChatComponent,
EmojiPanelComponent
],
...
})
export class AppModule {}
Then include the emoji-panel
component in the chat.html
file.
// chat.component.html
...
<div class="main">
...
<div class="emo-area">
<emoji-panel [showEmojis]="showEmojis" [result]="score" (onEmojiSelect)="selectEmoji($event)"></emoji-panel>
</div>
<div class="input-area">
...
</div>
</div>
Let’s update the chat.ts
to display or hide the emoji-panel based on the sentiment of each message.
Open the chat.ts
file and update it like so:
// src/components/chat/chat.ts
...
messages: Array<Message> = [];
message: string = '';
lastMessageId;
showEmojis = false;
score = {
tone: '',
score: 0,
};
sendMessage() {
if (this.message !== '') {
this.lastMessageId = v4();
this.showEmojis = false;
...
}
}
selectEmoji(e) {
const emoji = String.fromCodePoint(e);
this.message += ` ${emoji}`;
this.showEmojis = false;
}
...
ngOnInit() {
const channel = this.pusher.init();
channel.bind('message', (data) => {
if (data.id !== this.lastMessageId) {
const message: Message = {
...data,
type: 'incoming',
};
this.showEmojis = true;
this.score = data.sentiment;
this.messages = this.messages.concat(message);
}
});
}
...
selectEmoji
: this method gets the emoji from the codepoint passed as a parameter and then appends the selected emoji to the current message. Finally it hides the emoji panel by setting showEmojis
to false.
In the Pusher event callback, we set the showEmojis
property to true
. In the same callback, we assign the data's
sentiment property to the score
variable.
By now our application should provide emoji suggestions for received messages.
Testing on mobile devices
To test the application on your mobile device, download the IonicDevApp on your mobile device. Make sure your computer and your mobile device are connected to the same network. When you open the IonicDevApp, you should see Ionic apps running on your network listed.
Note: Both the server(
node server
), ngrok for proxying our server and the Ionic dev server(ionic serve
) must be running to get the application working. Run the commands in separate terminal sessions if you haven’t done so already.
To view the application, click on it and you should see a similar view to what was in the browser. Sending messages to the server might have worked in the browser but localhost doesn’t exist on your phone, so we’ll need to create a proxy to be able to send messages from mobile.
Using Ngrok as a proxy
To create a proxy for our server, we’ll download Ngrok. Visit the download page on the Ngrok website. Download the client for your OS. Unzip it and run the following command in the folder where Ngrok can be found:
./ngrok http 4000
Copy the forwarding url with https
and place it in the chat.ts
file that previously had http://localhost:4000/message
. Please do not copy mine from the screenshot above.
// src/components/chat/chat.ts
...
export class ChatComponent implements OnInit {
...
sendMessage() {
...
this.http
.post('<NGROK_URL>/messages', data)
.subscribe((res: Message) => {});
}
...
}
...
Ensure to include the forwarding URL you copied where the placeholder string is
Now you should be receiving messages sent from the phone on the browser. Or if you have more than one phone you can test it using two of them.
Note: Both the server(
node server
), ngrok for proxying our server and the Ionic dev server(ionic serve
) must be running to get the application working. Run the commands in separate terminal sessions if you haven’t done so already.
To build your application to deploy on either the AppStore or PlayStore, follow the instructions found here.
Conclusion
Using sentiment analysis library, we’ve been able to suggest emojis as replies for received messages and with the help of Pusher and Ionic we’ve built an application can send and receive messages in realtime. You can view the source code for the demo here.
24 June 2018
by Christian Nwamba