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.
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.
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.
public/landing
directory and create landing.css
file:1cd ../public/landing 2 touch landing.css
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 }
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.index.html
file in the public/landing
directory: touch index.html
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.
./public/landing
create app.js
file: touch app.js
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 displayShowAppropriateChatDisplay
- Decides which chat display to show depending on the action of the userShowChatInitiationDisplay
- Shows the initial display for the chat window for the user to initiate a chat sessionShowChatRoomDisplay
- Shows the chat window after the user has instantiated a new chat sessionNewChatMessage
- Adds a new chat message to the bubble chat UI. Verifies the sender name to choose the right styleWe 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.
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.
./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.
./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.
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:
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.
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.
Let’s write the style for the admin page.
./public/admin
directory and create admin.css
file:1cd ../public/admin 2 touch admin.css
admin.cs
s 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 }
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.
admin.html
file touch admin.html
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>
./public/admin
create admin.js
file: touch admin.js
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 windowDisplayChatMessage
- Displays new chat messagesLoadParticipant
- Shows a user_id
and 2 moderation buttons after a particular participant is selectedChatMessage
- Sends a message to the chat room using Pusher ChannelsWhen 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.
./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.
./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.
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
.
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.
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.
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!
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!