Back to search

Build a chat app with Ionic featuring sentiment analysis

  • Christian Nwamba
June 24th, 2018
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:

ionic-chat-sentiment-demo-1 ionic-chat-sentiment-demo-2

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.

ionic-chat-sentiment-cordova

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.

ionic-chat-sentiment-ionic-serve

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 the appId and secret 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 and cluster.

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 and id in the request body sent by the user.
  • The data object will contain the text and id 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 is positive, negative or neutral.
  • tone: this variable is the context of the message gotten after analysis. This will be negative if the comparative score is below 0, neutral if the score is above 0 but below 1. The tone is positive if the comparative score is 1 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:

ionic-chat-sentiment-demo-3

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 and PUSHER_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.

ionic-chat-sentiment-in-app

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

ionic-chat-sentiment-ngrok

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.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.