Building a chat widget with Go and JavaScript

Introduction

Introduction

The process of building products for an online demographic should be thorough and follow modern-day trends. One of such trend is making it possible for the customers and support agents to have realtime discussions over some form of two-way message channel. This would ensure that customers do not click away in confusion and switch to competitors in times of frustration.

In this tutorial, we will see how to build a realtime chat widget with Go, Pusher, and JavaScript. Here’s a demo of the application:

go-chat-widget-demo

In the above image, we built a website that sells motorcycles and integrates a chat widget. A customer is able to sign up to speak with a support agent and the agent on the other end can manage communication among a number of connected customers.

Prerequisites

To follow along with this article, you will need the following:

  • An IDE of your choice like Visual Studio Code.
  • Go (version >= 0.10.x) installed on your computer. Here’s how you can install Go.
  • Basic knowledge of the Go programming language.
  • Basic knowledge of JavaScript (ES6) and jQuery.
  • Once you have all the above requirements, we can proceed.

Skip the next section if you have already signed up with Pusher and created an application.

Setting up Pusher

The realtime feature of the chat widget is dependent on Pusher so you need to create an account here if you don’t already have one, after the signup process, you will be asked to create a new application. Let’s keep the app credentials nearby because we will need it to integrate Pusher within the cat widget.

Enabling client events

The final thing we will do is enable the Pusher application to trigger events from the client (browser) over a private channel. We need this feature because it is what will make it possible for a support agent to securely chat with a customer without having to send the message through the backend server first. Follow the steps below to activate client events from the dashboard:

  • Select the channel application.
  • Click on App Settings from the horizontal options list.
  • Check the Enable client events option.
    go-chat-widget-enable-client-events
  • Click on the Update button.

That’s all we need to do here.

Setting up the codebase

Let’s begin by navigating into the src directory that is located in the $GOPATH and creating a new directory for our app. This will be the root directory for this project:

1$ cd $GOPATH/src
2    $ mkdir go-pusher-chat-widget
3    $ cd go-pusher-chat-widget

Let’s create the main Go file (this is the entry point of the application) here and call it chat.go. Next, we will install the Go Pusher library that we will reference within the code for the backend server. Run the following code in the terminal to pull in the package:

    $ go get github.com/pusher/pusher-http-go

⚠️ If you use Windows and you encounter the error ‘cc.exe: sorry, unimplemented: 64-bit mode not compiled in ‘, then you need a Windows gcc port, such as https://sourceforge.net/projects/mingw-w64/. Also, see this GitHub issue.

Open the chat.go file in your favorite IDE and update it with the following code:

1// File: ./chat.go
2    package main
3    
4    // Here, we import the required packages (including Pusher)
5    import (
6        "encoding/json"
7        "fmt"
8        "io/ioutil"
9        "log"
10        "net/http"
11        pusher "github.com/pusher/pusher-http-go"
12    )
13    
14    // Here, we register the Pusher client
15    var client = pusher.Client{
16        AppId:   "PUSHER_APP_ID",
17        Key:     "PUSHER_APP_KEY",
18        Secret:  "PUSHER_APP_SECRET",
19        Cluster: "PUSHER_APP_CLUSTER",
20        Secure:  true,
21    }
22    
23    // Here, we define a customer as a struct
24    type customer struct {
25        Name  string `json:"name" xml:"name" form:"name" query:"name"`
26        Email string `json:"email" xml:"email" form:"email" query:"email"`
27    }
28    
29    func main() {
30    
31        // Serve the static files and templates from the public directory
32        http.Handle("/", http.FileServer(http.Dir("./public")))
33    
34        // -------------------------------------------------------
35        // Listen on these routes for new customer registration and User authorization,
36        // thereafter, handle each request using the matching handler function.
37        // -------------------------------------------------------
38        http.HandleFunc("/new/customer", broadcastCustomerDetails)
39        http.HandleFunc("/pusher/auth", pusherAuth)
40    
41        // Start executing the application on port 8070
42        log.Fatal(http.ListenAndServe(":8070", nil))
43    }

In the code above, we registered a new Pusher client with the credentials from the app we created earlier on the dashboard.

⚠️ Replace PUSHER_* keys with your app credentials.

In the main function, we defined two endpoints, /new/customer and /pusher/auth. The first will be hit when a new customer signs up and the last will authorize the users so they can subscribe to private channels.

We will be serving all static files from a public directory that we will create shortly.

Note that we did not pull in the ioutil and http packages because they are already among Go’s standard packages.

We also defined customer as a struct and attached extra definitions to its properties so that Go knows how to handle incoming payloads and bind their various structures with a new instance of the customer struct.

Let’s create the handler functions for the endpoints, add this code to the chat.go file just before the main function:

1// File: ./chat.go
2    
3    // [...]
4    
5    func broadcastCustomerDetails(rw http.ResponseWriter, req *http.Request) {
6        body, err := ioutil.ReadAll(req.Body)
7        if err != nil {
8            panic(err)
9        }
10        var newCustomer customer
11        err = json.Unmarshal(body, &newCustomer)
12        if err != nil {
13            panic(err)
14        }
15        client.Trigger("one-to-many", "new-customer", newCustomer)
16        json.NewEncoder(rw).Encode(newCustomer)
17    }
18    
19    func pusherAuth(res http.ResponseWriter, req *http.Request) {
20        params, _ := ioutil.ReadAll(req.Body)
21        response, err := client.AuthenticatePrivateChannel(params)
22        if err != nil {
23            panic(err)
24        }
25        fmt.Fprintf(res, string(response))
26    }
27    
28    // [...]

Above we have two functions. broadcastCustomerDetails receives a new customer's details and binds it to an instance of the customer struct. We then trigger the received details over to the admin dashboard in an event over the public channel. The pusherAuth authorizes users so they can subscribe to private channels.

This is all the code required for the backend server to work, let’s move on to the frontend.

Building the frontend

In this section, we will start building the frontend of the web application. We will create all the static files that are rendered when a browser is pointed to the address of our application.

Create a new folder in the project directory and call it public, this folder is the root directory for all of our frontend files. In this folder, create three folders css, js and img.

Next, create two files in the root of the public directory named index.html and support.html.

Creating the homepage

Open the index.html file and update it with the following code:

1<!-- File: ./public/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>X-Cycles</title>
8        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
9        <link rel="stylesheet" href="./css/app.css" >
10      </head>
11    
12      <body>
13        <div class="site-wrapper">
14          <div class="site-wrapper-inner">
15            <div class="cover-container">
16    
17              <header class="masthead clearfix">
18                <div class="inner">
19                  <h3 class="masthead-brand">X-Cycles</h3>
20                  <nav class="nav nav-masthead">
21                    <a class="nav-link active" href="#">Home</a>
22                    <a class="nav-link" href="#">Features</a>
23                    <a class="nav-link" href="#">Contact</a>
24                  </nav>
25                </div>
26              </header>
27    
28              <main role="main" class="inner cover">
29                <h1 class="cover-heading">X-cycles</h1>
30                <p class="lead">We sell the best motorcycles around.</p>
31                <p class="lead">
32                  <a href="#" class="btn btn-lg btn-secondary">GALLERY</a>
33                </p>
34              </main>
35    
36              <footer class="mastfoot">
37              </footer>
38    
39            </div>
40          </div>
41        </div>
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    
49                <form id="loginScreenForm">
50                  <div class="form-group">
51                    <input type="text" class="form-control" id="fullname" placeholder="Name*" required>
52                  </div>
53                  <div class="form-group">
54                    <input type="email" class="form-control" id="email" placeholder="Email Address*" required>
55                  </div>
56                  <button type="submit" class="btn btn-block btn-primary">Start Chat</button>
57                </form>
58    
59              </div>
60              <div class="chats">
61                <div class="loader-wrapper">
62                  <div class="loader">
63                    <span>{</span><span>}</span>
64                  </div>
65                </div>
66                <ul class="messages clearfix">
67                </ul>
68                <div class="input">
69                  <form class="form-inline" id="messageSupport">
70                    <div class="form-group">
71                      <input type="text" autocomplete="off" class="form-control" id="newMessage" placeholder="Enter Message">
72                    </div>
73                    <button type="submit" class="btn btn-primary">Send</button>
74                  </form>
75                </div>
76              </div>
77            </div>
78        </div>    
79    
80        <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
81        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
82        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
83        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>
84        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
85        <script type="text/javascript" src="./js/app.js"></script>
86      </body>
87    </html>

In the css directory, create an app.css file and update it with the following code:

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

Above we referenced a bg.jpg image. You can download a free picture here and place it in the public/img directory.

Now let's include some JavaScript. In the js directory, create an app.js file and paste the following code:

1// File: ./public/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        let chat = {
12            name:  undefined,
13            email: undefined,
14            myChannel: undefined,
15        }
16    
17        const chatPage   = $(document)
18        const chatWindow = $('.chatbubble')
19        const chatHeader = chatWindow.find('.unexpanded')
20        const chatBody   = chatWindow.find('.chat-window')
21    
22        let helpers = {
23            ToggleChatWindow: function () {
24                chatWindow.toggleClass('opened')
25                chatHeader.find('.title').text(
26                    chatWindow.hasClass('opened') ? 'Minimize Chat Window' : 'Chat with Support'
27                )
28            },
29    
30            ShowAppropriateChatDisplay: function () {
31                (chat.name) ? helpers.ShowChatRoomDisplay() : helpers.ShowChatInitiationDisplay()
32            },
33    
34            ShowChatInitiationDisplay: function () {
35                chatBody.find('.chats').removeClass('active')
36                chatBody.find('.login-screen').addClass('active')
37            },
38    
39            ShowChatRoomDisplay: function () {
40                chatBody.find('.chats').addClass('active')
41                chatBody.find('.login-screen').removeClass('active')
42                setTimeout(function(){
43                    chatBody.find('.loader-wrapper').hide()
44                    chatBody.find('.input, .messages').show()
45                }, 2000)
46            },
47    
48            NewChatMessage: function (message) {
49                if (message !== undefined) {
50                    const messageClass = message.sender !== chat.email ? 'support' : 'user'
51                    chatBody.find('ul.messages').append(
52                        `<li class="clearfix message ${messageClass}">
53                            <div class="sender">${message.name}</div>
54                            <div class="message">${message.text}</div>
55                        </li>`
56                    )
57                    chatBody.scrollTop(chatBody[0].scrollHeight)
58                }
59            },
60    
61            SendMessageToSupport: function (evt) {
62                evt.preventDefault()
63                let createdAt = new Date()
64                createdAt = createdAt.toLocaleString()
65                const message = $('#newMessage').val().trim()
66                
67                chat.myChannel.trigger('client-guest-new-message', {
68                    'sender': chat.name,
69                    'email': chat.email,
70                    'text': message,
71                    'createdAt': createdAt 
72                });
73    
74                helpers.NewChatMessage({
75                    'text': message,
76                    'name': chat.name,
77                    'sender': chat.email
78                })
79    
80                $('#newMessage').val('')
81            },
82    
83            LogIntoChatSession: function (evt) {
84                const name  = $('#fullname').val().trim()
85                const email = $('#email').val().trim().toLowerCase()
86    
87                chatBody.find('#loginScreenForm input, #loginScreenForm button').attr('disabled', true)
88    
89                if ((name !== '' && name.length >= 3) && (email !== '' && email.length >= 5)) {
90                    axios.post('/new/customer', {"name":name, "email":email}).then(response => {
91                        chat.name = name
92                        chat.email = email
93                        console.log(response.data.email)
94                        chat.myChannel = pusher.subscribe('private-' + response.data.email);
95                        helpers.ShowAppropriateChatDisplay()
96                    })
97                } else {
98                    alert('Enter a valid name and email.')
99                }
100                
101                evt.preventDefault()
102            }
103        }
104    
105    
106        pusher.bind('client-support-new-message', function(data){
107            helpers.NewChatMessage(data)
108        })
109    
110    
111        chatPage.ready(helpers.ShowAppropriateChatDisplay)
112        chatHeader.on('click', helpers.ToggleChatWindow)
113        
114        chatBody.find('#loginScreenForm').on('submit', helpers.LogIntoChatSession)
115        chatBody.find('#messageSupport').on('submit', helpers.SendMessageToSupport)
116    }());

Above, we instantiated a Pusher object instance and then we created a helpers object. In this object lies the meat of the script. In the helpers object we have a few methods that do specific tasks:

  • ToggleChatWindow - toggles the chat windows display.
  • ShowAppropriateChatDisplay - decides which chat display to show depending on the action of the user.
  • ShowChatInitiationDisplay - shows the initial display for the chat window for the user to initiate a chat session.
  • ShowChatRoomDisplay - shows the chat window after the user has instantiated a new chat session.
  • NewChatMessage - adds a new chat message to the chat window UI.
  • SendMessageToSupport - sends a chat message to the backend.
  • LogIntoChatSession - starts a new chat session.

Replace the PUSHER_* keys with the one available on your Pusher dashboard.

Creating the support dashboard

Open the support.html file and update it with the following code:

1<!-- File: ./public/support.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>X-Cycles | Support </title>
8        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
9        <link rel="stylesheet" href="./css/support.css" >
10      </head>
11    
12      <body>
13        <header>
14            <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
15                <a class="navbar-brand" href="#">Dashboard</a>
16            </nav>
17        </header>
18    
19        <div class="container-fluid">
20            <div class="row" id="mainrow">
21                <nav class="col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar">
22                    <ul class="nav nav-pills flex-column" id="rooms">
23                    </ul>
24                </nav>
25                <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" id="main">
26                    <h1>Chats</h1>
27                    <p>👈 Select a chat to load the messages</p>
28                    <p>&nbsp;</p>
29                    <div class="chat" style="margin-bottom:150px">
30                        <h5 id="room-title"></h5>
31                        <p>&nbsp;</p>
32                        <div class="response">
33                            <form id="replyMessage">
34                                <div class="form-group">
35                                    <input type="text" placeholder="Enter Message" class="form-control" name="message" />
36                                </div>
37                            </form>
38                        </div>
39                        <div class="table-responsive">
40                          <table class="table table-striped">
41                            <tbody id="chat-msgs">
42                            </tbody>
43                        </table>
44                    </div>
45                </main>
46            </div>
47        </div>
48    
49        <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
50        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
51        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
52        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>
53        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
54        <script type="text/javascript" src="./js/support.js"></script>
55      </body>
56    </html>

Let’s write the style for the support page. In the css directory, create a support.css file and paste the following code:

1/* File: ./public/css/support.css */
2    body {
3        padding-top: 3.5rem;
4    }
5    h1 {
6        padding-bottom: 9px;
7        margin-bottom: 20px;
8        border-bottom: 1px solid #eee;
9    }
10    .sidebar {
11        position: fixed;
12        top: 51px;
13        bottom: 0;
14        left: 0;
15        z-index: 1000;
16        padding: 20px 0;
17        overflow-x: hidden;
18        overflow-y: auto;
19        border-right: 1px solid #eee;
20    }
21    .sidebar .nav {
22        margin-bottom: 20px;
23    }
24    .sidebar .nav-item {
25        width: 100%;
26    }
27    .sidebar .nav-item + .nav-item {
28        margin-left: 0;
29    }
30    .sidebar .nav-link {
31        border-radius: 0;
32    }
33    .placeholders {
34        padding-bottom: 3rem;
35    }
36    .placeholder img {
37        padding-top: 1.5rem;
38        padding-bottom: 1.5rem;
39    }
40    tr .sender {
41        font-size: 12px;
42        font-weight: 600;
43    }
44    tr .sender span {
45        color: #676767;
46    }
47    .response {
48        display: none;
49    }

Now let's add the JavaScript for the page. In the js directory, create a support.js file and update it with the following code:

1// File: ./public/js/support.js
2    (function () {
3        var pusher = new Pusher('PUSHER_APP_KEY', {
4            authEndpoint: '/pusher/auth',
5            cluster: 'PUSHER_APP_CLUSTER',
6            encrypted: true
7        });
8    
9        let chat = {
10            messages: [],
11            currentRoom: '',
12            currentChannel: '',
13            subscribedChannels: [],
14            subscribedUsers: []
15        }
16    
17        var generalChannel = pusher.subscribe('one-to-many');
18    
19        const chatBody = $(document)
20        const chatRoomsList = $('#rooms')
21        const chatReplyMessage = $('#replyMessage')
22    
23        const helpers = {
24    
25            clearChatMessages: () => $('#chat-msgs').html(''),
26    
27            displayChatMessage: (message) => {
28                if (message.email === chat.currentRoom) {
29                    $('#chat-msgs').prepend(
30                        `<tr>
31                            <td>
32                                <div class="sender">${message.sender} @ <span class="date">${message.createdAt}</span></div>
33                                <div class="message">${message.text}</div>
34                            </td>
35                        </tr>`
36                    )
37                }
38            },
39    
40            loadChatRoom: evt => {
41                chat.currentRoom = evt.target.dataset.roomId
42                chat.currentChannel = evt.target.dataset.channelId
43                if (chat.currentRoom !== undefined) {
44                    $('.response').show()
45                    $('#room-title').text(evt.target.dataset.roomId)
46                }
47                evt.preventDefault()
48                helpers.clearChatMessages()
49            },
50    
51            replyMessage: evt => {
52                evt.preventDefault()
53                let createdAt = new Date()
54                createdAt = createdAt.toLocaleString()
55                const message = $('#replyMessage input').val().trim()
56                chat.subscribedChannels[chat.currentChannel].trigger('client-support-new-message', {
57                    'name': 'Admin',
58                    'email': chat.currentRoom,
59                    'text': message, 
60                    'createdAt': createdAt 
61                });
62                
63                helpers.displayChatMessage({
64                    'email': chat.currentRoom,
65                    'sender': 'Support',
66                    'text': message, 
67                    'createdAt': createdAt
68                })
69    
70                $('#replyMessage input').val('')
71            },
72        }
73    
74        generalChannel.bind('new-customer', function(data) {
75            chat.subscribedChannels.push(pusher.subscribe('private-' + data.email));
76            chat.subscribedUsers.push(data);
77            // render the new list of subscribed users and clear the former
78            $('#rooms').html("");
79            chat.subscribedUsers.forEach(function (user, index) {
80                    $('#rooms').append(
81                        `<li class="nav-item"><a data-room-id="${user.email}" data-channel-id="${index}" class="nav-link" href="#">${user.name}</a></li>`
82                    )
83            })
84        })
85    
86        pusher.bind('client-guest-new-message', function(data){
87            helpers.displayChatMessage(data)
88        })
89    
90        chatReplyMessage.on('submit', helpers.replyMessage)
91        chatRoomsList.on('click', 'li', helpers.loadChatRoom)
92    }())

Above, the script looks almost similar to the app.js script. The helpers object contains the following functions:

  • clearChatMessages - clears the chat message window.
  • displayChatMessage - displays a new chat message in the current window.
  • loadChatRoom - shows a users chat messages in the general chat window after a room is selected.
  • replyMessage - sends a chat message to the current room.

After declaring the helpers, we bind to the Pusher channel and register our listeners.

Replace the PUSHER_* keys with the one available on your Pusher dashboard.

Running the application

To test the application, we will run the application by typing this command in the terminal:

    $ go run chat.go

We can visit these addresses, http://127.0.0.1:8070 and http://127.0.0.1:8070/support.html, on a web browser using different windows to test that the application works correctly. Here’s what we should see:

go-chat-widget-demo

Conclusion

In this tutorial, we learned how to create a basic realtime web chat widget using Go and JavaScript. The source code for this project is available here on GitHub.