Build a live chat widget with Ruby and PostgreSQL
To follow this tutorial, you need to have Ruby, Rails and PostgreSQL installed on your machine. Basic knowledge of Ruby and CoffeeScript will be helpful.
Introduction
In this age of full online shopping experience, customer retention is key. Customers want answers to their questions in realtime with precision. Whenever this is unavailable, they tend to click away to other sites.
One of the most effective ways for businesses to attend to their customers is through the use of a live chat system. With it, businesses have been able to provide convenient answers to their customers while at the same time, also increase sales.
In this tutorial, we’ll explore how to create a live chat system in Ruby while leveraging on the awesome features of Pusher.
When we’re done, we should have built something like this:
Prerequisites
A basic understanding of Ruby, CoffeeScript and PostgreSQL will help you with this tutorial. You should also have PostgreSQL installed. Kindly check the PostgreSQL, Ruby and Rails documentation for further installation steps. You also need to have a Pusher account.
Setting up the application
Open your terminal and run the following commands to create our demo application:
# create a new Rails application
$ rails new pusher-widget -T --database=postgresql
Go ahead and change directory into the newly created folder:
# change directory
$ cd pusher-widget
In the root of your pusher-widget
directory, open your Gemfile
and add the following gems:
# Gemfile
gem 'bootstrap', '~> 4.1.0'
gem 'jquery-rails'
gem 'pusher'
gem 'figaro'
In your terminal, ensure you are in the project directory and install the gems by running:
$ bundle install
Next, we set up a database for our demo application. Check out this article on how to create a Postgres database and an associated user and password.
Once you have your database details, in your database.yml
file, under the development
key, add the following code:
# config/database.yml
...
development:
<<: *default
database: pusher-widget_development // add this line if it isn't already there
username: database_user // add this line
password: user_password // add this line
...
The username and password in the code above should have access to the pusher-widget_development
database. After that, run the following code to setup the database:
# setup database
$ rails db:setup
Bootstrap the application
With our database set up, we’ll go ahead and create our models and controllers. In your terminal, while in the project’s directory, run the following code:
# generate a chatroom model
$ rails g model chatroom email:string name:string
# generate a chat model
$ rails g model chat message:string name:string chatroom:references
# run database migrations
$ rails db:migrate
# generate chatrooms controller with views
$ rails g controller chatrooms index new create show
# generate chats controller with views
$ rails g controller chats index new create show
Start the application
After setting up the models and controllers, in your terminal, start the development server by running rails s
. Visit http://localhost:3000 in your browser to see your brand new application:
Pusher account setup
Head over to Pusher and sign up for a free account.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with for a better setup experience:
Click the App Keys tab to retrieve your keys
Building the homepage
Now that we have our Pusher credential and our models and controllers have been set up, we will go ahead and build our homepage.
Replace the code in your routes file with the following:
# config/routes.rb
Rails.application.routes.draw do
resources :chatrooms
resources :chats
get '/dashboard', to: 'chats#index'
root 'chatrooms#index'
end
Next, we hook up Bootstrap and add some styles. In your application.js
file, add the following code just before the last line:
// app/assets/javascripts/application.js
.....
//= require jquery3 # add this line
//= require popper # add this line
//= require bootstrap # add this line
//= require_tree .
Rename your application.css
file to application.scss
and add the following code:
# app/assets/stylesheets/application.scss
@import "bootstrap";
@import url('https://fonts.googleapis.com/css?family=Tajawal');
body {
font-family: 'Tajawal', sans-serif;
}
.full-page {
height: 100vh;
width: 100vw;
overflow: hidden;
}
.jumbotron {
margin-top: 8rem;
}
.popup-wrapper {
height: 20rem;
position: fixed;
right: 1rem;
bottom: 0;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
}
.popup-head {
border-top-left-radius: 15px;
border-top-right-radius: 15px;
text-align: center;
cursor: pointer;
p {
margin: 0;
}
}
.popup-trigger {
height: 2rem;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
position: fixed;
bottom: 0;
right: 1rem;
cursor: pointer;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
p {
margin: 0;
font-weight: 900;
}
}
.chat-bubble {
border-radius: 3px;
}
.chats {
height: 23vh;
overflow: scroll;
}
.dashboard-sidebar-chat {
border-radius: 3px;
margin: 1rem 0;
padding: 0.2rem .5rem;
cursor: pointer;
a {
text-decoration: none;
color: unset;
}
}
.admin-chats {
height: 70vh;
overflow: scroll;
}
Add the markup for our homepage in the chatrooms index.html.erb
file
# app/views/chatrooms/index.html.erb
<div class="container-fluid full-page bg-dark position-relative">
<div class="jumbotron jumbotron-fluid bg-transparent text-white">
<div class="container">
<h1 class="display-4">Pusher Chat Widget</h1>
<p class="lead">Realtime chat.</p>
</div>
</div>
<div class="popup-trigger bg-info text-white col-3">
<p>Chat with support</p>
</div>
<div class="popup-wrapper bg-white col-3 p-0 collapse">
<div class="popup-head bg-info p-1">
<p>Close chat window</p>
</div>
<div class="start-chat-wrapper bg-light px-1 mt-5">
<%= form_with(model: @chatroom, format: :json, id: 'start-chat-form') do |form| %>
<div class="field">
<%= form.text_field :name, id: :name, class: "form-control", required: true, placeholder: "Enter your name" %>
</div>
<div class="field">
<%= form.email_field :email, id: :email, class: "form-control mt-3", required: true, placeholder: "Enter your email" %>
</div>
<div class="actions">
<%= form.submit 'Start chat', class: "btn btn-primary btn-block mt-2" %>
</div>
<% end %>
</div>
<div class="chat-wrapper bg-light px-1 collapse">
<div class="chats">
</div>
<div class="chat-form">
<%= form_with( scope: :chat, url: chats_path, format: :json, id: 'chat-form') do |form| %>
<div class="field">
<%= form.text_field :message, id: :message, class: "form-control", required: true, placeholder: "Enter your message" %>
<%= form.hidden_field :name, id: :name %>
<%= form.hidden_field :chatroom_id, id: :chatroom_id %>
</div>
<% end %>
</div>
</div>
</div>
</div>
If you followed the tutorial so far you should have been able to create the homepage with the chat widget at the bottom right of the screen. Reloading your homepage should display this:
If you encounter a RegExp error while trying to set up Bootstrap, In config/boot.rb
, change the ExecJS runtime from Duktape to Node.
# config/boot.rb
ENV['EXECJS_RUNTIME'] ='Node'
Building the admin dashboard
Now that our homepage is set, next we’ll build the admin dashboard. Let’s add the markup for our dashboard in the chats index.html.erb
file
# app/views/chats/index.html.erb
<div class="container-fluid full-page position-relative">
<div class="row">
<div class="col-3 col-md-2 bg-dark full-page px-1 py-2 sidebar">
<% @chatrooms.each do |chatroom| %>
<% if chatroom.chats.any? %>
<div class="dashboard-sidebar-chat bg-info">
<%= link_to chatroom.email, chat_path(id: chatroom.id), remote: true, class: 'sidebar-chat' %>
</div>
<% end %>
<% end %>
</div>
<div class="col-9 col-md-10 bg-light full-page container">
<h5 class="mt-4">👈 Select a chat from the sidebar to load the message</h5>
<div class="admin-chat-wrapper">
<h3 class="user-email mt-5"></h3>
<div class="chat-form">
<div class="admin-chats"></div>
<%= form_with( scope: :chat, url: chats_path, format: :json, id: 'admin-chat-form') do |form| %>
<div class="field">
<%= form.text_field :message, id: :message, class: "form-control", required: true, placeholder: "Enter your message" %>
<%= form.hidden_field :name, id: :name, value: 'Pusher support' %>
<%= form.hidden_field :chatroom_id, id: :chatroom_id %>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
Add the following code to the chatrooms controller:
# app/controllers/chatrooms_controller.rb
class ChatroomsController < ApplicationController
def index
@chatroom = Chatroom.new
end
def new
@chatroom = Chatroom.new
end
def create
@chatroom = Chatroom.new(chatroom_params)
respond_to do |format|
if @chatroom.save
format.html { redirect_to @chatroom }
format.json { render :show, status: :created, location: @chatroom }
else
format.html { render :new }
format.json { render json: @chatroom.errors, status: :unprocessable_entity }
end
end
end
def show
@chatroom = Chatroom.find(params[:id])
render json: @chatroom.chats
end
private
def chatroom_params
params.require(:chatroom).permit(:email, :name)
end
end
Also, add the following to the chats_controller.rb
file
# app/controllers/chats_controller.erb
class ChatsController < ApplicationController
def index
@chatrooms = Chatroom.all
end
def create
@chat = Chat.new(chat_params)
respond_to do |format|
if @chat.save
format.json { render :show, status: :created, location: @chat }
else
format.json { render json: @chat.errors, status: :unprocessable_entity }
end
end
end
def new
@chat = Chat.new
end
def show
@chats = Chat.where(chatroom_id: params[:id])
respond_to do |format|
if @chats
format.json { render :chats, status: :ok }
else
format.json { render json: @chats.errors, status: :unprocessable_entity }
end
end
end
private
def chat_params
params.require(:chat).permit(:message, :name, :chatroom_id)
end
end
If you visit http://localhost:3000/dashboard in your browser, you should be greeted with this awesome view:
Sending live chat messages
Our live chat system is ready to start receiving messages. All that is left is to handle the messages being sent by both the users and admins. Whenever messages are sent, we update the chat interface via AJAX. In your chatrooms.coffee
file, add the following code:
# app/assets/javascripts/chatrooms.coffee
$(document).ready =>
popupWrapper = $('.popup-wrapper')
popupTrigger = $('.popup-trigger')
# open the live chat widget if clicked
$('.popup-head').click ->
popupWrapper.addClass('collapse')
popupTrigger.removeClass('collapse')
return
# close the live chat widget if clicked
$('.popup-trigger').click ->
popupWrapper.removeClass('collapse')
popupTrigger.addClass('collapse')
return
# if the user's name and email is successfully submitted, hide the form and show the chat interface in the widget
$('#start-chat-form').on 'ajax:success', (data) ->
chatroom = data.detail[0]
$('.chat-form').removeClass('collapse')
$('.start-chat-wrapper').addClass('collapse')
$('.chat-wrapper').removeClass('collapse')
$('#chat-form #name').val(chatroom.name)
$('#chat-form #chatroom_id').val(chatroom.id)
getChats chatroom.id
$('#start-chat-form')[0].reset()
return
getChats = (id) ->
token = $('meta[name="csrf-token"]').attr('content')
$.ajax
url: 'chatrooms/' + id
type: 'get'
beforeSend: (xhr) ->
xhr.setRequestHeader 'X-CSRF-Token', token
return
success: (data) ->
return
return
# update the user's chat with new chat messages
updateChat = (data) ->
if data.chatroom_id == parseInt($('input#chatroom_id').val())
$('.chats').append """
<div class="chat-bubble-wrapper d-block">
<div class="chat-bubble bg-dark p-1 text-white my-1 d-inline-block">
<small class="chat-username">#{data.name}</small>
<p class="m-0 chat-message">#{data.message}</p>
</div>
</div>
"""
return
# if the user's chat message is successfully sent, reset the chat input field
$('#chat-form').on 'ajax:success', (data) ->
chat = data.detail[0]
$('#chat-form')[0].reset()
return
# function for displaying chat messages that belong to chat selcted in the admin sidebar
loadAdminChat = (chatArray) ->
$('.admin-chats').html ""
$('input#chatroom_id').val(chatArray.chats[0].chatroom_id)
$.map(chatArray.chats, (chat) ->
$('.admin-chats').append """
<div class="chat-bubble-wrapper d-block">
<div class="chat-bubble bg-dark p-1 text-white my-1 d-inline-block" style="min-width: 10rem;">
<small class="chat-username">#{chat.name}</small>
<p class="m-0 chat-message">#{chat.message}</p>
</div>
</div>
"""
return
)
return
# if the available chat in the sidebar is clicked, call the function that displays it's messages
$('body').on 'ajax:success', '.sidebar-chat', (data) ->
chat = data.detail[0]
loadAdminChat chat
return
# function to update admin's chat with new chat messages
updateAdminChat = (chat) ->
if chat.chatroom_id == parseInt($('input#chatroom_id').val())
$('.admin-chats').append """
<div class="chat-bubble-wrapper d-block">
<div class="chat-bubble bg-dark p-1 text-white my-1 d-inline-block" style="min-width: 10rem;">
<small class="chat-username">#{chat.name}</small>
<p class="m-0 chat-message">#{chat.message}</p>
</div>
</div>
"""
return
# function to update the available chats in the sidebar
updateAdminChatrooms = (chatroom) ->
$('.sidebar').append """
<div class="dashboard-sidebar-chat bg-info">
<a class="sidebar-chat" data-remote="true" href="/chats/#{chatroom.id}">#{chatroom.email}</a>
</div>
"""
return
# if admin's chat is successfully sent, clear the chat input field
$('#admin-chat-form').on 'ajax:success', (data) ->
chat = data.detail[0]
$('#admin-chat-form')[0].reset()
return
We’ll make use of Jbuilder to build our server response into JSON. In your chatroom views folder, create a show.json.jbuilder
file and add the following code:
# app/views/chatrooms/show.json.jbuilder
json.extract! @chatroom, :id, :name, :email
json.url chatroom_url(@chatroom, format: :json)
In the views/chats folder, create two files: show.json.jbuilder
and chats.json.jbuilder
and add the following code respectively:
# app/views/chats/show.json.jbuilder
json.extract! @chat, :id, :message, :name, :chatroom_id
json.url chat_url(@chat, format: :json)
# app/views/chats/show.json.builder
json.chats @chats do |chat|
json.(chat, :id, :name, :message, :chatroom_id)
end
Lastly, we add update the chatroom model with the following
# app/models/chatroom.rb
class Chatroom < ApplicationRecord
has_many :chats # add this line
end
If you followed the tutorial so far you should have been able to send messages from the chat widget on the homepage and if you reload your admin dashboard, you should see your message there.
Sweet! Next, we’ll remove the hassles of reloading with Pusher.
Realtime service with Pusher
For a live chat widget to be successful, the company should be immediately aware when there is a new message from a customer. We’ll go ahead and get that done with Pusher.
Firstly, we will initialize a Pusher client in our application. In the config/initializers
directory, create a pusher.rb
file and add the following code:
# config/initializers/pusher.rb
require 'pusher'
Pusher.app_id = ENV["PUSHER_APP_ID"]
Pusher.key = ENV["PUSHER_KEY"]
Pusher.secret = ENV["PUSHER_SECRET"]
Pusher.cluster = ENV["PUSHER_CLUSTER"]
Pusher.logger = Rails.logger
Pusher.encrypted = true
In your terminal, run figaro install
to generate an application.yml
file. In the application.yml
file add your Pusher keys:
# config/application.yml
PUSHER_APP_ID: 'xxxxxx'
PUSHER_KEY: 'xxxxxxxxxxxxxxxxx'
PUSHER_SECRET: 'xxxxxxxxxxxxxx'
PUSHER_CLUSTER: 'xx'
Add the Pusher library inside the head tag in the application.html.erb
file just before the javascript_include_tag
:
<%# app/views/layouts/application.html.erb %>
<head>
....
<script src="https://js.pusher.com/4.1/pusher.min.js"></script> // add this line
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
For the admin to be notified of a new customer chat, we notify Pusher whenever a customer starts a new chat. On the admin end, we’ll subscribe to events on our Pusher channel and update the UI accordingly.
Same thing applies for when both the user and admin exchange messages, we publish the messages via Pusher and subscribe to the updates on the frontend.
Update your chat and chatroom model respectively:
# app/models/chat.rb
class Chat < ApplicationRecord
after_create :notify_pusher
belongs_to :chatroom
def notify_pusher
Pusher.trigger('chat', 'new-chat', self.as_json)
end
end
# app/models/chatroom.rb
class Chatroom < ApplicationRecord
after_create :notify_pusher
has_many :chats
def notify_pusher
Pusher.trigger('chat', 'new-chatroom', self.as_json)
end
end
In the code above, we add an after_create
callback to both the chat and the chatroom models, which calls the function to publish new chats and chatrooms.
Rename your chatroom.coffee
file to chatroom.coffee.erb
and add the following code to the end of the file:
# app/assets/javascripts/chatroom.coffee.erb
.....
pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
encrypted: true)
channel = pusher.subscribe('chat')
channel.bind 'new-chat', (data) ->
updateChat data
updateAdminChat data
channel.bind 'new-chatroom', (data) ->
updateAdminChatrooms data
return
In the code above, we subscribe our Pusher client to the chat channel. Whenever there is a new chat or chatroom, we update the admin and user’s chat interface.
Ensure the code added above is indented as other code in the file.
Bringing it all together
Restart your development server and send some messages through the chat widget, they should pop up on the admin side.
Conclusion
In this tutorial, you learned how to build a customer support widget and administrator interface using Ruby and Pusher. Feel free to explore more by visiting Pusher’s documentation.
The source code to the article is available on GitHub.
21 May 2018
by Christian Nwamba