Build a chat widget with Python and JavaScript

Introduction

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:

python-chat-widget-demo

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.

Prerequisites

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.

Setting up the app environment

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.

Setting up Pusher

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:

python-chat-widget-app-keys

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:

python-chat-widget-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

File and folder structure

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.

Building the backend

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.

Building the frontend

In this section, we are going to do the following things:

  • Create two new files, index.html and admin.html in the templates directory.
  • Create an img directory in the static directory and add a background image called bg.jpg inside it. You can find and download free images here.
  • Create a 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!

Setting up the homepage view

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    }

Setting up the admin dashboard view

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>&nbsp;</p>
28                    <div class="chat" style="margin-bottom:150px">
29                        <h5 id="room-title"></h5>
30                        <p>&nbsp;</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    }

Writing the app.js script

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.

Running the application

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:

python-chat-widget-demo

Conclusion

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.