Build read receipts in JavaScript

Introduction

When building realtime applications, we often want to know the actual time when a process or event occurs. For example, in an instant messaging application we want to know if and when our message was delivered to the intended client. We see this in WhatsApp where messages are sent in realtime and you see status of each message when it is delivered and read, with double grey tick when delivered and double blue tick when read. We can easily build a message delivery status using Pusher and JavaScript.

How?

Pusher has a concept of channels and event which are fundamental to it. We can send a message to a client through a channel and have that client notify us of a read receipt by triggering an event which the sender will listen to and react accordingly.

Channels provide a way of filtering data and controlling access to different streams of information, while events are the primary method of packaging messages in the Pusher system which forms the basis of all communication.

To implement a message delivery status with Pusher, we'll have to subscribe to a channel and listen for events on the channel. We'll build a simple chat application in JavaScript that will send out messages to a client and the client will trigger an event when received.

Application setup

To use Pusher API we have to signup and create a Pusher app from the dashboard. We can create as many applications as we want and each one will get an application id and secret key which we'll use to initialise a Pusher instance on client or server side code.

Create a new Pusher account

  • Sign Up to Pusher, or login if you already have an account.

  • After signup we get to the dashboard and shown a screen to setup up a new pusher app.

    • Enter a name for the application. In this case I'll call it "chat".
    • Select a cluster
    • Select the option "Create app for multiple environments" if you want to have different instances for development, staging and production
    • Choose a frontend tech. I'll choose VanillaJS since I won't be using any framework
    • Select Node.js as my backend
  • Click Create App to create the Pusher app.

read-receipts-javascript-create-app

Code

We will use channels as a means to send messages and trigger events through the channel. There are 3 types of channels in Pusher:

  • Public Channel which can be subscribed to by anyone who knows the name of the channel.
  • Private Channel which lets your server control access to the data you are broadcasting.
  • Presence Channel which is an extension of the private channel, but forces channel subscribers to register user information when subscribing. It also enables users to know who is online.

Clients needs to be authenticated to use the private and presence channels. For the sample app, we'll build the client using vanilla JS and server (for authentication) using NodeJS. Because I don't want message to go through the server, but from client to client, and I don't need to know if the user is online or away, I'll use a private channel for this demonstration, but the same technique will apply using any channel type. Client events can only be triggered in private or presence channels, and to use any of these channel types, the user/client must be authenticated, therefore the need for NodeJS back-end for authentication.

Also, to use client events, they must be enabled for the application. Go to your Pusher dashboard and on the App Settings tab, select "Enable Client Event" and update.

Backend

Since we're building our backend in Node using Express, let's initialise a new node app and install the needed dependencies. Run the following command:

  • npm init and select the default options
  • npm i --save body-parser express pusher to install express and the Pusher node package

Add a new file called server.js which will contain logic to authenticate the Pusher client and also render the static files we'll be adding later. This file will contain the content below

1var express = require('express');
2var bodyParser = require('body-parser');
3
4var Pusher = require('pusher');
5
6var app = express();
7app.use(bodyParser.json());
8app.use(bodyParser.urlencoded({ extended: false }));
9
10var pusher = new Pusher({ appId: APP_ID, key: APP_KEY, secret:  APP_SECRET, cluster: APP_Cluster });
11
12app.get('/',function(req,res){      
13     res.sendFile('index.html', {root: __dirname });
14});
15
16app.use(express.static(__dirname + '/'));
17
18app.post('/pusher/auth', function(req, res) {
19  var socketId = req.body.socket_id;
20  var channel = req.body.channel_name;
21  var auth = pusher.authenticate(socketId, channel);
22  res.send(auth);
23});
24
25var port = process.env.PORT || 5000;
26app.listen(port, function () {
27  console.log(`Example app listening on port ${port}!`)
28});

We instantiate Pusher by passing in an object that contains the details of our app ID and secret key, which can be found in the Pusher dashboard, on the App Keys tab. The line var auth = pusher.authenticate(socketId, channel); authenticates the client with Pusher and returns an authentication code to the calling client. To allow this file to run when we start npm, we update package.json with the following value:

1"scripts": {
2    "start": "node server.js",
3    "test": "echo \"Error: no test specified\" && exit 1"
4  },

Frontend

With the back-end in place, we now move on to crafting the front-end. We'll be using the template from this site with a slight modification.

Add a new file named index.html and style.css with the following content in each file:

Index.html

1<!DOCTYPE html>
2<html>
3<head>
4
5    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
6
7    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
8
9    <scriptsrc="https://code.jquery.com/jquery-2.2.4.min.js"
10        integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="
11        crossorigin="anonymous"></script>
12
13    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
14
15    <link rel="stylesheet" href="style.css">
16    <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
17    <script src="index.js"></script>
18</head>
19<body>
20    <div class="container">
21    <div class="row form-group">
22        <div class="col-xs-12 col-md-offset-2 col-md-8 col-lg-8 col-lg-offset-2">
23            <div class="panel panel-primary">
24                <div class="panel-heading">
25                    <span class="glyphicon glyphicon-comment"></span> Comments
26                    <div class="btn-group pull-right">
27                        <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
28                            <span class="glyphicon glyphicon-chevron-down"></span>
29                        </button>
30                        <ul class="dropdown-menu slidedown">
31                            <li><a href="http://www.jquery2dotnet.com"><span class="glyphicon glyphicon-refresh">
32                            </span>Refresh</a></li>
33                            <li><a href="http://www.jquery2dotnet.com"><span class="glyphicon glyphicon-ok-sign">
34                            </span>Available</a></li>
35                            <li><a href="http://www.jquery2dotnet.com"><span class="glyphicon glyphicon-remove">
36                            </span>Busy</a></li>
37                            <li><a href="http://www.jquery2dotnet.com"><span class="glyphicon glyphicon-time"></span>
38                                Away</a></li>
39                            <li class="divider"></li>
40                            <li><a href="http://www.jquery2dotnet.com"><span class="glyphicon glyphicon-off"></span>
41                                Sign Out</a></li>
42                        </ul>
43                    </div>
44                </div>
45                <div class="panel-body body-panel">
46                    <ul class="chat">
47
48                    </ul>
49                </div>
50                <div class="panel-footer clearfix">
51                    <textarea id="message" class="form-control" rows="3"></textarea>
52                    <span class="col-lg-6 col-lg-offset-3 col-md-6 col-md-offset-3 col-xs-12" style="margin-top: 10px">
53                        <button class="btn btn-warning btn-lg btn-block" id="btn-chat">Send</button>
54                    </span>
55                </div>
56            </div>
57        </div>
58    </div>
59</div>
60
61<script id="new-message-other" type="text/template">
62    <li class="left clearfix">
63        <span class="chat-img pull-left">
64            <img src="http://placehold.it/50/55C1E7/fff&text=U" alt="User Avatar" class="img-circle" />
65        </span>
66        <div class="chat-body clearfix">
67            <p>
68                {{body}}
69            </p>
70        </div>
71    </li>
72</script>
73
74<script id="new-message-me" type="text/template">
75    <li id="{{id}}" class="right clearfix">
76        <span class="chat-img pull-right">
77            <img src="http://placehold.it/50/FA6F57/fff&text=ME" alt="User Avatar" class="img-circle" />
78        </span>
79        <div class="chat-body clearfix">
80            <div class="header">
81                <small class="text-muted">{{status}}</small>
82
83            </div>
84            <p>
85                {{body}}
86            </p>
87        </div>
88    </li>
89</script>
90
91</body>
92</html>

style.css

1@import url("http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css");
2.chat
3{
4    list-style: none;
5    margin: 0;
6    padding: 0;
7}
8
9.chat li
10{
11    margin-bottom: 10px;
12    padding-bottom: 5px;
13    border-bottom: 1px dotted #B3A9A9;
14}
15
16.chat li.left .chat-body
17{
18    margin-left: 60px;
19}
20
21.chat li.right .chat-body
22{
23    margin-right: 60px;
24}
25
26.chat li .chat-body p
27{
28    margin: 0;
29    color: #777777;
30}
31
32.panel .slidedown .glyphicon, .chat .glyphicon
33{
34    margin-right: 5px;
35}
36
37.body-panel
38{
39    overflow-y: scroll;
40    height: 250px;
41}
42
43::-webkit-scrollbar-track
44{
45    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
46    background-color: #F5F5F5;
47}
48
49::-webkit-scrollbar
50{
51    width: 12px;
52    background-color: #F5F5F5;
53}
54
55::-webkit-scrollbar-thumb
56{
57    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
58    background-color: #555;
59}

The page we added holds a 1-to-1 chat template. At line 18 we added script to load the Pusher JavaScript library, and at 19 we're loading a custom JavaScript file which we will use to handle interactions from the page. Add this file with the following content:

index.js

1$(document).ready(function(){
2    // Enable pusher logging - don't include this in production
3    Pusher.logToConsole = true;
4
5    var pusher = new Pusher('APP_KEY', {
6        cluster: 'eu',
7        encrypted: false
8    });
9
10    var channel = pusher.subscribe('private-channel');
11    //channel name prefixed with 'private' because it'll be a private channel
12});

From the code above we first connect to Pusher by creating a Pusher object with the App_Key and cluster. These values are gotten from the Pusher dashboard. encrypted is set to false to allow it to send information on an unencrypted connection.

Afterwards, we subscribe to a channel which is to be used for sending out messages. Channel names can be anything, but must be a maximum of 164 characters. Another restriction on a private channel is that it must be prefixed with private- .

Next we bind to events. This way we can receive messages from a client through the channel we subscribed to. Add the following line to index.js

1channel.bind('client-message-added', onMessageAdded);
2channel.bind('client-message-delivered', onMessageDelivered);
3
4$('#btn-chat').click(function(){
5    const id = generateId();
6    const message = $("#message").val();
7    $("#message").val("");
8
9    let template = $("#new-message-me").html();
10    template = template.replace("{{id}}", id);
11    template = template.replace("{{body}}", message);
12    template = template.replace("{{status}}", "");
13
14    $(".chat").append(template);
15
16    //send message
17    channel.trigger("client-message-added", { id, message });
18});
19function generateId() {
20    return Math.round(new Date().getTime() + (Math.random() * 100));
21}
22
23function onMessageAdded(data) {
24    let template = $("#new-message-other").html();
25    template = template.replace("{{body}}", data.message);
26
27    $(".chat").append(template);
28
29    //notify sender
30    channel.trigger("client-message-delivered", { id: data.id });
31}
32
33function onMessageDelivered(data) {
34    $("#" + data.id).find("small").html("Delivered");
35}

I will be triggering events from the client and don't want it to go through the back-end or be validated. This is just for this demo. Client events must be prefixed by client- that is why I have done so with the code above. Events with any other prefix will be rejected by the Pusher server, as will events sent to channels to which the client is not subscribed.

It is important that you apply additional care when when triggering client events. Since these originate from other users, and could be subject to tampering by a malicious user of your site.

client-message-added will be triggered when a user enters a new message. Once the other user gets the message, it is displayed on the page, and client-message-delivered event is triggered to notify the sender of receipt. This way we can achieve the objective of getting notified about message delivery status in our application.

Run the application and see how it works.

read-receipts-javascript-demo

Wrap Up

With what you've seen so far, and knowing that Channels and Events are the fundamentals of Pusher, I hope I've shown you how to implement a message delivery status using Pusher and JavaScript. You can find the code on GitHub