Live comments and ratings using sentiment analysis and Angular
You should have Node and npm installed on your machine. A basic understanding of Angular and Node is required.
Introduction
Sentiment analysis is a way to evaluate written or spoken language to determine if the expression is favorable, unfavorable, or neutral, and to what degree. You can read up about it here.
Live comments offer a realtime comment experience that doesn’t require a page refresh. You see comments when they’re posted.
Using Angular, you can extend the template language with your components and use a wide array of existing components. With Pusher we can enable realtime messaging in the chat using Pusher’s pub/sub pattern.
We’ll be building a live comments application using Pusher, Angular and the sentiment library for emoji suggestions based on the context of messages received.
Using our application, admin users can view how videos are rated based on the analysis of the messages sent in the live comments section.
Here’s a demo of the final product:
Prerequisites
To follow this tutorial a basic understanding of Angular 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, then using Pusher’s pub/sub pattern, we’ll listen 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
Using the Angular CLI (command line interface) provided by the Angular team, we’ll initialize our project. To initialize the project, first, install the CLI by running npm install @angular/cli
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 Angular project using the CLI, open a terminal and run:
ng new angular-live-comments --style=scss --routing
The command tells the CLI to create a new project called angular-live-comments
, use the CSS pre-processor SCSS rather than CSS for styling and set up routing for the application.
Open the newly created folder angular-live-comments
, your folder structure should be identical to this:
angular-live-comments/
e2e/
node_modules/
src/
app/
app.component.html
app.component.ts
app.component.css
...
Open a terminal inside the project folder and start the application by running ng serve
or npm start
. Open your browser and visit http://localhost:4200. What you see should be identical to the screenshot below.
Building our server
Now that we have our Angular application running, let’s build out a part of our server.
To do this we’ll need to install Express. Express is a fast, unopinionated, minimalist web framework for Node.js. We’ll use this to receive requests from our Angular application.
To install express, run npm install express
in a terminal in the root folder of your project.
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 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 that haven’t been installed, body-parser
, pusher
and dotenv
. Install these packages by running the following command in your terminal.
npm i body-parser pusher dotenv
- 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 to avoid adding sensitive information like theappId
andsecret
into our codebase directly. - The dotenv package will load the variables provided in our
.env
file into our environment. - CORS: 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.
The dotenv
library should always be initialized at the start of our file because we need to load the variables as early as possible to make them available throughout the application.
We also installed the Pusher library as a dependency. Follow the steps above to create a Pusher account if you haven’t done so already
Let’s create a .env
file to load the variables we’ll be needing into the Node environment. Create the file in the root folder of your project and update it with the code below.
// .env
PUSHER_APP_ID=APP_ID
PUSHER_KEY=PUSHER_KEY
PUSHER_SECRET=PUSHER_SECRET
Please ensure you replace the placeholder values above with your Pusher appId
, key
and secret
.
This is a standard Node application configuration, nothing specific to our app.
Sending messages
To enable users to 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');
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: 'eu',
encrypted: true,
});
...
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, name } = body;
const data = {
text,
name,
timeStamp: new Date(),
};
try {
pusher.trigger(['chat', 'rate'], 'message', data);
} catch (e) {}
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
andname
in the request body sent by the user. - The
data
object contains thetext
andname
sent by the user. It also includes a timestamp. - The
trigger
method which takes a trigger identifier, we included a list of channels because we wish to dispatch the event across two channels(chat
,rate
). - The trigger function also takes a second argument, the event name (
message
), and a payload(data
). - We still go ahead to respond with an object containing the
data
variable we created.
Sentiment analysis
Sentiment analysis uses data mining processes and techniques to extract and capture data for analysis in order to discern the subjective opinion of a document or collection of documents, like blog posts, reviews, news articles and social media feeds like tweets and status updates. - Technopedia
Using sentiment analysis, we’ll analyze 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. To install this library, open a terminal in the root folder of your project and run the following command.
npm install sentiment
We’ll 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, name } = body;
const result = sentiment.analyze(text);
const comparative = result.comparative;
const data = {
text,
name,
timeStamp: new Date(),
score: result.score,
};
try {
pusher.trigger(['chat', 'rate'], 'message', data);
} catch (e) {}
res.json(data);
});
...
- Include the sentiment library in the project.
result
: here, we 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.- A new property (
score
) is added to the response data containing the message’s score after analysis.
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 begin to build out our chat interface. We’ll create a chat
component to hold the chat interface. We’ll create this using the CLI. Run ng generate component chat
in a terminal in the root folder of your project.
Update the recently created files as follows:
// chat.component.html
<div>
<div class="input-area">
<form (submit)="sendMessage()" name="messageForm" #messageForm="ngForm">
<div>
<input type="text" placeholder="Your name" name="name" id="name" [(ngModel)]="message.name">
<textarea type="text" placeholder="Your message" name="message" id="message" [(ngModel)]="message.text" rows="5"></textarea>
</div>
<button>
<span data-feather="send"></span>
</button>
</form>
</div>
</div>
In the code snippet above:
- We have a form containing an input element, a textarea and a submit button.
- We are using an icon-set called feather-icons in our project. To include feather-icons in your project, simply add the cdn link in your
index.html
file.
// index.html
...
<script src="https://unpkg.com/feather-icons/dist/feather.min.js"></script>
</body>
...
Open the chat.component.ts
file and update with the code below:
// chat.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { HttpClient } from '@angular/common/http';
declare const feather: any;
export interface Message {
text: string;
name: string;
}
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.scss'],
})
export class ChatComponent implements OnInit {
constructor(private http: HttpClient) {}
@Output() onSendMessage: EventEmitter<Message> = new EventEmitter();
message = {
name: '',
text: '',
};
sendMessage() {
if (this.message.text !== '' && this.message.name !== '') {
this.http
.post(`http://localhost:4000/messages`, this.message)
.subscribe((res: Message) => {
this.onSendMessage.emit(res);
this.message = {
name: '',
text: '',
};
});
}
}
ngOnInit() {
feather.replace();
}
}
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.
In the ngOnInit
lifecycle, we initialize [feather](https://feathericons.com)
, our chosen icon set.
To make use of the HttpClient
service, we’ll need to import the HttpClientModule
into the app.module.ts
file. Also to make use of form-related directives, we’ll need to import the FormsModule
. Update your app module file as follows:
// app.module.ts
...
import { ChatComponent } from './chat/chat.component';
import {HttpClientModule} from '@angular/common/http';
import {FormsModule} from "@angular/forms";
@NgModule({
declarations: [AppComponent, ChatComponent],
imports: [BrowserModule, AppRoutingModule, HttpClientModule, FormsModule],
providers: [],
bootstrap: [AppComponent],
})
...
Styling
Open the chat.component.scss
file and update it with the following styles below:
// chat.component.scss
%input {
width: 100%;
border: none;
background: rgba(0, 0, 0, 0.08);
padding: 10px;
color: rgba(0, 0, 0, 0.3);
font-size: 14px;
font-weight: bold;
font-family: 'Roboto Condensed', sans-serif;
border-radius: 15px;
&:focus{
outline: none;
}
}
.input-area {
width: 100%;
form {
display: flex;
flex-direction: column;
div {
display: flex;
flex-direction: column;
max-width: 450px;
input {
@extend %input;
margin: 0 0 10px 0;
}
textarea {
@extend %input;
}
}
button {
width: 25%;
border: none;
background: darkslategray;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin-top: 10px;
padding: 5px 20px;
border-radius: 27px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12),
0 2px 4px 0 rgba(0, 0, 0, 0.08);
}
}
}
Home view
Let’s create the home component, this will house (pun intended) our chat
component, video and list of messages. Run ng generate component home
in a terminal in the root folder of your project.
Open the home.component.html
file and replace the contents with the snippet below.
// home.component.html
<div>
<div class="video">
<iframe width="500" height="300" src="https://www.youtube.com/embed/7CVtTOpgSyY" frameborder="0" allow="autoplay; encrypted-media"
allowfullscreen></iframe>
</div>
<div class="messages">
<h4>Messages</h4>
<div class="message" *ngFor="let message of messages">
<div class="pic">
<img src="/assets/man.svg" alt="profile-img">
</div>
<div class="message-text">
<span>{{message.name}}</span>
<p>{{message.text}}</p>
</div>
</div>
</div>
<app-chat></app-chat>
</div>
Note: you can find the assets used throughout the article in the GitHub repo.
Open the home.component.ts
file and update it with the following snippet:
// home.component.ts
import { Component, OnInit } from '@angular/core';
import { Message } from '../chat/chat.component';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
constructor() {}
messages: Array<Message> = [];
ngOnInit() {
}
}
Styling
Open the home.component.scss
file and update it with the styles below:
.video {
width: 500px;
height: 300px;
background: rgba(0, 0, 0, 0.2);
margin-bottom: 20px;
}
.messages {
margin-bottom: 30px;
border-bottom: 2px solid rgba(0, 0, 0, 0.2);
max-width: 500px;
h4 {
margin: 10px 0;
}
.message {
display: flex;
.pic {
display: flex;
align-items: center;
img {
height: 40px;
width: 40px;
border-radius: 50%;
}
}
.message-text {
padding: 10px;
span {
font-size: 11px;
opacity: 0.8;
font-weight: bold;
}
p {
font-size: 15px;
opacity: 0.6;
margin: 2px 0;
}
}
}
}
Introducing Pusher
So far we have an application that allows users send in comments, but these comments are only visible to the sender. We’ll include the Pusher library in our application to enable realtime features like seeing comments as they come in without having to refresh the page.
Open a terminal in the root folder of the project and install the package by running the following command:
npm install pusher-js
We’ll add the library as a third party script to be loaded by Angular CLI. CLI config is always stored in the .angular-cli.json
file. Modify the scripts
property to include the link to pusher.min.js
.
// .angular-cli.json
...
"scripts": [
"../node_modules/pusher-js/dist/web/pusher.min.js"
]
...
Now that Pusher has been made available in our project, we’ll 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.
ng generate service pusher
This command simply tells the CLI to generate a service named pusher
. Now open the pusher.service.ts
file and update it with the code below.
// pusher.service.ts
import { Injectable } from '@angular/core';
declare const Pusher: any;
@Injectable()
export class PusherService {
constructor() {
// Replace this with your pusher key
this.pusher = new Pusher('<PUSHER_KEY>', {
cluster: 'eu',
encrypted: true,
});
}
pusher;
public init(channel) {
return this.pusher.subscribe(channel);
}
}
- First, we initialize Pusher in the constructor.
- The
init
subscribes to the channel passed as a parameter.
Note: ensure you replace the
PUSHER_KEY
string with your actual Pusher key.
To make the service available application wide, import it into the module file.
// app.module.ts
...
import { HttpClientModule } from '@angular/common/http';
import {PusherService} from './pusher.service';
@NgModule({
....
providers: [PusherService],
....
})
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 in the home.component.ts
file.
// home.component.ts
import { Component, OnInit } from '@angular/core';
import { Message } from '../chat/chat.component';
import { PusherService } from '../pusher.service';
...
constructor(private pusher: PusherService){}
messages: Array<Message> = [];
ngOnInit() {
const channel = this.pusher.init('chat');
channel.bind('message', (data) => {
this.messages = this.messages.concat(data);
});
}
}
Routing
To enable routing between the home
and admin
page, we’ll define routes for each component in the app-routing.module.ts
file.
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{
component: HomeComponent,
path: '',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
routes
: previously, the routes
variable was an empty array, but we’ve updated it to include two objects containing our route component
and path
.
Next we’ll replace all the contents in your app.component.html
file leaving just the router-outlet
. Your app.component.html
file should look like the snippet below:
// app.component.html
<div class="main">
<router-outlet></router-outlet>
</div>
Let’s have a look at what our home page looks like after the updates. Navigate to http://localhost:4200
Admin page
Whenever we post a video, we want to be able to tell how the video was perceived by users using their comments on the video. Sentiment analysis is used to achieve this. All comments under the video will be analyzed to determine the user’s attitude towards the video. All videos posted will be rated based on the tone of every comment posted.
If the comments under a video are mostly negative, the video will get a simple thumbs down(👎🏼) and a thumbs up(👍🏼) if the comments are positive.
To create the admin page, run ng generate component admin
in a terminal in the root folder of your project.
Replace the contents of the admin.component.html
file with the snippet below.
// admin.component.html
<div class="admin">
<h3>Admin</h3>
<div>
<h4>Videos List</h4>
<div class="video">
<div class="vid-thumbnail">
<img src="/assets/vid-thumbnail.png" alt="video thumbnail">
</div>
<div class="vid-desc">
<span>Pixar</span>
<p>Shooting Star Clip</p>
</div>
<div class="vid-rating">
<span class="header">
Rating
</span>
<div [hidden]="rating < 1">
<span data-feather="thumbs-up" class="positive"></span>
</div>
<div [hidden]="rating >= 1">
<span data-feather="thumbs-down" class="negative"></span>
</div>
</div>
</div>
</div>
</div>
Note: all assets used are available in the repo here.
We have the thumbs up and thumbs down icons, we display thumbs up if the rating is one and above. Thumbs down is displayed when the video rating is below one. The rating
property will be defined in the admin.component.ts
file below.
Styling
Add the styles below to the admin.component.scss
file.
// admin.component.scss
.admin {
width: 500px;
.video {
display: flex;
box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.2);
padding: 10px;
.vid-thumbnail {
flex: 1;
img {
height: 70px;
width: 120px;
}
}
.vid-desc {
flex: 4;
padding: 0 8px;
span {
font-size: 15px;
font-weight: bold;
opacity: 0.8;
}
p {
margin: 3px;
font-size: 17px;
opacity: 0.6;
}
}
.vid-rating {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.header{
font-size: 12px;
margin: 0 0 5px;
opacity: 0.6;
}
.positive {
color: #40a940;
}
.negative {
color: rgb(196, 64, 64);
}
}
}
}
Open the admin.component.ts
file and update it as so:
// admin.component.ts
import { Component, OnInit } from '@angular/core';
import { PusherService } from '../pusher.service';
declare const feather: any;
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss'],
})
export class AdminComponent implements OnInit {
constructor(private pusher: PusherService) {}
rating = 1;
ngOnInit() {
feather.replace();
const channel = this.pusher.init('rate');
channel.bind('message', (data) => {
this.rating += data.score;
});
}
}
rating
: starting out, every video has a rating of 1.- In the
ngOnInit
lifecycle, we initialize feather and subscribe to therate
channel. We then listen for amessage
event. In the callback, thescore
property of the data returned is added to therating
property.
Now let’s define the route for the admin page. Open the app-routing.module.ts
file and update the routes
array like so:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AdminComponent } from './admin/admin.component';
const routes: Routes = [
{
component: HomeComponent,
path: '',
},
{
component: AdminComponent,
path: 'admin',
},
];
...
Navigate to http://localhost:4200/admin to view the admin page. Here’s a screenshot of what it looks like:
There’s not much going on here, but now our admin page rates videos in realtime whenever there’s a new comment.
Here’s a screenshot of both pages side by side.
Conclusion
Using the sentiment analysis library, we can rate videos on our site by analyzing the comments posted under the videos. Using Pusher Channels, we were able to implement live comments functionality in our application. You can view the source code for the demo here.
8 May 2018
by Christian Nwamba