Build a chat app using Ember.js
You will need Node and npm installed on your machine.
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:
This chat application will be built using Ember.js and Pusher. 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: 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
, hencereq.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:
// change directory
cd chat-app-ember
// start the development server
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:
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:
npm install --save axios pusher pusher-js he strftime
npm install --save body-parser cors dotenv express sentiment
ember install ember-browserify
Pusher account setup
Head over to Pusher and sign up for a free account.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with to be provided with some boilerplate code:
You can retrieve your keys from the App Keys tab:
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:
PUSHER_APP_ID=YOUR_APP_ID
PUSHER_APP_KEY=YOUR_APP_KEY
PUSHER_APP_SECRET=YOUR_APP_SECRET_KEY
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:
// app.js
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const Pusher = require('pusher');
const Sentiment = require('sentiment');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
// Ensure that your pusher credential are properly set in the .env file
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: process.env.PUSHER_APP_CLUSTER,
encrypted: true
});
app.set('port', process.env.PORT || 3000);
app.post('/messages', (req, res) => {
const sentiment = new Sentiment();
const sentimentScore = sentiment.analyze(req.body.text).score;
const payload = {
text: req.body.text,
username: req.body.username,
time: req.body.time,
sentiment: sentimentScore
}
pusher.trigger('chat', 'message', payload);
res.send(payload)
})
app.listen(app.get('port'), () => {
console.log("Listening on port " + app.get('port'));
})
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:
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:
// generate a service
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:
// ./app/services/active-user.js
import Service from '@ember/service';
export default Service.extend({
user: null,
setUser(username) {
this.set('user', username);
},
hasUser() {
return this.get('user') != undefined;
}
});
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 applicationchat-username
: this component will be used to show the user a form to enter their usernamechat-message
: component to display each chat messagelist``-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:
// ./app/components/chat-app.js
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
export default Component.extend({
activeUserService: service('active-user'),
nameIsSet: computed('activeUserService.user',function() {
return this.get('activeUserService').hasUser();
})
});
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:
{{!-- ./app/templates/components/chat-app.hbs --}}
<div>
{{#if nameIsSet}}
<div class="container">
<div class="row chat-window col-xs-12 col-md-5 col-md-offset-3" id="chat_window_1">
<div class="col-xs-12 col-md-12">
<div class="panel panel-default">
<div class="panel-heading top-bar">
<div class="col-md-8 col-xs-8">
<h3 class="panel-title"><span class="glyphicon glyphicon-comment"></span> Welcome <b>{{ userName }}</b></h3>
</div>
<div class="col-md-4 col-xs-4" style="text-align: right;">
<a href="#"><span class="glyphicon glyphicon-remove icon_close" data-id="chat_window_1"></span></a>
</div>
</div>
<div class="panel-body msg_container_base">
{{#list-messages}}{{/list-messages}}
</div>
</div>
</div>
</div>
</div>
{{else}}
<div class="text-center">
{{#chat-username}}{{/chat-username}}
</div>
{{/if}}
</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:
// ./app/components/chat-username.js
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
activeUserService: service('active-user'),
actions: {
userSubmittedName() {
const user = this.get('userName');
this.get('activeUserService').setUser(user);
}
}
});
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:
{{!-- ./app/templates/components/chat-username.hbs --}}
<div class="intro">
<p>Enter your username name and start chatting! </p>
<div class="start-chat">
{{input placeholder="jack" value=userName }}
<button {{action "userSubmittedName"}}>
Start chat
</button>
</div>
</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:
// ./app/components/chat-message.js
import Component from '@ember/component';
import { computed } from '@ember/object';
import strftime from 'npm:strftime';
import he from 'npm:he';
export default Component.extend({
timestamp: computed('message.time', function() {
return strftime('%H:%M:%S %P', new Date(this.get('message').time));
}),
text: computed('message.text', function() {
return he.decode(this.get('message').text);
}),
});
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:
{{!-- ./app/templates/components/chat-message.hbs --}}
<div class="col-md-2 col-xs-2 avatar">
<img src="http://www.bitrebels.com/wp-content/uploads/2011/02/Original-Facebook-Geek-Profile-Avatar-1.jpg" class=" img-responsive ">
</div>
<div class="col-md-10 col-xs-10">
<div class="messages msg_receive">
<p>{{ text }}</p>
<time datetime="2009-11-13T20:00">{{ message.username }} • {{ timestamp }}</time>
<p>{{ message.mood }}</p>
</div>
</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:
// ./app/components/list-messages.js
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import axios from 'npm:axios';
import Pusher from 'npm:pusher-js';
const SAD_EMOJI = [55357, 56864];
const HAPPY_EMOJI = [55357, 56832];
const NEUTRAL_EMOJI = [55357, 56848];
export default Component.extend({
activeUserService: service('active-user'),
messages: ['Welcome to your chat app!'].map((message) => {
return {
username: 'Admin',
time: new Date(),
text: message,
};
}),
init() {
this._super(...arguments);
let pusher = new Pusher('YOUR_APP_KEY', { // update your APP_KEY
cluster: 'CLUSTER',
encrypted: true
});
const channel = pusher.subscribe('chat');
channel.bind('message', data => {
const analysis = data.sentiment > 0 ? HAPPY_EMOJI : (data.sentiment === 0 ? NEUTRAL_EMOJI : SAD_EMOJI);
const response = {
text: data.text,
username: data.username,
time: data.time,
mood: String.fromCodePoint(...analysis)
}
this.get('messages').pushObject(response);
});
},
actions: {
newMessage() {
const text = this.get('newMessage');
const username = this.get('activeUserService').get('user');
const time = new Date();
axios.post('http://localhost:3000/messages', { text, username, time });
this.set('newMessage', '');
}
}
});
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.
{{!-- ./app/templates/components/list-messages.hbs --}}
{{#each messages as |message|}}
<div class="row msg_container base_receive">
{{#chat-message message=message}}{{/chat-message}}
</div>
{{/each}}
<div class="panel-footer">
<div class="input-group">
{{ input value=newMessage id="btn-input" type="text" class="form-control input-sm chat_input" placeholder="Write your message here..."}}
<span class="input-group-btn">
<button class="btn btn-primary btn-sm" id="btn-chat" {{action "newMessage"}}>Send</button>
</span>
</div>
</div>
Updating the app template
Update the application template with:
{{!-- ./app/templates/application.hbs --}}
{{#chat-app}}{{/chat-app}}
{{outlet}}
Styling
To add some minimal styling to the page, open ./app/styles/app.css
and paste the code in it:
// ./app/styles/app.css
.col-md-2, .col-md-10{
padding:0;
}
.chat-window{
margin-top: 50px;
}
.chat-window > div > .panel{
border-radius: 5px 5px 0 0;
}
.icon_minim{
padding:2px 10px;
}
.msg_container_base{
background: #e5e5e5;
margin: 0;
padding: 0 10px 10px;
min-height:150px;
overflow-x:hidden;
}
.top-bar {
background: #666;
color: white;
padding: 10px;
position: relative;
overflow: hidden;
}
.msg_sent{
padding-bottom:20px !important;
margin-right:0;
}
.messages {
background: white;
padding: 10px;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
max-width:100%;
}
.messages > p {
font-size: 16px;
margin: 0 0 0.2rem 0;
}
.messages > time {
font-size: 13px;
color: #ccc;
}
.msg_container {
padding: 10px;
overflow: hidden;
display: flex;
}
img {
display: block;
width: 100%;
}
.avatar {
position: relative;
}
.base_receive > .avatar:after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border: 5px solid #FFF;
border-left-color: rgba(0, 0, 0, 0);
border-bottom-color: rgba(0, 0, 0, 0);
}
.msg_container_base::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar
{
width: 12px;
background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar-thumb
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
.btn-group.dropup{
position:fixed;
left:0px;
bottom:0;
}
.red-gradient-background {
background: #300D4F;
background-image: -webkit-linear-gradient(top, #300D4F, #d14e37);
background-image: linear-gradient(to bottom, #300D4F, #d14e37);
}
.red-gradient-background h2 {
color: #ffffff;
text-transform: capitalize;
}
.intro {
margin-top: 50px;
font-size: 16px;
}
.intro p {
margin-bottom: 20px;
font-size: 16px;
}
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:
<!-- ./app/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>ChatAppEmber</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for "head"}}
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/chat-app-ember.css">
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
{{content-for "head-footer"}}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light red-gradient-background">
<div class="collapse navbar-collapse justify-content-md-center" id="navbarsExample08">
<h2>Realtime chat app</h2>
</div>
</nav>
{{content-for "body"}}
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/chat-app-ember.js"></script>
{{content-for "body-footer"}}
</body>
</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:
// start the server
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:
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.
15 June 2018
by Christian Nwamba