Building quality digital products is a requirement toward acquiring long-term customers, but inefficient communication is an efficient way to lose them just as quickly as you gain them. The internet is currently the world’s largest marketplace and everyone is building something for an online audience to consume, however, it would be a shame if there isn’t a way to receive feedback or interact with customers in realtime.
In this tutorial, we will look at how we can create a realtime chat widget using Pusher, Python, and JavaScript. When we are done building, the final application will look and work like this:
In the image above, we can see a digital product called “SPIN” and it has a chat widget option for visiting customers to interact with. On the left browser window, a customer visits this website and fills in his/her details before submitting the form.
There is an admin on the right browser window who can see all connected customers and respond to all their messages accordingly, providing effective and realtime support.
To follow along with this tutorial, a basic knowledge of Python, Flask, JavaScript (ES6 syntax) and jQuery is required. You will also need the following installed:
Virtualenv is great for creating isolated Python environments, so we can install dependencies in an isolated environment, and not pollute our global packages directory.
Let’s install virtualenv
with this command:
$ pip install virtualenv
IMPORTANT: Virtualenv comes preinstalled with Python 3 so you may not need to install it if you are on this version.
Let’s create our project folder, and activate a virtual environment within it:
1$ mkdir python-pusher-chat-widget 2 $ cd python-pusher-chat-widget 3 $ virtualenv .venv 4 $ source .venv/bin/activate # Linux based systems 5 $ \path\to\env\Scripts\activate # Windows users
Now that we have the virtual environment setup, we can install Flask and the remaining dependencies with this command:
$ pip install flask flask-cors simplejson
We need to install the Pusher Channels library as we will need that for realtime updates.
To get started with Pusher Channels, sign up for a free Pusher account. Then go to the dashboard and create a new Channels app.
Follow the application creation wizard and then you should be given your application credentials, we will use this later in the article:
There’s one more thing we need to do here on this dashboard; because we will directly be triggering the message events on the client side of the application, we need to turn on a special feature that is turned off by default for security reasons. To learn more about triggering events on the client side, you can read the documentation here.
On the dashboard, click on App settings and scroll to the bottom of the page then select the option that says Enable client events:
Great, now let’s install the Channels Python library, so that we can use Pusher Channels in the application:
$ pip install pusher
Here’s a representation of the file/folder structure for this app:
1├── python-pusher-chat-widget 2 ├── app.py 3 ├── static 4 └── templates
The static
folder will contain the static files to be used as is defined by Flask standards. The templates
folder will hold the HTML templates. In our application, app.py
is the main entry point and will contain our server-side code.
Let’s create the app.py
file and then the static
and templates
folders.
Before we start writing code to determine how the frontend of our application will be rendered, let’s fully develop the backend and all of its endpoints so that the frontend has something to communicate with when we build it.
Let’s open the app.py
file and paste the following code:
1// File: ./app.py 2 3 from flask import Flask, render_template, request, jsonify, make_response, json 4 from flask_cors import CORS 5 from pusher import pusher 6 import simplejson 7 8 app = Flask(__name__) 9 cors = CORS(app) 10 app.config['CORS_HEADERS'] = 'Content-Type' 11 12 # configure pusher object 13 pusher = pusher.Pusher( 14 app_id='PUSHER_APP_ID', 15 key='PUSHER_APP_KEY', 16 secret='PUSHER_APP_SECRET', 17 cluster='PUSHER_APP_CLUSTER', 18 ssl=True) 19 20 @app.route('/') 21 def index(): 22 return render_template('index.html') 23 24 @app.route('/admin') 25 def admin(): 26 return render_template('admin.html') 27 28 @app.route('/new/guest', methods=['POST']) 29 def guestUser(): 30 data = request.json 31 pusher.trigger(u'general-channel', u'new-guest-details', { 32 'name' : data['name'], 33 'email' : data['email'] 34 }) 35 return json.dumps(data) 36 37 @app.route("/pusher/auth", methods=['POST']) 38 def pusher_authentication(): 39 auth = pusher.authenticate(channel=request.form['channel_name'],socket_id=request.form['socket_id']) 40 return json.dumps(auth) 41 42 if __name__ == '__main__': 43 app.run(host='0.0.0.0', port=5000, debug=True)
Replace the
PUSHER_APP_*
keys with the values on your Pusher dashboard.
The logic for this application is simple, we will require a Pusher public channel so that whenever a new customer connects with the chat widget, their details are sent over to the admin (using that public channel) and the admin can subscribe to a private channel (the customer will have to subscribe to this private channel too) using the customer’s email as a unique ID. The admin and that customer can further engage in one to one messaging over that private channel.
Let’s go over the code in the app.py
file to see how it satisfies the logic we just discussed. We first imported all the required packages, then registered a new Pusher instance. Next, we declared four endpoints:
/
- This endpoint returns the static HTML template that defines the homepage of this app.
/admin
- This endpoint returns the static HTML template that defines the admin dashboard.
/new/guest/
- This endpoint receives a POST request containing the details of a new customer and pushes it to the public channel — general-channel — in a “new-guest-details” event. The admin on the other side responds to this event by subscribing to a private channel using the user’s email as the unique ID.
We used the trigger method on the Pusher instance here, the trigger method has the following syntax: pusher.trigger("a_channel", "an_event", {key: "data"})
. You can find the docs for the Pusher Python library here to get more information on configuring and using Pusher in Python.
/pusher/auth
- This endpoint is responsible for enabling our applications to connect to private channels. Without this auth
endpoint, we will not be authorized to send client events over private channels. You can learn more about private channels here and about how to authorize users here.
In this section, we are going to do the following things:
index.html
and admin.html
in the templates
directory.img
directory in the static
directory and add a background image called bg.jpg
inside it. You can find and download free images here.css
and js
directory within the static
directory. In the css
directory, create a new admin.css
and app.css
file. In the js
directory, create a new admin.js
and app.js
file.We will be using Bootstrap as a base style for the application. We will also be using other third-party libraries so let’s fetch the source and place them in the appropriate directory inside the static
directory.
Add these files in the static/js
directory:
axios.js
- download the source code here.bootstrap.js
- download the source code here.jquery.js
- download the source code here.popper.js
- download the source code here.Add this file in the static/css
directory:
bootstrap.css
- download the source code here.The new folder structure should be:
1├── python-pusher-chat-widget 2 ├── app.py 3 ├── static 4 ├── css 5 ├── admin.css 6 ├── app.css 7 ├── bootstrap.css 8 ├── img 9 ├── bg.jpg 10 ├── js 11 ├── admin.js 12 ├── app.js 13 ├── axios.js 14 ├── bootstrap.js 15 ├── jquery.js 16 ├── popper.js 17 ├── templates 18 ├── admin.html 19 ├── index.html
If you currently have this folder structure then you are good to go!
In the templates/index.html
file, paste the following code:
1<!-- File: ./templates/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>Spin Spinner Spinnest!</title> 8 <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.css') }}"> 9 <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}"> 10 </head> 11 <body> 12 <div class="site-wrapper"> 13 <div class="site-wrapper-inner"> 14 <div class="cover-container"> 15 16 <header class="masthead clearfix"> 17 <div class="inner"> 18 <h3 class="masthead-brand">SPIN</h3> 19 <nav class="nav nav-masthead"> 20 <a class="nav-link active" href="#">Home</a> 21 <a class="nav-link" href="#">Features</a> 22 <a class="nav-link" href="#">Contact</a> 23 </nav> 24 </div> 25 </header> 26 27 <main role="main" class="inner cover"> 28 <h1 class="cover-heading">SPIN</h1> 29 <p class="lead">SPIN is a simple realtime chat widget powered by Pusher.</p> 30 <p class="lead"> 31 <a href="#" class="btn btn-lg btn-secondary">GO for a SPIN?</a> 32 </p> 33 </main> 34 35 <footer class="mastfoot"> 36 </footer> 37 38 </div> 39 </div> 40 </div> 41 42 <div class="chatbubble"> 43 <div class="unexpanded"> 44 <div class="title">Chat with Support</div> 45 </div> 46 <div class="expanded chat-window"> 47 <div class="login-screen container"> 48 <form id="loginScreenForm"> 49 <div class="form-group"> 50 <input type="text" class="form-control" id="fullname" placeholder="Name*" required> 51 </div> 52 <div class="form-group"> 53 <input type="email" class="form-control" id="email" placeholder="Email Address*" required> 54 </div> 55 <button type="submit" class="btn btn-block btn-primary">Start Chat</button> 56 </form> 57 </div> 58 <div class="chats"> 59 <div class="loader-wrapper"> 60 <div class="loader"> 61 <span>{</span><span>}</span> 62 </div> 63 </div> 64 <ul class="messages clearfix"> 65 </ul> 66 <div class="input"> 67 <form class="form-inline" id="messageSupport"> 68 <div class="form-group"> 69 <input type="text" autocomplete="off" class="form-control" id="newMessage" placeholder="Enter Message"> 70 </div> 71 <button type="submit" class="btn btn-primary">Send</button> 72 </form> 73 </div> 74 </div> 75 </div> 76 </div> 77 78 <script src="https://js.pusher.com/4.0/pusher.min.js"></script> 79 <script src="{{ url_for('static', filename='js/jquery.js') }}"></script> 80 <script src="{{ url_for('static', filename='js/popper.js') }}"></script> 81 <script src="{{ url_for('static', filename='js/bootstrap.js') }}"></script> 82 <script src="{{ url_for('static', filename='js/axios.js') }}"></script> 83 <script src="{{ url_for('static', filename='js/app.js') }}"></script> 84 </body> 85 </html>
In this file, we have the HTML for the homepage. We also used Flask’s url_for
function to dynamically link to all the local scripts and styles that we created.
Because we require our application to send and receive messages in realtime, we imported the official Pusher JavaScript library with this line of code:
<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
We included some custom classes within the HTML elements, however, these classes will be useless if we do not define them in the matching CSS file, open the static/css/app.css
file and paste the following code:
1/* File: static/css/app.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: #333; 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: url(../img/bg.jpg); 36 background-size: cover; 37 background-repeat: no-repeat; 38 background-position: center; 39 } 40 41 .site-wrapper-inner { 42 display: table-cell; 43 vertical-align: top; 44 } 45 46 .cover-container { 47 margin-right: auto; 48 margin-left: auto; 49 } 50 51 .inner { 52 padding: 2rem; 53 } 54 55 .masthead { 56 margin-bottom: 2rem; 57 } 58 59 .masthead-brand { 60 margin-bottom: 0; 61 } 62 63 .nav-masthead .nav-link { 64 padding: .25rem 0; 65 font-weight: 700; 66 color: rgba(255,255,255,.5); 67 background-color: transparent; 68 border-bottom: .25rem solid transparent; 69 } 70 71 .nav-masthead .nav-link:hover, 72 .nav-masthead .nav-link:focus { 73 border-bottom-color: rgba(255,255,255,.25); 74 } 75 76 .nav-masthead .nav-link + .nav-link { 77 margin-left: 1rem; 78 } 79 80 .nav-masthead .active { 81 color: #fff; 82 border-bottom-color: #fff; 83 } 84 85 @media (min-width: 48em) { 86 .masthead-brand { 87 float: left; 88 } 89 90 .nav-masthead { 91 float: right; 92 } 93 } 94 95 .cover { 96 padding: 0 1.5rem; 97 } 98 99 .cover .btn-lg { 100 padding: .75rem 1.25rem; 101 font-weight: 700; 102 } 103 104 .mastfoot { 105 color: rgba(255,255,255,.5); 106 } 107 108 @media (min-width: 40em) { 109 .masthead { 110 position: fixed; 111 top: 0; 112 } 113 114 .mastfoot { 115 position: fixed; 116 bottom: 0; 117 } 118 119 .site-wrapper-inner { 120 vertical-align: middle; 121 } 122 123 .masthead, 124 .mastfoot, 125 .cover-container { 126 width: 100%; 127 } 128 } 129 130 @media (min-width: 62em) { 131 .masthead, 132 .mastfoot, 133 .cover-container { 134 width: 42rem; 135 } 136 } 137 138 .chatbubble { 139 position: fixed; 140 bottom: 0; 141 right: 30px; 142 transform: translateY(300px); 143 transition: transform .3s ease-in-out; 144 } 145 146 .chatbubble.opened { 147 transform: translateY(0) 148 } 149 150 .chatbubble .unexpanded { 151 display: block; 152 background-color: #e23e3e; 153 padding: 10px 15px 10px; 154 position: relative; 155 cursor: pointer; 156 width: 350px; 157 border-radius: 10px 10px 0 0; 158 } 159 160 .chatbubble .expanded { 161 height: 300px; 162 width: 350px; 163 background-color: #fff; 164 text-align: left; 165 padding: 10px; 166 color: #333; 167 text-shadow: none; 168 font-size: 14px; 169 } 170 171 .chatbubble .chat-window { 172 overflow: auto; 173 } 174 175 .chatbubble .loader-wrapper { 176 margin-top: 50px; 177 text-align: center; 178 } 179 180 .chatbubble .messages { 181 display: none; 182 list-style: none; 183 margin: 0 0 50px; 184 padding: 0; 185 } 186 187 .chatbubble .messages li { 188 width: 85%; 189 float: left; 190 padding: 10px; 191 border-radius: 5px 5px 5px 0; 192 font-size: 14px; 193 background: #c9f1e6; 194 margin-bottom: 10px; 195 } 196 197 .chatbubble .messages li .sender { 198 font-weight: 600; 199 } 200 201 .chatbubble .messages li.support { 202 float: right; 203 text-align: right; 204 color: #fff; 205 background-color: #e33d3d; 206 border-radius: 5px 5px 0 5px; 207 } 208 209 .chatbubble .chats .input { 210 position: absolute; 211 bottom: 0; 212 padding: 10px; 213 left: 0; 214 width: 100%; 215 background: #f0f0f0; 216 display: none; 217 } 218 219 .chatbubble .chats .input .form-group { 220 width: 80%; 221 } 222 223 .chatbubble .chats .input input { 224 width: 100%; 225 } 226 227 .chatbubble .chats .input button { 228 width: 20%; 229 } 230 231 .chatbubble .chats { 232 display: none; 233 } 234 235 .chatbubble .login-screen { 236 margin-top: 20px; 237 display: none; 238 } 239 240 .chatbubble .chats.active, 241 .chatbubble .login-screen.active { 242 display: block; 243 } 244 245 /* Loader Credit: https://codepen.io/ashmind/pen/zqaqpB */ 246 .chatbubble .loader { 247 color: #e23e3e; 248 font-family: Consolas, Menlo, Monaco, monospace; 249 font-weight: bold; 250 font-size: 10vh; 251 opacity: 0.8; 252 } 253 254 .chatbubble .loader span { 255 display: inline-block; 256 -webkit-animation: pulse 0.4s alternate infinite ease-in-out; 257 animation: pulse 0.4s alternate infinite ease-in-out; 258 } 259 260 .chatbubble .loader span:nth-child(odd) { 261 -webkit-animation-delay: 0.4s; 262 animation-delay: 0.4s; 263 } 264 265 @-webkit-keyframes pulse { 266 to { 267 -webkit-transform: scale(0.8); 268 transform: scale(0.8); 269 opacity: 0.5; 270 } 271 } 272 273 @keyframes pulse { 274 to { 275 -webkit-transform: scale(0.8); 276 transform: scale(0.8); 277 opacity: 0.5; 278 } 279 }
In the templates/admin.html
file, paste the following code:
1<!-- File: templates/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>Admin</title> 8 <link href="{{ url_for('static', filename='css/bootstrap.css') }}" rel="stylesheet"> 9 <link href="{{ url_for('static', filename='css/admin.css') }}" rel="stylesheet"> 10 </head> 11 <body> 12 <header> 13 <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> 14 <a class="navbar-brand" href="#">Dashboard</a> 15 </nav> 16 </header> 17 18 <div class="container-fluid"> 19 <div class="row" id="mainrow"> 20 <nav class="col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar"> 21 <ul class="nav nav-pills flex-column" id="rooms"> 22 </ul> 23 </nav> 24 <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" id="main"> 25 <h1>Chats</h1> 26 <p>👈 Select a chat to load the messages</p> 27 <p> </p> 28 <div class="chat" style="margin-bottom:150px"> 29 <h5 id="room-title"></h5> 30 <p> </p> 31 <div class="response"> 32 <form id="replyMessage"> 33 <div class="form-group"> 34 <input type="text" placeholder="Enter Message" class="form-control" name="message" /> 35 </div> 36 </form> 37 </div> 38 <div class="table-responsive"> 39 <table class="table table-striped"> 40 <tbody id="chat-msgs"> 41 </tbody> 42 </table> 43 </div> 44 </main> 45 </div> 46 </div> 47 48 <script src="https://js.pusher.com/4.0/pusher.min.js"></script> 49 <script src="{{ url_for('static', filename='js/jquery.js') }}"></script> 50 <script src="{{ url_for('static', filename='js/popper.js') }}"></script> 51 <script src="{{ url_for('static', filename='js/bootstrap.js') }}"></script> 52 <script src="{{ url_for('static', filename='js/axios.js') }}"></script> 53 <script src="{{ url_for('static', filename='js/admin.js') }}"></script> 54 </body> 55 </html>
Open the static/css/admin.css
file and paste the following code:
1/* File: static/css/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 }
In this section, we will write the script that works with the homepage and supports the customers’ functions. This script will define the logic that will enable a customer to submit the form after filling in his/her details and everything else.
We will define some helper functions within an IIFE and these functions will run on the occurrence of several DOM events and possibly pass on the execution to other helper functions.
Open the app.js
file and paste the following:
1// File: static/js/app.js 2 (function() { 3 'use strict'; 4 5 var pusher = new Pusher('PUSHER_APP_KEY', { 6 authEndpoint: '/pusher/auth', 7 cluster: 'PUSHER_APP_CLUSTER', 8 encrypted: true 9 }); 10 11 // ---------------------------------------------------- 12 // Chat Details 13 // ---------------------------------------------------- 14 15 let chat = { 16 name: undefined, 17 email: undefined, 18 myChannel: undefined, 19 } 20 21 22 // ---------------------------------------------------- 23 // Targeted Elements 24 // ---------------------------------------------------- 25 26 const chatPage = $(document) 27 const chatWindow = $('.chatbubble') 28 const chatHeader = chatWindow.find('.unexpanded') 29 const chatBody = chatWindow.find('.chat-window') 30 31 32 // ---------------------------------------------------- 33 // Register helpers 34 // ---------------------------------------------------- 35 36 let helpers = { 37 38 // ---------------------------------------------------- 39 // Toggles the display of the chat window. 40 // ---------------------------------------------------- 41 42 ToggleChatWindow: function () { 43 chatWindow.toggleClass('opened') 44 chatHeader.find('.title').text( 45 chatWindow.hasClass('opened') ? 'Minimize Chat Window' : 'Chat with Support' 46 ) 47 }, 48 49 // -------------------------------------------------------------------- 50 // Show the appropriate display screen. Login screen or Chat screen. 51 // -------------------------------------------------------------------- 52 53 ShowAppropriateChatDisplay: function () { 54 (chat.name) ? helpers.ShowChatRoomDisplay() : helpers.ShowChatInitiationDisplay() 55 }, 56 57 // ---------------------------------------------------- 58 // Show the enter details form. 59 // ---------------------------------------------------- 60 61 ShowChatInitiationDisplay: function () { 62 chatBody.find('.chats').removeClass('active') 63 chatBody.find('.login-screen').addClass('active') 64 }, 65 66 // ---------------------------------------------------- 67 // Show the chat room messages display. 68 // ---------------------------------------------------- 69 70 ShowChatRoomDisplay: function () { 71 chatBody.find('.chats').addClass('active') 72 chatBody.find('.login-screen').removeClass('active') 73 74 setTimeout(function(){ 75 chatBody.find('.loader-wrapper').hide() 76 chatBody.find('.input, .messages').show() 77 }, 2000) 78 }, 79 80 // ---------------------------------------------------- 81 // Append a message to the chat messages UI. 82 // ---------------------------------------------------- 83 84 NewChatMessage: function (message) { 85 if (message !== undefined) { 86 const messageClass = message.sender !== chat.email ? 'support' : 'user' 87 88 chatBody.find('ul.messages').append( 89 `<li class="clearfix message ${messageClass}"> 90 <div class="sender">${message.name}</div> 91 <div class="message">${message.text}</div> 92 </li>` 93 ) 94 95 96 chatBody.scrollTop(chatBody[0].scrollHeight) 97 } 98 }, 99 100 // ---------------------------------------------------- 101 // Send a message to the chat channel. 102 // ---------------------------------------------------- 103 104 SendMessageToSupport: function (evt) { 105 106 evt.preventDefault() 107 108 let createdAt = new Date() 109 createdAt = createdAt.toLocaleString() 110 111 const message = $('#newMessage').val().trim() 112 113 chat.myChannel.trigger('client-guest-new-message', { 114 'sender': chat.name, 115 'email': chat.email, 116 'text': message, 117 'createdAt': createdAt 118 }); 119 120 helpers.NewChatMessage({ 121 'text': message, 122 'name': chat.name, 123 'sender': chat.email 124 }) 125 126 console.log("Message added!") 127 128 $('#newMessage').val('') 129 }, 130 131 // ---------------------------------------------------- 132 // Logs user into a chat session. 133 // ---------------------------------------------------- 134 135 LogIntoChatSession: function (evt) { 136 const name = $('#fullname').val().trim() 137 const email = $('#email').val().trim().toLowerCase() 138 139 // Disable the form 140 chatBody.find('#loginScreenForm input, #loginScreenForm button').attr('disabled', true) 141 142 if ((name !== '' && name.length >= 3) && (email !== '' && email.length >= 5)) { 143 axios.post('/new/guest', {name, email}).then(response => { 144 chat.name = name 145 chat.email = email 146 chat.myChannel = pusher.subscribe('private-' + response.data.email); 147 helpers.ShowAppropriateChatDisplay() 148 }) 149 } else { 150 alert('Enter a valid name and email.') 151 } 152 153 evt.preventDefault() 154 } 155 } 156 157 // ------------------------------------------------------------------ 158 // Listen for a new message event from the admin 159 // ------------------------------------------------------------------ 160 161 pusher.bind('client-support-new-message', function(data){ 162 helpers.NewChatMessage(data) 163 }) 164 165 166 // ---------------------------------------------------- 167 // Register page event listeners 168 // ---------------------------------------------------- 169 170 chatPage.ready(helpers.ShowAppropriateChatDisplay) 171 chatHeader.on('click', helpers.ToggleChatWindow) 172 chatBody.find('#loginScreenForm').on('submit', helpers.LogIntoChatSession) 173 chatBody.find('#messageSupport').on('submit', helpers.SendMessageToSupport) 174 }())
Above we have the JavaScript that powers the clients chat widget. In the code, we start by instantiating Pusher (remember to replace the PUSHER_*
keys with the keys in your Pusher dashboard).
We have a helpers
property that has a few functions attached to it. Each function has a comment explaining what it does right before it is defined. At the bottom of the script is where we register all the events and listeners that make the widget function as expected.
Writing the admin.js script
The code in the admin.js
is similar to the app.js
and functions in a similat manner. Open the admin.js
add paste the following code:
1// File: static/js/admin.js 2 (function () { 3 'use strict'; 4 5 // ---------------------------------------------------- 6 // Configure Pusher instance 7 // ---------------------------------------------------- 8 9 var pusher = new Pusher('PUSHER_APP_KEY', { 10 authEndpoint: '/pusher/auth', 11 cluster: 'PUSHER_APP_CLUSTER', 12 encrypted: true 13 }); 14 15 // ---------------------------------------------------- 16 // Chat Details 17 // ---------------------------------------------------- 18 19 let chat = { 20 messages: [], 21 currentRoom: '', 22 currentChannel: '', 23 subscribedChannels: [], 24 subscribedUsers: [] 25 } 26 27 // ---------------------------------------------------- 28 // Subscribe to the generalChannel 29 // ---------------------------------------------------- 30 31 var generalChannel = pusher.subscribe('general-channel'); 32 33 // ---------------------------------------------------- 34 // Targeted Elements 35 // ---------------------------------------------------- 36 37 const chatBody = $(document) 38 const chatRoomsList = $('#rooms') 39 const chatReplyMessage = $('#replyMessage') 40 41 // ---------------------------------------------------- 42 // Register helpers 43 // ---------------------------------------------------- 44 45 const helpers = { 46 47 // ------------------------------------------------------------------ 48 // Clear the chat messages UI 49 // ------------------------------------------------------------------ 50 51 clearChatMessages: () => $('#chat-msgs').html(''), 52 53 // ------------------------------------------------------------------ 54 // Add a new chat message to the chat window. 55 // ------------------------------------------------------------------ 56 57 displayChatMessage: (message) => { 58 if (message.email === chat.currentRoom) { 59 60 $('#chat-msgs').prepend( 61 `<tr> 62 <td> 63 <div class="sender">${message.sender} @ <span class="date">${message.createdAt}</span></div> 64 <div class="message">${message.text}</div> 65 </td> 66 </tr>` 67 ) 68 } 69 }, 70 71 // ------------------------------------------------------------------ 72 // Select a new guest chatroom 73 // ------------------------------------------------------------------ 74 75 loadChatRoom: evt => { 76 chat.currentRoom = evt.target.dataset.roomId 77 chat.currentChannel = evt.target.dataset.channelId 78 79 if (chat.currentRoom !== undefined) { 80 $('.response').show() 81 $('#room-title').text(evt.target.dataset.roomId) 82 } 83 84 evt.preventDefault() 85 helpers.clearChatMessages() 86 }, 87 88 // ------------------------------------------------------------------ 89 // Reply a message 90 // ------------------------------------------------------------------ 91 replyMessage: evt => { 92 evt.preventDefault() 93 94 let createdAt = new Date() 95 createdAt = createdAt.toLocaleString() 96 97 const message = $('#replyMessage input').val().trim() 98 99 chat.subscribedChannels[chat.currentChannel].trigger('client-support-new-message', { 100 'name': 'Admin', 101 'email': chat.currentRoom, 102 'text': message, 103 'createdAt': createdAt 104 }); 105 106 helpers.displayChatMessage({ 107 'email': chat.currentRoom, 108 'sender': 'Support', 109 'text': message, 110 'createdAt': createdAt 111 }) 112 113 114 $('#replyMessage input').val('') 115 }, 116 } 117 118 119 // ------------------------------------------------------------------ 120 // Listen to the event that returns the details of a new guest user 121 // ------------------------------------------------------------------ 122 123 generalChannel.bind('new-guest-details', function(data) { 124 125 chat.subscribedChannels.push(pusher.subscribe('private-' + data.email)); 126 127 chat.subscribedUsers.push(data); 128 129 // render the new list of subscribed users and clear the former 130 $('#rooms').html(""); 131 chat.subscribedUsers.forEach(function (user, index) { 132 133 $('#rooms').append( 134 `<li class="nav-item"><a data-room-id="${user.email}" data-channel-id="${index}" class="nav-link" href="#">${user.name}</a></li>` 135 ) 136 }) 137 138 }) 139 140 141 // ------------------------------------------------------------------ 142 // Listen for a new message event from a guest 143 // ------------------------------------------------------------------ 144 145 pusher.bind('client-guest-new-message', function(data){ 146 helpers.displayChatMessage(data) 147 }) 148 149 150 // ---------------------------------------------------- 151 // Register page event listeners 152 // ---------------------------------------------------- 153 154 chatReplyMessage.on('submit', helpers.replyMessage) 155 chatRoomsList.on('click', 'li', helpers.loadChatRoom) 156 }())
Just like in the app.js
we have the helpers
object that holds the meat of the script and towards the bottom, the listeners and events are called and registered.
Replace the
PUSHER_APP_*
keys with the keys on your Pusher dashboard.
We can test the application using this command:
$ flask run
Now if we visit 127.0.0.1:5000 and 127.0.0.1:5000/admin we should test the application:
In this article, we have learned how we can leverage the power of Pusher in creating a chat widget powered by a Python backend. The entire code for this tutorial is available on GitHub.