Build a chat app for a live event with Node.js, MySQL and Pusher Channels Part 2: Adding chat functionality and admin dashboard

Introduction

This is a two-part tutorial. Make sure you started with Part 1 to cover the full build.

We'll explain how to build a simple chat room that can be featured alongside a live event. By taking advantage of the user concept in Pusher to build strong authentication and moderation features, and offer your users a safer and more enjoyable chat experience.

The backend is built using Node.js and Express while we've used JavaScript for the frontend and Pusher Channels.

Implementation steps

Part 1 of this tutorial covers steps 1 to 5 - from how to set up Pusher Channels, enable client events, get App Keys, set the code base, and build the login page.

Part 2 continues with steps 6 to 8 on how to build and test the homepage with a chat widget, build the admin dashboard, run the app and see it all in action.

Step 6: Build homepage with live chat widget

Let's add more to the homepage route that will welcome our users and will serve as the basis for building an extensive UI.

As you might notice in the Adding the Login logic with Node.js section in part 1 of this tutorial, the landing page view depends on the participant type. It will look different for a regular participant and the admin user.

Check Build the admin dashboard section for more details.

Create CSS for homepage with chat widget

  • Navigate to public/landing directory and create landing.css file:
1cd ../public/landing
2    touch landing.css
  • Edit the landing.css file and add:
1/* pusher-event-chat/public/landing/landing.css */
2    a,
3    a:focus,
4    a:hover {
5    color: #fff;
6    }
7
8    .btn-secondary,
9    .btn-secondary:hover,
10    .btn-secondary:focus {
11    color: #333;
12    text-shadow: none;
13    background-color: #fff;
14    border: .05rem solid #fff;
15    }
16
17    html,
18    body {
19    height: 100%;
20    background-color: #435165;
21    }
22
23    body {
24    color: #fff;
25    text-align: center;
26    text-shadow: 0 .05rem .1rem rgba(0,0,0,.5);
27    }
28
29    .site-wrapper {
30    display: table;
31    width: 100%;
32    height: 100%; /* For at least Firefox */
33    min-height: 100%;
34    box-shadow: inset 0 0 5rem rgba(0,0,0,.5);
35    background-size: cover;
36    background-repeat: no-repeat;
37    background-position: center;
38    }
39
40    .site-wrapper-inner {
41    display: table-cell;
42    vertical-align: top;
43    }
44
45    .cover-container {
46    margin-right: auto;
47    margin-left: auto;
48    }
49
50    .inner {
51    padding: 2rem;
52    }
53
54    .masthead {
55    margin-bottom: 2rem;
56    }
57
58    .masthead-brand {
59    margin-bottom: 0;
60    }
61
62    .nav-masthead .nav-link {
63    padding: .25rem 0;
64    font-weight: 700;
65    color: rgba(255,255,255,.5);
66    background-color: transparent;
67    border-bottom: .25rem solid transparent;
68    }
69
70    .nav-masthead .nav-link:hover,
71    .nav-masthead .nav-link:focus {
72    border-bottom-color: rgba(255,255,255,.25);
73    }
74
75    .nav-masthead .nav-link + .nav-link {
76    margin-left: 1rem;
77    }
78
79    .nav-masthead .active {
80    color: #fff;
81    border-bottom-color: #fff;
82    }
83
84    @media (min-width: 48em) {
85
86    .masthead-brand {
87        float: left;
88    }
89
90    .nav-masthead {
91        float: right;
92    }
93
94    }
95
96    /*
97    * Cover
98    */
99
100    .cover {
101    padding: 0 1.5rem;
102    }
103
104    .cover .btn-lg {
105    padding: .75rem 1.25rem;
106    font-weight: 700;
107    }
108
109    .mastfoot {
110    color: rgba(255,255,255,.5);
111    }
112
113    @media (min-width: 40em) {
114
115    .masthead {
116        position: fixed;
117        top: 0;
118    }
119
120    .mastfoot {
121        position: fixed;
122        bottom: 0;
123    }
124
125    .site-wrapper-inner {
126        vertical-align: middle;
127    }
128
129    /* Handle the widths */
130    .masthead,
131    .mastfoot,
132    .cover-container {
133        width: 100%;
134    }
135
136    }
137
138    @media (min-width: 62em) {
139
140    .masthead,
141    .mastfoot,
142    .cover-container {
143        width: 42rem;
144    }
145
146    }
147
148    .chatbubble {
149        position: fixed;
150        bottom: 0;
151        right: 30px;
152        transform: translateY(300px);
153        transition: transform .3s ease-in-out;
154    }
155
156    .chatbubble.opened {
157        transform: translateY(0)
158    }
159
160    .chatbubble .unexpanded {
161        display: block;
162        background-color: #e23e3e;
163        padding: 10px 15px 10px;
164        position: relative;
165        cursor: pointer;
166        width: 350px;
167        border-radius: 10px 10px 0 0;
168    }
169
170    .chatbubble .expanded {
171        height: 300px;
172        width: 350px;
173        background-color: #fff;
174        text-align: left;
175        padding: 10px;
176        color: #333;
177        text-shadow: none;
178        font-size: 14px;
179    }
180
181    .chatbubble .chat-window {
182    overflow: auto;
183    }
184
185    .chatbubble .loader-wrapper {
186        margin-top: 50px;
187        text-align: center;
188    }
189
190    .chatbubble .messages {
191        display: none;
192        list-style: none;
193        margin: 0 0 50px;
194        padding: 0;
195    }
196
197    .chatbubble .messages li {
198        width: 85%;
199        float: left;
200        padding: 10px;
201        border-radius: 5px 5px 5px 0;
202        font-size: 14px;
203        background: #c9f1e6;
204        margin-bottom: 10px;
205    }
206
207    .chatbubble .messages li .sender {
208        font-weight: 600;
209    }
210
211    .chatbubble .messages li.reply {
212        float: right;
213        text-align: right;
214        color: #fff;
215        background-color: #e33d3d;
216        border-radius: 5px 5px 0 5px;
217    }
218
219    .chatbubble .chats .input {
220        position: absolute;
221        bottom: 0;
222        padding: 10px;
223        left: 0;
224        width: 100%;
225        background: #f0f0f0;
226        display: none;
227    }
228
229    .chatbubble .chats .input .form-group {
230        width: 80%;
231    }
232
233    .chatbubble .chats .input input {
234        width: 100%;
235    }
236
237    .chatbubble .chats .input button {
238        width: 20%;
239    }
240
241    .chatbubble .chats {
242    display: none;
243    }
244
245    .chatbubble .join-screen {
246    margin-top: 20px;
247    display: none;
248    }
249
250    .chatbubble .chats.active,
251    .chatbubble .join-screen.active {
252    display: block;
253    }
254
255    /* Loader Credit: https://codepen.io/ashmind/pen/zqaqpB */
256    .chatbubble .loader {
257    color: #e23e3e;
258    font-family: Consolas, Menlo, Monaco, monospace;
259    font-weight: bold;
260    font-size: 10vh;
261    opacity: 0.8;
262    }
263
264    .chatbubble .loader span {
265    display: inline-block;
266    -webkit-animation: pulse 0.4s alternate infinite ease-in-out;
267            animation: pulse 0.4s alternate infinite ease-in-out;
268    }
269
270    .chatbubble .loader span:nth-child(odd) {
271    -webkit-animation-delay: 0.4s;
272            animation-delay: 0.4s;
273    }
274
275    @-webkit-keyframes pulse {
276    to {
277        -webkit-transform: scale(0.8);
278                transform: scale(0.8);
279        opacity: 0.5;
280    }
281
282    }
283
284    @keyframes pulse {
285
286    to {
287        -webkit-transform: scale(0.8);
288                transform: scale(0.8);
289        opacity: 0.5;
290    }
291
292    }

Create homepage template with HTML

The homepage will consist of two key HTML classes:

  • Site-wrapper - Includes a variety of links and inactive buttons for further website development.
  • Chatbubble - A basic chat room that will be visible to all the participants of the event.
  • Create index.html file in the public/landing directory:
    touch index.html
  • Edit the index.html by adding the code:
1<!-- pusher-event-chat/public/landing/index.html -->
2    <!DOCTYPE html>
3    <html lang="en">
4    <head>
5        <meta charset="utf-8">
6        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7        <title>Real-time tech event</title>
8        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
9        <link rel="stylesheet" href="./landing/landing.css">
10    </head>
11    <body>
12        <div class="site-wrapper">
13        <div class="site-wrapper-inner">
14            <div class="cover-container">
15            <header class="masthead clearfix">
16                <div class="inner">
17                <h3 class="masthead-brand">;Real-time tech event</h3>
18                <nav class="nav nav-masthead">
19                    <a class="nav-link active" href="#">Home</a>
20                    <a class="nav-link" href="#">Schedule</a>
21                    <a class="nav-link" href="#">Contact</a>
22                </nav>
23                </div>
24            </header>
25            <main role="main" class="inner cover">
26                <h1 class="cover-heading">Real-time tech event</h1>
27                <p class="lead">;Powering realtime experiences for mobile and web!</p>
28                <p class="lead">
29                <a href="#" class="btn btn-lg btn-secondary">SCHEDULE</a>
30                </p>
31            </main>
32            <footer class="mastfoot">
33            </footer>
34            </div>
35        </div>
36        </div>
37        <div class="chatbubble">
38        <div class="unexpanded">
39            <div class="title">;Chat with other participants</div>
40        </div>
41        <div class="expanded chat-window">
42            <div class="join-screen container">
43            <form id="joinScreenForm">
44                <button type="button" class="btn btn-block btn-primary">Join Chat</button>
45            </form>
46            </div>
47            <div class="chats">
48            <div class="loader-wrapper">
49                <div class="loader">
50                <span>{</span><span>}</span>
51                </div>
52            </div>
53            <ul class="messages clearfix">
54            </ul>
55            <div class="input">
56                <form class="form-inline" id="messageOthers">
57                <div class="form-group">
58                    <input type="text" autocomplete="off" class="form-control" id="newMessage" placeholder="Enter Message">
59                </div>
60                <button type="submit" class="btn btn-primary">Send</button>
61                </form>
62            </div>
63            </div>
64        </div>
65    </div>
66        <script src="https://js.pusher.com/7.1.0-beta/pusher.min.js"></script>
67        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
68        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
69        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
70        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
71        <script type="text/javascript" src="./landing/app.js"></script>
72    </body>
73    </html>

The template enables users to join the chat and submit their messages.

Now let’s include some JavaScript.

Build live chat with Pusher Channels

  • In the same directory ./public/landing create app.js file:
    touch app.js
  • Start editing the file by adding the following code:
1// pusher-event-chat/public/landing/app.js
2    (function() {
3
4        'use strict';
5
6        var pusher = new Pusher("YOUR_PUSHER_APP_KEY", {
7            userAuthentication: {
8                endpoint: "/pusher/user-auth",
9            },
10            channelAuthorization: {
11                endpoint: "/pusher/auth",
12            },
13            cluster: 'YOUR_PUSHER_APP_CLUSTER',
14            forceTLS: true,
15        });
16
17        let chat = {
18            owner: undefined,
19        }
20
21        const messageEventName = 'client-new-message'
22        const chatChannelName = 'presence-groupChat'
23        const warningEvent = 'client-warn-user'
24        const terminateEvent = 'client-terminate-user'
25
26        const chatPage = $(document)
27        const chatWindow = $('.chatbubble')
28        const chatHeader = chatWindow.find('.unexpanded')
29        const chatBody = chatWindow.find('.chat-window')
30
31        let helpers = {
32            ToggleChatWindow: function() {
33                chatWindow.toggleClass('opened')
34                chatHeader.find('.title').text(
35                    chatWindow.hasClass('opened') ? 'Minimize Chat Window' : 'Chat with other participants'
36                )
37            },
38
39            ShowAppropriateChatDisplay: function() {
40                (chat.owner) ? helpers.ShowChatRoomDisplay(): helpers.ShowChatInitiationDisplay()
41            },
42
43            ShowChatInitiationDisplay: function() {
44                chatBody.find('.chats').removeClass('active')
45                chatBody.find('.join-screen').addClass('active')
46            },
47
48            ShowChatRoomDisplay: function() {
49                chatBody.find('.chats').addClass('active')
50                chatBody.find('.join-screen').removeClass('active')
51                setTimeout(function() {
52                    chatBody.find('.loader-wrapper').hide()
53                    chatBody.find('.input, .messages').show()
54                }, 2000)
55            },
56
57            NewChatMessage: function(message) {
58                if (message !== undefined) {
59                    const messageClass = message.sender !== chat.owner ? 'reply' : 'user'
60                    chatBody.find('ul.messages').append(
61                        `<li class="clearfix message ${messageClass}">
62                            <div class="sender">${message.sender}</div>
63                            <div class="message">${message.text}</div>
64                        </li>`
65                    )
66
67                    chatBody.scrollTop(chatBody[0].scrollHeight)
68                }
69            },
70
71            SendMessageToOthers: function(evt) {
72                evt.preventDefault()
73                let createdAt = new Date()
74                createdAt = createdAt.toLocaleString()
75                const message = $('#newMessage').val().trim()
76
77                var channel = pusher.channel(chatChannelName);
78                channel.trigger(messageEventName, {
79                    'sender': chat.owner,
80                    'text': message,
81                    'createdAt': createdAt
82                });
83
84                helpers.NewChatMessage({
85                    'text': message,
86                    'name': chat.owner,
87                    'sender': chat.owner
88                })
89
90                console.log("Message added!")
91                $('#newMessage').val('')
92            },
93
94            JoinChatSession: function() {
95                chatBody.find('#joinScreenForm button').attr('disabled', true)
96                pusher.signin();
97
98                const channel = pusher.subscribe(chatChannelName);
99                channel.bind('pusher:subscription_succeeded', () => {
100                    let me = channel.members.me
101                    chat.owner = me.info.fullname
102                    helpers.ShowAppropriateChatDisplay()
103                });
104
105                helpers.Listen()
106            },
107
108            Listen() {
109                const channel = pusher.channel(chatChannelName);
110                channel.bind(messageEventName, (data) => {
111                    helpers.NewChatMessage(data)
112                })
113
114                pusher.user.bind(warningEvent, function(data) {
115                    alert(JSON.stringify(data.message));
116                });
117
118                pusher.user.bind(terminateEvent, function(data) {
119                    alert(JSON.stringify(data.message));
120                    chat.owner = '';
121                    helpers.ShowAppropriateChatDisplay()
122                });
123            }
124        }
125
126        chatPage.ready(helpers.ShowAppropriateChatDisplay)
127        chatHeader.on('click', helpers.ToggleChatWindow)
128        chatBody.find('#joinScreenForm').on('click', helpers.JoinChatSession)
129        chatBody.find('#messageOthers').on('submit', helpers.SendMessageToOthers)
130    }());

NOTE: Replace the YOUR_PUSHER_* keys with the one available on your Pusher dashboard. Refer to Get App Keys.

The code is rich in logic and a lot of things are happening here. Let’s go step by step.

We already have a site template, but you probably noticed that it does nothing. Every element is static. That’s why at the very beginning of our JavaScript code we are adding:

1const chatPage   = $(document)
2    const chatWindow = $('.chatbubble')
3    const chatHeader = chatWindow.find('.unexpanded')
4    const chatBody   = chatWindow.find('.chat-window')

The $(document) interface represents any web page loaded in the browser and serves as an entry point into the web page's content. For convenience and clean code, we've declare variables for most frequently used web page's elements.

Next, we create a helper object. In this object lies the meat of the script. In the helper's object we have a few methods which have a specific tasks:

  • ToggleChatWindow - Toggles the chat window display
  • ShowAppropriateChatDisplay - Decides which chat display to show depending on the action of the user
  • ShowChatInitiationDisplay - Shows the initial display for the chat window for the user to initiate a chat session
  • ShowChatRoomDisplay - Shows the chat window after the user has instantiated a new chat session
  • NewChatMessage - Adds a new chat message to the bubble chat UI. Verifies the sender name to choose the right style

We have three additional functions, but we will look at them in more detail as all of them are using Pusher. Our live chat room will use a Pusher Presence channel as the main communication medium because we can expose the additional feature of an awareness of who is subscribed to that channel.

  • As with Private channels, an HTTP Request is made to a configurable authorization URL to determine if the current user has permissions to access the channel. Therefore, we will need to set up an endpoint to authorize the user to enter the chat.

Go back to the ./server/server.js file and add:

1app.post('/pusher/auth', (request, response) => {
2     const socketId = request.body.socket_id;
3     const channel = request.body.channel_name;
4     const presenceData = {
5         user_id: request.session.username,
6         user_info: {
7             fullname: request.session.fullname,
8         }
9     };
10     const auth = pusher.authorizeChannel(socketId, channel, presenceData);
11     response.send(auth);
12    });

The function gets data from the browser session, as the participant is already logged in. The server just needs to call pusher.authorizeChannel to authorize the user.

  • We also have to remember that users may join the event website from multiple devices. That as a result may create multiple chat sessions. So we want to expand the connection-user relationship and add the user authentication endpoint. Keep editing ./server/server.js file and paste the following:
1app.post("/pusher/user-auth", (request, response) => {
2        const socketId = request.body.socket_id;
3        const userData = {
4            id: request.session.username,
5            email: request.session.email,
6            fullname: request.session.fullname,
7        };
8        const authUser = pusher.authenticateUser(socketId, userData);
9        response.send(authUser);
10    });

Learn more about User Authentication.

  • Now we can focus again on the ./public/landing/app.js and its helper function explanations. We’ve started the file by instantiating a Pusher object and declaring appropriate auth-endpoints.

LogIntoChatSession helper function starts a new chat session by calling pusher.signin() and subscribing to the chat channel.

SendMessageToOthers sends a chat message to the server via the chat channel and calls the other helper function to properly display the sent message.

Listen function binds callbacks to receive messages sent on the chat channel and directly to the user.

Send live chat messages

At this point, we are done with building our group chat.

To test the app, we need to start up our Express server by executing the command below (from the ./server directory):

    node server.js

The app should be running now and can be accessed through http://localhost:3000.

Go on and try the app out. Log in as regular participants in different browser windows and start chat sessions.

You should get something similar to the demo:

pusher-channels-tutorial-chat-app-widget-nodejs-javascript-two-chat-users

NOTE: The list of messages exists briefly. If the user refreshes the browser, messages will go away. We don’t store messages on your behalf. This has the added benefit that you can add whatever post-processing logic you like, such as link detection or swear words.

Step 7: Build admin dashboard

As the last big part of this tutorial, let’s create a basic admin dashboard.

This can be made accessible to specific users only. In our case, we will just hardcode the admin user’s name.

We want to keep our users safe, so we want to have ways of dealing with people who are spoiling the fun with spam. We will have a two-part approach. First, we will warn users directly, and then we will terminate their connections.

Create CSS for admin dashboard

Let’s write the style for the admin page.

  • Navigate to the ./public/admin directory and create admin.css file:
1cd ../public/admin
2    touch admin.css
  • Edit the admin.css file and add:
1/* pusher-event-chat/public/admin/admin.css */
2    body {
3        padding-top: 3.5rem;
4    }
5
6    h1 {
7        padding-bottom: 9px;
8        margin-bottom: 20px;
9        border-bottom: 1px solid #eee;
10    }
11
12    .sidebar {
13        position: fixed;
14        top: 51px;
15        bottom: 0;
16        left: 0;
17        z-index: 1000;
18        padding: 20px 0;
19        overflow-x: hidden;
20        overflow-y: auto;
21        border-right: 1px solid #eee;
22    }
23
24    .sidebar .nav {
25        margin-bottom: 20px;
26    }
27
28    .sidebar .nav-item {
29        width: 100%;
30    }
31
32    .sidebar .nav-item + .nav-item {
33        margin-left: 0;
34    }
35
36    .sidebar .nav-link {
37        border-radius: 0;
38    }
39
40    .placeholders {
41        padding-bottom: 3rem;
42    }
43
44    .placeholder img {
45        padding-top: 1.5rem;
46        padding-bottom: 1.5rem;
47    }
48
49    tr .sender {
50        font-size: 12px;
51        font-weight: 600;
52    }
53
54    tr .sender span {
55        color: #676767;
56    }
57
58    .response {
59        display: none;
60    }

Create admin dashboard with HTML

This page will have a simple table view showing the live chat, list of active participants, and joined since admin was active.

NOTE: The list of active users is ephemeral, if the browser is refreshed, they will go away. For a complete list of chat users, the Admin had to be logged in before users joined.

  • Create admin.html file
    touch admin.html
  • Update the admin.html with the following code:
1<!-- pusher-event-chat/public/admin/admin.html -->
2<!DOCTYPE html>
3    <html lang="en">
4    <head>
5<meta charset="utf-8">
6<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7<title>Real-time tech event | Admin </title>
8<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
9<link rel="stylesheet" href="./admin/admin.css" >
10    </head>
11    <body>
12
13<header>
14    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
15        <a class="navbar-brand" href="#">Admin dashboard</a>
16    </nav>
17</header>
18
19<div class="container-fluid">
20    <div class="row" id="mainrow">
21        <nav class="col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar">
22            <ul class="nav nav-pills flex-column" id="participants">
23            </ul>
24        </nav>
25        <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" id="main">
26            <h1>Participants</h1>
27            <p>👈 Select a chat participant to warn or immediately dismiss</p>
28            <p> </p>
29            <h5 id="participant-name"></h5>
30            <p> </p>
31            <form id="warnParticipant">
32                    <button type="button" class="btn btn-block btn-primary">Send a Warning</button>
33            </form>
34            <p> </p>
35            <form id="dismissParticipant">
36                <button type="button" class="btn btn-block btn-primary">Terminate User Connections </button>
37            </form>
38            <p> </p>
39            <div class="chat" style="margin-bottom:150px">
40                <div class="response">
41                    <form id="chatMessage">
42                        <div class="form-group">
43                            <input type="text" placeholder="Enter Message" class="form-control" name="message" />
44                        </div>
45                    </form>
46                </div>
47                <div class="table-responsive">
48                <table class="table table-striped">
49                    <tbody id="chat-msgs">
50                    </tbody>
51                </table>
52            </div>
53        </main>
54    </div>
55</div>
56<script src="https://js.pusher.com/7.1.0-beta/pusher.min.js"></script>
57<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
58<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
59<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
60<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
61<script type="text/javascript" src="./admin/admin.js"></script>
62
63    </body>
64    </html>

Warn and remove misbehaving user

  • In the same directory ./public/admin create admin.js file:
    touch admin.js
  • Start editing the file by adding the following code:
1// pusher-event-chat/public/admin/admin.js
2    (function() {
3
4        'use strict';
5
6        var pusher = new Pusher('YOUR_PUSHER_APP_KEY', {
7            authEndpoint: '/pusher/auth',
8            cluster: 'YOUR_PUSHER_APP_CLUSTER',
9            forceTLS: true,
10        });
11
12        let chat = {
13            subscribedUsers: [],
14            currentParticipant: ''
15        }
16
17        var chatChannel = pusher.subscribe('presence-groupChat');
18        const messageEventName = 'client-new-message'
19
20        const chatBody = $(document)
21        const participantsList = $('#participants')
22        const chatMessage = $('#chatMessage')
23
24        const helpers = {
25
26            ClearChatMessages: () => $('#chat-msgs').html(''),
27
28            DisplayChatMessage: (message) => {
29                $('.response').show()
30                $('#chat-msgs').prepend(
31                    `<tr>
32                            <td>
33                                <div class="sender">${message.sender} @ <span class="date">${message.createdAt}</span></div>
34                                <div class="message">${message.text}</div>
35                            </td>
36                        </tr>`
37                )
38            },
39
40            LoadParticipant: evt => {
41                chat.currentParticipant = evt.target.dataset.roomId
42                if (chat.currentParticipant !== undefined) {
43                    $('#participant-name').text(evt.target.dataset.roomId)
44
45                    chatBody.find('#warnParticipant').show()
46                    chatBody.find('#dismissParticipant').show()
47
48                    chatBody.find('#dismissParticipant').off('click').on('click', helpers.TerminateUserConnection)
49                    chatBody.find('#warnParticipant').off('click').on('click', helpers.SendWarning)
50                }
51                evt.preventDefault()
52            },
53
54            ChatMessage: evt => {
55                evt.preventDefault()
56                let createdAt = new Date()
57                createdAt = createdAt.toLocaleString()
58                const message = $('#chatMessage input').val().trim()
59                chatChannel.trigger(messageEventName, {
60                    'sender': 'Admin',
61                    'text': message,
62                });
63                helpers.DisplayChatMessage({
64                    'sender': 'Admin',
65                    'text': message,
66                    'createdAt': createdAt
67                })
68
69                $('#chatMessage input').val('')
70            },
71
72            SendWarning: evt => {
73                if (chat.currentParticipant !== undefined) {
74                    axios.post('/warn', {
75                        "user_id": chat.currentParticipant
76                    }).then(response => {
77                        console.log(chat.currentParticipant + ' warned')
78                    })
79                }
80                evt.preventDefault()
81            },
82
83            TerminateUserConnection: evt => {
84                if (chat.currentParticipant !== undefined) {
85                    axios.post('/terminate', {
86                        "user_id": chat.currentParticipant
87                    }).then(response => {
88                        console.log(chat.currentParticipant + ' terminated')
89                    })
90
91                    chatBody.find('#warnParticipant').hide()
92                    chatBody.find('#dismissParticipant').hide()
93                    $('#participant-name').text('')
94                    chat.currentParticipant = ''
95                }
96                evt.preventDefault()
97            },
98
99            UpdateParticipantsList: (activeParticipants) => {
100                let uniqueActiveParticipants = [...new Set(activeParticipants)];
101                uniqueActiveParticipants.forEach(function(user) {
102                    $('#participants').append(
103                        `<li class="nav-item"><a data-room-id="${user.id}" class="nav-link" href="#">${user.info.fullname}</a></li>`
104                    )
105                })
106            }
107        }
108
109        chatChannel.bind("pusher:member_added", (member) => {
110            chat.subscribedUsers.push(member);
111            $('#participants').html("");
112            helpers.UpdateParticipantsList(chat.subscribedUsers)
113        });
114
115        chatChannel.bind("pusher:member_removed", (member) => {
116            var remainingUsers = chat.subscribedUsers.filter(data => data.id != member.id);
117            $('#participants').html("");
118            chat.subscribedUsers = remainingUsers
119            helpers.UpdateParticipantsList(remainingUsers)
120        });
121
122        chatChannel.bind(messageEventName, function(data) {
123            helpers.DisplayChatMessage(data)
124        })
125
126        chatBody.find('#warnParticipant').hide()
127        chatBody.find('#dismissParticipant').hide()
128
129        chatMessage.on('submit', helpers.ChatMessage)
130        participantsList.on('click', 'li', helpers.LoadParticipant)
131    }())

NOTE: Replace the YOUR_PUSHER_* keys with the one available on your Pusher dashboard. See Pusher Get App Keys.

The script looks similar to the app.js script. The helper object contains the following functions:

  • ClearChatMessages - Clears the chat window
  • DisplayChatMessage - Displays new chat messages
  • LoadParticipant - Shows a user_id and 2 moderation buttons after a particular participant is selected
  • ChatMessage - Sends a message to the chat room using Pusher Channels

When you press the warning button, our server warns the user with the given ID.

1SendWarning: evt => {
2        if (chat.currentParticipant !== undefined) {
3            axios.post('/warn', {
4                "user_id": chat.currentParticipant
5            }).then(response => {
6                console.log(chat.currentParticipant + ' warned')
7            })
8        }
9        evt.preventDefault()
10    },

In practice, this involves calling pusher.sendToUser() with a warning message.

  • Update ./server/server.js with the following code:
1const warningEvent = 'client-warn-user'
2    const warningMessage = 'This is your first warning. Further misbehaving will lead to your removal from the event.'
3    app.post('/warn', (request, response) => {
4        const warnResp = pusher.sendToUser(request.body.user_id, warningEvent, {
5            message: warningMessage
6        });
7        response.send(warnResp);
8    });

In our client code for the homepage, we’ve bound alert() with the warning message functionality. The alert() will only be triggered in the browser sessions of the user with the specific ID. In a live example, you might want to allow admins to provide a reason that will be sent out in the message to the user.

  • If a participant continues to misbehave or spam, we will allow our admins to click terminate connections. This sends a request to the server to execute an API call to the Pusher servers. This terminates all connections that have been authenticated with the specific ID. Update ./server/server.js with the following code:
1const terminateEvent = 'client-terminate-user'
2    const terminateMessage = 'Your chat sessions have been terminated by the Admin.'
3    app.post('/terminate', (request, response) => {
4        pusher.sendToUser(request.body.user_id, terminateEvent, {
5            message: terminateMessage
6        });
7        const terminateResp = pusher.terminateUserConnections(request.body.user_id);
8        response.send(terminateResp)
9    });

NOTE: This will not prevent this user from signing in again. By design, we don't permanently store your chat users. You will have to modify your server code to terminate that session and prevent further logins from that user.

The awareness of active chat users is based on the Pusher Presence channel events: pusher:member_added and pusher:member_removed.

The pusher:member_added event is triggered when a user joins a channel. It’s quite possible that a user has multiple connections to the same channel (for example, by having multiple browser tabs open) and in this case, the events will only be triggered when the first tab is opened.

pusher-channels-tutorial-send-a-warning-terminate-chat-user

Step 8: Test app

Here we are. We are done with building the app!

To test the app, start up our server by executing the command below, from the ./server directory:

   node server.js

The app should be running now and you can access it through http://localhost:3000.

  • Open at least three different browser windows.
  • Log in as the admin in one window and as other regular participants in other windows. You can also log in using the same credentials in multiple windows to check how the demo app works with multiple connections for the same user.
  • Now you can play around and test. Chat with different users. Send messages from the admin dashboard.
  • Choose one active participant and click Send a Warning. Check the browser, you should see alerts there if you are logged in as that warned user.
  • Choose one active participant and click Terminate User Connections. Go and check browser windows for the given user. The chat window should be unavailable.

NOTE: The user can join the chat again once the page is refreshed. You will have to modify your server code to terminate that session and prevent further logins from that user.

pusher-channels-tutorial-chat-app-widget-nodejs-javascript-two-chat-users

See it all in action

Check out the Github repo for this project to see the demo code altogether.

This tutorial app gives you an idea how to implement more user control in your applications with Pusher Channels.

pusher-channels-admin-interaction-with-user

Pro tips

Our tutorials serve to showcase what can be done with our developer APIs. To further inspire you, here are a few ways how you can level up this use case!

  • Permanently store messages for users can view their chat history
  • Monitor and store signed-in participants for admin overview
  • Add a user search feature
  • Automate warnings and implement content filtering once messages are published
  • Add JWT for users authorization

Pusher provides everything you need to build dynamic realtime for your apps. Sign up for a Pusher account to start building chat with Channels or show us what you've already created using Pusher!