Build a chat app for a live event with Node.js, MySQL and Pusher Channels Part 2: Adding chat functionality and admin dashboard
A basic understanding of Node.js and JavaScript (ES6 syntax) would help you with this tutorial. However, it is not a requirement as this is a step by-step guide. You will need to have Node.js, NPM, and MySQL installed on your machine.
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 createlanding.css
file:
cd ../public/landing
touch landing.css
- Edit the
landing.css
file and add:
/* pusher-event-chat/public/landing/landing.css */
a,
a:focus,
a:hover {
color: #fff;
}
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:focus {
color: #333;
text-shadow: none;
background-color: #fff;
border: .05rem solid #fff;
}
html,
body {
height: 100%;
background-color: #435165;
}
body {
color: #fff;
text-align: center;
text-shadow: 0 .05rem .1rem rgba(0,0,0,.5);
}
.site-wrapper {
display: table;
width: 100%;
height: 100%; /* For at least Firefox */
min-height: 100%;
box-shadow: inset 0 0 5rem rgba(0,0,0,.5);
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.site-wrapper-inner {
display: table-cell;
vertical-align: top;
}
.cover-container {
margin-right: auto;
margin-left: auto;
}
.inner {
padding: 2rem;
}
.masthead {
margin-bottom: 2rem;
}
.masthead-brand {
margin-bottom: 0;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
color: rgba(255,255,255,.5);
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255,255,255,.25);
}
.nav-masthead .nav-link + .nav-link {
margin-left: 1rem;
}
.nav-masthead .active {
color: #fff;
border-bottom-color: #fff;
}
@media (min-width: 48em) {
.masthead-brand {
float: left;
}
.nav-masthead {
float: right;
}
}
/*
* Cover
*/
.cover {
padding: 0 1.5rem;
}
.cover .btn-lg {
padding: .75rem 1.25rem;
font-weight: 700;
}
.mastfoot {
color: rgba(255,255,255,.5);
}
@media (min-width: 40em) {
.masthead {
position: fixed;
top: 0;
}
.mastfoot {
position: fixed;
bottom: 0;
}
.site-wrapper-inner {
vertical-align: middle;
}
/* Handle the widths */
.masthead,
.mastfoot,
.cover-container {
width: 100%;
}
}
@media (min-width: 62em) {
.masthead,
.mastfoot,
.cover-container {
width: 42rem;
}
}
.chatbubble {
position: fixed;
bottom: 0;
right: 30px;
transform: translateY(300px);
transition: transform .3s ease-in-out;
}
.chatbubble.opened {
transform: translateY(0)
}
.chatbubble .unexpanded {
display: block;
background-color: #e23e3e;
padding: 10px 15px 10px;
position: relative;
cursor: pointer;
width: 350px;
border-radius: 10px 10px 0 0;
}
.chatbubble .expanded {
height: 300px;
width: 350px;
background-color: #fff;
text-align: left;
padding: 10px;
color: #333;
text-shadow: none;
font-size: 14px;
}
.chatbubble .chat-window {
overflow: auto;
}
.chatbubble .loader-wrapper {
margin-top: 50px;
text-align: center;
}
.chatbubble .messages {
display: none;
list-style: none;
margin: 0 0 50px;
padding: 0;
}
.chatbubble .messages li {
width: 85%;
float: left;
padding: 10px;
border-radius: 5px 5px 5px 0;
font-size: 14px;
background: #c9f1e6;
margin-bottom: 10px;
}
.chatbubble .messages li .sender {
font-weight: 600;
}
.chatbubble .messages li.reply {
float: right;
text-align: right;
color: #fff;
background-color: #e33d3d;
border-radius: 5px 5px 0 5px;
}
.chatbubble .chats .input {
position: absolute;
bottom: 0;
padding: 10px;
left: 0;
width: 100%;
background: #f0f0f0;
display: none;
}
.chatbubble .chats .input .form-group {
width: 80%;
}
.chatbubble .chats .input input {
width: 100%;
}
.chatbubble .chats .input button {
width: 20%;
}
.chatbubble .chats {
display: none;
}
.chatbubble .join-screen {
margin-top: 20px;
display: none;
}
.chatbubble .chats.active,
.chatbubble .join-screen.active {
display: block;
}
/* Loader Credit: https://codepen.io/ashmind/pen/zqaqpB */
.chatbubble .loader {
color: #e23e3e;
font-family: Consolas, Menlo, Monaco, monospace;
font-weight: bold;
font-size: 10vh;
opacity: 0.8;
}
.chatbubble .loader span {
display: inline-block;
-webkit-animation: pulse 0.4s alternate infinite ease-in-out;
animation: pulse 0.4s alternate infinite ease-in-out;
}
.chatbubble .loader span:nth-child(odd) {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
@-webkit-keyframes pulse {
to {
-webkit-transform: scale(0.8);
transform: scale(0.8);
opacity: 0.5;
}
}
@keyframes pulse {
to {
-webkit-transform: scale(0.8);
transform: scale(0.8);
opacity: 0.5;
}
}
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 thepublic/landing
directory:
touch index.html
- Edit the
index.html
by adding the code:
<!-- pusher-event-chat/public/landing/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Real-time tech event</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="./landing/landing.css">
</head>
<body>
<div class="site-wrapper">
<div class="site-wrapper-inner">
<div class="cover-container">
<header class="masthead clearfix">
<div class="inner">
<h3 class="masthead-brand">;Real-time tech event</h3>
<nav class="nav nav-masthead">
<a class="nav-link active" href="#">Home</a>
<a class="nav-link" href="#">Schedule</a>
<a class="nav-link" href="#">Contact</a>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Real-time tech event</h1>
<p class="lead">;Powering realtime experiences for mobile and web!</p>
<p class="lead">
<a href="#" class="btn btn-lg btn-secondary">SCHEDULE</a>
</p>
</main>
<footer class="mastfoot">
</footer>
</div>
</div>
</div>
<div class="chatbubble">
<div class="unexpanded">
<div class="title">;Chat with other participants</div>
</div>
<div class="expanded chat-window">
<div class="join-screen container">
<form id="joinScreenForm">
<button type="button" class="btn btn-block btn-primary">Join Chat</button>
</form>
</div>
<div class="chats">
<div class="loader-wrapper">
<div class="loader">
<span>{</span><span>}</span>
</div>
</div>
<ul class="messages clearfix">
</ul>
<div class="input">
<form class="form-inline" id="messageOthers">
<div class="form-group">
<input type="text" autocomplete="off" class="form-control" id="newMessage" placeholder="Enter Message">
</div>
<button type="submit" class="btn btn-primary">Send</button>
</form>
</div>
</div>
</div>
</div>
<script src="https://js.pusher.com/7.1.0-beta/pusher.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script type="text/javascript" src="./landing/app.js"></script>
</body>
</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
createapp.js
file:
touch app.js
- Start editing the file by adding the following code:
// pusher-event-chat/public/landing/app.js
(function() {
'use strict';
var pusher = new Pusher("YOUR_PUSHER_APP_KEY", {
userAuthentication: {
endpoint: "/pusher/user-auth",
},
channelAuthorization: {
endpoint: "/pusher/auth",
},
cluster: 'YOUR_PUSHER_APP_CLUSTER',
forceTLS: true,
});
let chat = {
owner: undefined,
}
const messageEventName = 'client-new-message'
const chatChannelName = 'presence-groupChat'
const warningEvent = 'client-warn-user'
const terminateEvent = 'client-terminate-user'
const chatPage = $(document)
const chatWindow = $('.chatbubble')
const chatHeader = chatWindow.find('.unexpanded')
const chatBody = chatWindow.find('.chat-window')
let helpers = {
ToggleChatWindow: function() {
chatWindow.toggleClass('opened')
chatHeader.find('.title').text(
chatWindow.hasClass('opened') ? 'Minimize Chat Window' : 'Chat with other participants'
)
},
ShowAppropriateChatDisplay: function() {
(chat.owner) ? helpers.ShowChatRoomDisplay(): helpers.ShowChatInitiationDisplay()
},
ShowChatInitiationDisplay: function() {
chatBody.find('.chats').removeClass('active')
chatBody.find('.join-screen').addClass('active')
},
ShowChatRoomDisplay: function() {
chatBody.find('.chats').addClass('active')
chatBody.find('.join-screen').removeClass('active')
setTimeout(function() {
chatBody.find('.loader-wrapper').hide()
chatBody.find('.input, .messages').show()
}, 2000)
},
NewChatMessage: function(message) {
if (message !== undefined) {
const messageClass = message.sender !== chat.owner ? 'reply' : 'user'
chatBody.find('ul.messages').append(
`<li class="clearfix message ${messageClass}">
<div class="sender">${message.sender}</div>
<div class="message">${message.text}</div>
</li>`
)
chatBody.scrollTop(chatBody[0].scrollHeight)
}
},
SendMessageToOthers: function(evt) {
evt.preventDefault()
let createdAt = new Date()
createdAt = createdAt.toLocaleString()
const message = $('#newMessage').val().trim()
var channel = pusher.channel(chatChannelName);
channel.trigger(messageEventName, {
'sender': chat.owner,
'text': message,
'createdAt': createdAt
});
helpers.NewChatMessage({
'text': message,
'name': chat.owner,
'sender': chat.owner
})
console.log("Message added!")
$('#newMessage').val('')
},
JoinChatSession: function() {
chatBody.find('#joinScreenForm button').attr('disabled', true)
pusher.signin();
const channel = pusher.subscribe(chatChannelName);
channel.bind('pusher:subscription_succeeded', () => {
let me = channel.members.me
chat.owner = me.info.fullname
helpers.ShowAppropriateChatDisplay()
});
helpers.Listen()
},
Listen() {
const channel = pusher.channel(chatChannelName);
channel.bind(messageEventName, (data) => {
helpers.NewChatMessage(data)
})
pusher.user.bind(warningEvent, function(data) {
alert(JSON.stringify(data.message));
});
pusher.user.bind(terminateEvent, function(data) {
alert(JSON.stringify(data.message));
chat.owner = '';
helpers.ShowAppropriateChatDisplay()
});
}
}
chatPage.ready(helpers.ShowAppropriateChatDisplay)
chatHeader.on('click', helpers.ToggleChatWindow)
chatBody.find('#joinScreenForm').on('click', helpers.JoinChatSession)
chatBody.find('#messageOthers').on('submit', helpers.SendMessageToOthers)
}());
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:
const chatPage = $(document)
const chatWindow = $('.chatbubble')
const chatHeader = chatWindow.find('.unexpanded')
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 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:
app.post('/pusher/auth', (request, response) => {
const socketId = request.body.socket_id;
const channel = request.body.channel_name;
const presenceData = {
user_id: request.session.username,
user_info: {
fullname: request.session.fullname,
}
};
const auth = pusher.authorizeChannel(socketId, channel, presenceData);
response.send(auth);
});
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:
app.post("/pusher/user-auth", (request, response) => {
const socketId = request.body.socket_id;
const userData = {
id: request.session.username,
email: request.session.email,
fullname: request.session.fullname,
};
const authUser = pusher.authenticateUser(socketId, userData);
response.send(authUser);
});
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 callingpusher.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:
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 createadmin.css
file:
cd ../public/admin
touch admin.css
- Edit the
admin.cs
s file and add:
/* pusher-event-chat/public/admin/admin.css */
body {
padding-top: 3.5rem;
}
h1 {
padding-bottom: 9px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
padding: 20px 0;
overflow-x: hidden;
overflow-y: auto;
border-right: 1px solid #eee;
}
.sidebar .nav {
margin-bottom: 20px;
}
.sidebar .nav-item {
width: 100%;
}
.sidebar .nav-item + .nav-item {
margin-left: 0;
}
.sidebar .nav-link {
border-radius: 0;
}
.placeholders {
padding-bottom: 3rem;
}
.placeholder img {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
tr .sender {
font-size: 12px;
font-weight: 600;
}
tr .sender span {
color: #676767;
}
.response {
display: none;
}
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:
<!-- pusher-event-chat/public/admin/admin.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Real-time tech event | Admin </title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="./admin/admin.css" >
</head>
<body>
<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" href="#">Admin dashboard</a>
</nav>
</header>
<div class="container-fluid">
<div class="row" id="mainrow">
<nav class="col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar">
<ul class="nav nav-pills flex-column" id="participants">
</ul>
</nav>
<main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" id="main">
<h1>Participants</h1>
<p>👈 Select a chat participant to warn or immediately dismiss</p>
<p> </p>
<h5 id="participant-name"></h5>
<p> </p>
<form id="warnParticipant">
<button type="button" class="btn btn-block btn-primary">Send a Warning</button>
</form>
<p> </p>
<form id="dismissParticipant">
<button type="button" class="btn btn-block btn-primary">Terminate User Connections </button>
</form>
<p> </p>
<div class="chat" style="margin-bottom:150px">
<div class="response">
<form id="chatMessage">
<div class="form-group">
<input type="text" placeholder="Enter Message" class="form-control" name="message" />
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-striped">
<tbody id="chat-msgs">
</tbody>
</table>
</div>
</main>
</div>
</div>
<script src="https://js.pusher.com/7.1.0-beta/pusher.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script type="text/javascript" src="./admin/admin.js"></script>
</body>
</html>
Warn and remove misbehaving user
- In the same directory
./public/admin
createadmin.js
file:
touch admin.js
- Start editing the file by adding the following code:
// pusher-event-chat/public/admin/admin.js
(function() {
'use strict';
var pusher = new Pusher('YOUR_PUSHER_APP_KEY', {
authEndpoint: '/pusher/auth',
cluster: 'YOUR_PUSHER_APP_CLUSTER',
forceTLS: true,
});
let chat = {
subscribedUsers: [],
currentParticipant: ''
}
var chatChannel = pusher.subscribe('presence-groupChat');
const messageEventName = 'client-new-message'
const chatBody = $(document)
const participantsList = $('#participants')
const chatMessage = $('#chatMessage')
const helpers = {
ClearChatMessages: () => $('#chat-msgs').html(''),
DisplayChatMessage: (message) => {
$('.response').show()
$('#chat-msgs').prepend(
`<tr>
<td>
<div class="sender">${message.sender} @ <span class="date">${message.createdAt}</span></div>
<div class="message">${message.text}</div>
</td>
</tr>`
)
},
LoadParticipant: evt => {
chat.currentParticipant = evt.target.dataset.roomId
if (chat.currentParticipant !== undefined) {
$('#participant-name').text(evt.target.dataset.roomId)
chatBody.find('#warnParticipant').show()
chatBody.find('#dismissParticipant').show()
chatBody.find('#dismissParticipant').off('click').on('click', helpers.TerminateUserConnection)
chatBody.find('#warnParticipant').off('click').on('click', helpers.SendWarning)
}
evt.preventDefault()
},
ChatMessage: evt => {
evt.preventDefault()
let createdAt = new Date()
createdAt = createdAt.toLocaleString()
const message = $('#chatMessage input').val().trim()
chatChannel.trigger(messageEventName, {
'sender': 'Admin',
'text': message,
});
helpers.DisplayChatMessage({
'sender': 'Admin',
'text': message,
'createdAt': createdAt
})
$('#chatMessage input').val('')
},
SendWarning: evt => {
if (chat.currentParticipant !== undefined) {
axios.post('/warn', {
"user_id": chat.currentParticipant
}).then(response => {
console.log(chat.currentParticipant + ' warned')
})
}
evt.preventDefault()
},
TerminateUserConnection: evt => {
if (chat.currentParticipant !== undefined) {
axios.post('/terminate', {
"user_id": chat.currentParticipant
}).then(response => {
console.log(chat.currentParticipant + ' terminated')
})
chatBody.find('#warnParticipant').hide()
chatBody.find('#dismissParticipant').hide()
$('#participant-name').text('')
chat.currentParticipant = ''
}
evt.preventDefault()
},
UpdateParticipantsList: (activeParticipants) => {
let uniqueActiveParticipants = [...new Set(activeParticipants)];
uniqueActiveParticipants.forEach(function(user) {
$('#participants').append(
`<li class="nav-item"><a data-room-id="${user.id}" class="nav-link" href="#">${user.info.fullname}</a></li>`
)
})
}
}
chatChannel.bind("pusher:member_added", (member) => {
chat.subscribedUsers.push(member);
$('#participants').html("");
helpers.UpdateParticipantsList(chat.subscribedUsers)
});
chatChannel.bind("pusher:member_removed", (member) => {
var remainingUsers = chat.subscribedUsers.filter(data => data.id != member.id);
$('#participants').html("");
chat.subscribedUsers = remainingUsers
helpers.UpdateParticipantsList(remainingUsers)
});
chatChannel.bind(messageEventName, function(data) {
helpers.DisplayChatMessage(data)
})
chatBody.find('#warnParticipant').hide()
chatBody.find('#dismissParticipant').hide()
chatMessage.on('submit', helpers.ChatMessage)
participantsList.on('click', 'li', helpers.LoadParticipant)
}())
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 auser_id
and 2 moderation buttons after a particular participant is selectedChatMessage
- 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.
SendWarning: evt => {
if (chat.currentParticipant !== undefined) {
axios.post('/warn', {
"user_id": chat.currentParticipant
}).then(response => {
console.log(chat.currentParticipant + ' warned')
})
}
evt.preventDefault()
},
In practice, this involves calling pusher.sendToUser()
with a warning message.
- Update
./server/server.js
with the following code:
const warningEvent = 'client-warn-user'
const warningMessage = 'This is your first warning. Further misbehaving will lead to your removal from the event.'
app.post('/warn', (request, response) => {
const warnResp = pusher.sendToUser(request.body.user_id, warningEvent, {
message: warningMessage
});
response.send(warnResp);
});
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:
const terminateEvent = 'client-terminate-user'
const terminateMessage = 'Your chat sessions have been terminated by the Admin.'
app.post('/terminate', (request, response) => {
pusher.sendToUser(request.body.user_id, terminateEvent, {
message: terminateMessage
});
const terminateResp = pusher.terminateUserConnections(request.body.user_id);
response.send(terminateResp)
});
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.
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.
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.
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!
9 June 2022
by Agata Walukiewicz