Build a chat app using Ember.js

Introduction

Introduction

Building an engaging online chat application will definitely boost interaction between the involved parties, be it an online store with chat app for customer support in order to improve sales and ensure customers satisfaction, or a social platform with numerous users. In addition to the realtime engagement, wouldn’t it be nice to be able to determine the attitude of users based on a particular response during a chat?

In this tutorial, we will build a chat application with sentiments. Sentiment analysis will help us to computationally identify and categorize opinions expressed in a message during a chat. Using sentiment analysis, we will then proceed to suggest emojis based on the context of the received message.

Here’s what the completed demo will look like:

ember-chat-demo

This chat application will be built using Ember.js and Pusher Channels. We will approach the project by setting up the client side first and then proceed to configure the server side of the application by using Express.

Prerequisites

It is important that you have Node.js and npm installed on your machine in order to properly run the basic setups, learn how to install Node.js and npm here. A quick run-down of the core technologies we will be using include:

  • Ember.js: a productive frontend framework built for ambitious web developers.
  • Pusher Channels
  • a Node.js client to interact with the Pusher REST API
  • Express: Node utility for handling HTTP requests via routes
  • Axios: a promise-based HTTP client that works both in the browser and in a Node.js environment.
  • Body Parser: Attaches the request payload on Express’s req, hence req.body stores this payload for each request.
  • Sentiment: Sentiment is a module that uses the AFINN-165 wordlist and Emoji Sentiment Ranking to perform sentiment analysis on arbitrary blocks of input text.

Setting up the application

Installing Ember.js is quite easy. The most convenient and recommended way of getting an Ember app up and running with a single command is by using the Ember CLI tool. You can install it with npm, open your terminal and type this command:

     npm install -g ember-cli

The Ember CLI will give us access to the ember command that will be used to set up our project easily.

Creating the chat application

We can now proceed to create our application by using the ember new command. Open up the terminal on your machine and run a command that will create an application named chat-app-ember in your project directory or any location specified:

     ember new chat-app-ember

Next, change directory into the newly created project and start the development server:

1// change directory
2     cd chat-app-ember
3     
4     // start the development server
5     ember serve

This will start the development server on http://localhost:4200. Open your favorite browser and navigate to that URL, you should see this:

ember-start-page

That is the default welcome page by Ember.js, we will restructure this in a bit.

Install server dependencies

Run the following command to install the dependencies required for this project using:

1npm install --save axios pusher pusher-js he strftime
2    npm install --save body-parser cors dotenv express sentiment
3    ember install ember-browserify

Pusher account setup

To get started with Pusher Channels, sign up](https://dashboard.pusher.com/accounts/sign_up) for a free Pusher account.

ember-pusher-signup

Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:

ember-pusher-create-app

Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher Channels with to be provided with some boilerplate code:

ember-pusher-config-app

You can retrieve your keys from the App Keys tab:

ember-pusher-app-keys

Setting the environment variables

Create a .env file in the root directory of your application and add your application credentials as obtained from your Pusher dashboard as follows:

1PUSHER_APP_ID=YOUR_APP_ID
2      PUSHER_APP_KEY=YOUR_APP_KEY
3      PUSHER_APP_SECRET=YOUR_APP_SECRET_KEY
4      PUSHER_APP_CLUSTER=CLUSTER

We will make use of the variables specified in the above snippet later in our project. And do ensure that you replace YOUR_APP_ID, YOUR_APP_KEY, YOUR_APP_SECRET_KEY and CLUSTER placeholders with the appropriate credentials.

Setting up the server

Our application will need and work better with a simple server that will receive and process a POST request with a new message during chat. It’ll trigger an event on the Pusher channel that the client side can subscribe to. We’ll set it up by using Node and Express and run it on http://localhost:3000.

Since the Ember application that we configured earlier is running on a different domain, we’ll enable CORS in order to ensure communication between the Express server and Ember.

Create an app.js file in the root directory of your application and add the following code snippet to it to set up the server:

1// app.js
2    
3      const express = require('express');
4      const bodyParser = require('body-parser');
5      const cors = require('cors');
6      const Pusher = require('pusher');
7      const Sentiment = require('sentiment');
8      require('dotenv').config();
9      
10      const app = express();
11      
12      app.use(cors());
13      app.use(bodyParser.urlencoded({extended: false}));
14      app.use(bodyParser.json());
15    
16    // Ensure that your pusher credential are properly set in the .env file  
17      const pusher = new Pusher({
18          appId: process.env.PUSHER_APP_ID,
19          key: process.env.PUSHER_APP_KEY,
20          secret: process.env.PUSHER_APP_SECRET,
21          cluster: process.env.PUSHER_APP_CLUSTER,
22          encrypted: true
23      });
24      
25      app.set('port', process.env.PORT || 3000);
26      
27      app.post('/messages', (req, res) => {
28          const sentiment = new Sentiment();
29          const sentimentScore = sentiment.analyze(req.body.text).score;
30          const payload = {
31              text: req.body.text,
32              username: req.body.username,
33              time: req.body.time,
34              sentiment: sentimentScore
35          }
36          pusher.trigger('chat', 'message', payload);
37          res.send(payload)
38      })
39      app.listen(app.get('port'), () => {
40          console.log("Listening on port " + app.get('port'));
41      })

Here we loaded all the necessary middlewares for the Express server and then configured Pusher using the credentials we added to our environment variables earlier.

Furthermore, we also created /messages endpoint that will receive and process messages sent in from our Ember app. Finally, we triggered an event named message to Pusher Channels on a channel called chat.

Open another terminal and run the command below to start the server on http://localhost:3000

    node app.js

This will log a message to the console as shown by the image below:

ember-chat-node-running

Back to the client side

Now that we are done setting up the server, it’s time to switch our focus to the client. Next, we’ll create components and services required for our application.

Creating the Ember service

Our application will require a username so that we can identify the chatting individuals. In order to keep things simple, we won’t setup a registration and login process but rather use Ember service to store data that components within our application needs to access.

We are going to create a service that will be responsible for storing the name of the active user. Once created, the service can be accessed by injecting it into the UI components as we’ll see later.

Generate a new Ember service by running the command below from your terminal:

1// generate a service
2      ember g service active-user

The command above will create a new file app/services/active-user.js and a corresponding test file that is not required for now. active-user service will be responsible for storing the user’s name before they can start chatting. Open the file and update it with:

1// ./app/services/active-user.js 
2      
3      import Service from '@ember/service';
4      export default Service.extend({
5          user: null,
6          setUser(username) {
7              this.set('user', username);
8          },
9          hasUser() {
10              return this.get('user') != undefined;
11          }
12      });

We created a property user and provided a method for setting the user. The second method hasUser will return true if we have an active user so that we can decide if a user is logged in or not.

Create the UI components

Ember supports the usage and sharing of UI elements on multiple pages. We will leverage this and generate components for separate parts of our application. The components required for our application are:

  • chat-app: this will encompass the entire chat application
  • chat-username: this component will be used to show the user a form to enter their username
  • chat-message: component to display each chat message
  • list``-messages: will list all the existing messages

The Ember component generally consist of two parts: a JavaScript component file that defines behavior and a Handlebars template that defines the markup for the component’s UI.

Chat component

We’ll start by generating a component for the chat-app, by using the command below:

      ember g component chat-app

Component file

Locate the component file ./app/components/chat-app.js and update it with:

1// ./app/components/chat-app.js
2    
3      import Component from '@ember/component';
4      import { inject as service } from '@ember/service';
5      import { computed } from '@ember/object';
6      export default Component.extend({
7          activeUserService: service('active-user'),
8          nameIsSet: computed('activeUserService.user',function() {
9              return this.get('activeUserService').hasUser();
10          })
11      });

Here we injected the active-user service into the chat-app component by using the inject function from the @ember/service module.

We then created a computed property which will be set to true or false if a username for a user is set.

Chat app template

The template for the chat-app component will conditionally render two other components list-messages and chat-username if the username of a user is set. Open the template file ./app/templates/components/chat-app.hbs and update it as shown below:

1{{!-- ./app/templates/components/chat-app.hbs --}}
2    
3      <div>
4        {{#if nameIsSet}}
5        <div class="container">
6          <div class="row chat-window col-xs-12 col-md-5 col-md-offset-3" id="chat_window_1">
7            <div class="col-xs-12 col-md-12">
8              <div class="panel panel-default">
9                <div class="panel-heading top-bar">
10                  <div class="col-md-8 col-xs-8">
11                    <h3 class="panel-title"><span class="glyphicon glyphicon-comment"></span> Welcome <b>{{ userName }}</b></h3>
12                  </div>
13                  <div class="col-md-4 col-xs-4" style="text-align: right;">
14                    <a href="#"><span class="glyphicon glyphicon-remove icon_close" data-id="chat_window_1"></span></a>
15                  </div>
16                </div>
17                <div class="panel-body msg_container_base">
18                   {{#list-messages}}{{/list-messages}}
19                </div>
20              </div>
21            </div>
22          </div>
23      </div>
24      {{else}}
25      <div class="text-center">
26          {{#chat-username}}{{/chat-username}}
27      </div>
28      {{/if}} 
29      </div>

Username component

The username component will show the user a form to enter their name. Run this command to generate the component:

    ember g component chat-username

Username component file

Once a user inputs a username and submits the form, we will use the userSubmittedName action to get the name and send it to the active-user service. Update the username component file as shown here:

1// ./app/components/chat-username.js
2     
3      import Component from '@ember/component';
4      import { inject as service } from '@ember/service';
5      export default Component.extend({
6          activeUserService: service('active-user'),
7          actions: {
8              userSubmittedName() {
9                  const user = this.get('userName');
10                  this.get('activeUserService').setUser(user);
11              }
12          }
13      });

Username component template

The username template just shows the input field and a button that calls the userSubmittedName action that we defined earlier. Add the content below to ./app/templates/components/chat-username.hbs file:

1{{!-- ./app/templates/components/chat-username.hbs --}}
2     
3      <div class="intro">
4          <p>Enter your username name and start chatting! </p>
5          <div class="start-chat">
6              {{input placeholder="jack" value=userName }}
7              <button {{action "userSubmittedName"}}>
8                  Start chat
9              </button>
10          </div>
11      </div>

Chat message component

Next, we’ll generate the chat-message component with:

    ember g component chat-message

Chat message component file

This component will display each individual’s message:

1// ./app/components/chat-message.js
2      
3      import Component from '@ember/component';
4      import { computed } from '@ember/object';
5      import strftime from 'npm:strftime';
6      import he from 'npm:he';
7      export default Component.extend({
8          timestamp: computed('message.time', function() {
9              return strftime('%H:%M:%S %P', new Date(this.get('message').time));
10          }),
11          text: computed('message.text', function() {
12              return he.decode(this.get('message').text);
13          }),
14      });

In the code snippet above, we defined two computed properties; timestamp and text. The timestamp property uses the strftime library to format time while we used the he library to decode our HTML from the server.

Chat message component template

This template will display the username along with two properties that we defined earlier in our component. Open ./app/templates/components/chat-message.hbs and update it like this:

1{{!-- ./app/templates/components/chat-message.hbs --}}
2    
3      <div class="col-md-2 col-xs-2 avatar">
4          <img src="http://www.bitrebels.com/wp-content/uploads/2011/02/Original-Facebook-Geek-Profile-Avatar-1.jpg" class=" img-responsive ">
5      </div>
6      <div class="col-md-10 col-xs-10">
7          <div class="messages msg_receive">
8              <p>{{ text }}</p>
9               <time datetime="2009-11-13T20:00">{{ message.username }} • {{ timestamp }}</time>
10              <p>{{ message.mood }}</p>
11          </div>
12      </div>

List messages component

All the existing messages posted by an individual user will be listed by this component. To generate it, open your terminal and run the command below:

     ember g component list-messages

List messages component file

Open the ./app/components/list``-messages``.js file and add the following content:

1// ./app/components/list-messages.js
2    
3      import Component from '@ember/component';
4      import { inject as service } from '@ember/service';
5      import axios from 'npm:axios';
6      import Pusher from 'npm:pusher-js';
7      
8      const SAD_EMOJI = [55357, 56864];
9      const HAPPY_EMOJI = [55357, 56832];
10      const NEUTRAL_EMOJI = [55357, 56848];
11      
12      export default Component.extend({
13          activeUserService: service('active-user'),
14          messages: ['Welcome to your chat app!'].map((message) => {
15              return {
16                username: 'Admin',
17                time: new Date(),
18                text: message,
19              };
20            }),
21          init() {
22              this._super(...arguments);
23              let pusher = new Pusher('YOUR_APP_KEY', { // update your APP_KEY
24                  cluster: 'CLUSTER',
25                  encrypted: true
26              });
27              const channel = pusher.subscribe('chat');
28              channel.bind('message', data => {
29                  const analysis = data.sentiment > 0 ? HAPPY_EMOJI : (data.sentiment === 0 ? NEUTRAL_EMOJI : SAD_EMOJI);
30                  const response = {
31                      text: data.text,
32                      username: data.username,
33                      time: data.time,
34                      mood: String.fromCodePoint(...analysis)
35                  }
36                  this.get('messages').pushObject(response);
37              });
38          },
39          actions: {
40              newMessage() {
41                  const text = this.get('newMessage');
42                  const username = this.get('activeUserService').get('user');
43                  const time = new Date();
44            
45                  axios.post('http://localhost:3000/messages', { text, username, time });
46                  this.set('newMessage', '');
47              }
48          }
49      });

First, we imported the required module for this component and we added some constants of code points for a particular sentiment emoji. This will help us rendered an emoji corresponding to the mood of the user during the chat as analyzed by the sentiment module.

In addition, we injected the active-user service and set a default chat message for every user to see before starting a chat.

Next, we initialize Pusher with the APP_KEY and CLUSTER as obtained from our Pusher account dashboard and then proceeded to use the subscribe() method from Pusher to subscribe to the created chat channel.

Finally, the newMessage() action basically receives the message posted by a user and POST it to the server alongside the username and timestamp. This action will be triggered once the Send button in the template for this component is clicked.

Don’t forget to replace the YOUR_APP_KEY and CLUSTER placeholder with the appropriate details from your Pusher account dashboard.

List messages component template

The responsibility of this template is to loop over each message and render a chat-message component.

1{{!-- ./app/templates/components/list-messages.hbs --}}
2    
3      {{#each messages as |message|}}
4      <div class="row msg_container base_receive">
5            {{#chat-message message=message}}{{/chat-message}}
6      </div>
7      {{/each}}
8      <div class="panel-footer">
9          <div class="input-group">
10            {{ input value=newMessage id="btn-input" type="text" class="form-control input-sm chat_input" placeholder="Write your message here..."}}
11            <span class="input-group-btn">
12                <button class="btn btn-primary btn-sm" id="btn-chat" {{action "newMessage"}}>Send</button>
13            </span>
14         </div>
15      </div>

Updating the app template

Update the application template with:

1{{!-- ./app/templates/application.hbs --}}
2      
3      {{#chat-app}}{{/chat-app}}
4      {{outlet}}

Styling

To add some minimal styling to the page, open ./app/styles/app.css and paste the code in it:

1// ./app/styles/app.css
2      
3      .col-md-2, .col-md-10{
4        padding:0;
5      }
6      .chat-window{
7        margin-top: 50px;
8      }
9      .chat-window > div > .panel{
10        border-radius: 5px 5px 0 0;
11      }
12      .icon_minim{
13        padding:2px 10px;
14      }
15      .msg_container_base{
16      background: #e5e5e5;
17      margin: 0;
18      padding: 0 10px 10px;
19      min-height:150px;
20      overflow-x:hidden;
21      }
22      .top-bar {
23      background: #666;
24      color: white;
25      padding: 10px;
26      position: relative;
27      overflow: hidden;
28      }
29      .msg_sent{
30        padding-bottom:20px !important;
31        margin-right:0;
32      }
33      .messages {
34      background: white;
35      padding: 10px;
36      border-radius: 2px;
37      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
38      max-width:100%;
39      }
40      .messages > p {
41        font-size: 16px;
42        margin: 0 0 0.2rem 0;
43      }
44      .messages > time {
45        font-size: 13px;
46        color: #ccc;
47      }
48      .msg_container {
49        padding: 10px;
50        overflow: hidden;
51        display: flex;
52      }
53      img {
54        display: block;
55        width: 100%;
56      }
57      .avatar {
58        position: relative;
59      }
60      .base_receive > .avatar:after {
61        content: "";
62        position: absolute;
63        top: 0;
64        right: 0;
65        width: 0;
66        height: 0;
67        border: 5px solid #FFF;
68        border-left-color: rgba(0, 0, 0, 0);
69        border-bottom-color: rgba(0, 0, 0, 0);
70      }
71      .msg_container_base::-webkit-scrollbar-track
72      {
73        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
74        background-color: #F5F5F5;
75      }
76      .msg_container_base::-webkit-scrollbar
77      {
78        width: 12px;
79        background-color: #F5F5F5;
80      }
81      .msg_container_base::-webkit-scrollbar-thumb
82      {
83        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
84        background-color: #555;
85      }
86      .btn-group.dropup{
87        position:fixed;
88        left:0px;
89        bottom:0;
90      }
91      .red-gradient-background {
92        background: #300D4F;
93        background-image: -webkit-linear-gradient(top, #300D4F, #d14e37);
94        background-image: linear-gradient(to bottom, #300D4F, #d14e37);
95      }
96      .red-gradient-background h2 {
97        color: #ffffff;
98        text-transform: capitalize;
99      }
100      .intro {
101        margin-top: 50px;
102        font-size: 16px;
103      }
104      .intro p {
105        margin-bottom: 20px;
106        font-size: 16px;
107      }

Update the index page

Open the ./app/index.html file and include the CDN file for Bootstrap to enhance the styling and layout of our application, also include a navigation bar code snippet just after the opening of the <body> tag. Open up the file and add update as shown below:

1<!-- ./app/index.html -->
2        <!DOCTYPE html>
3      <html>
4        <head>
5          <meta charset="utf-8">
6          <meta http-equiv="X-UA-Compatible" content="IE=edge">
7          <title>ChatAppEmber</title>
8          <meta name="description" content="">
9          <meta name="viewport" content="width=device-width, initial-scale=1">
10          {{content-for "head"}}
11          <link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
12          <link integrity="" rel="stylesheet" href="{{rootURL}}assets/chat-app-ember.css">
13          <link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
14      
15          {{content-for "head-footer"}}
16        </head>
17        <body>
18            <nav class="navbar navbar-expand-lg navbar-light bg-light red-gradient-background">
19                <div class="collapse navbar-collapse justify-content-md-center" id="navbarsExample08">
20                  <h2>Realtime chat app</h2>
21                </div>
22              </nav>
23          {{content-for "body"}}
24          <script src="{{rootURL}}assets/vendor.js"></script>
25          <script src="{{rootURL}}assets/chat-app-ember.js"></script>
26          {{content-for "body-footer"}}
27        </body>
28      </html>

Test the application

Stop the Ember development server if it is currently running by typing Ctrl + C in the terminal and restart it with:

1// start the server
2    ember serve

Don’t forget to run node app.js to start the Express server incase you haven’t done that. Once you are done, navigate to http://localhost:4200 to test the application:

ember-chat-screenshot

Conclusion

In this tutorial, we have been able to build a basic chat application with sentiment. Sentiment analysis was used here to detect the mood of users during a chat. This tutorial can be improved on by adding extra features to make it go beyond just a basic chat application.

I hope you found this helpful. The source code can be found here on GitHub.