Back to search

Build a live chat widget with Ruby and PostgreSQL

  • Christian Nwamba
May 21st, 2018
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:

ruby-chat-widget-demo

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:

youre-on-rails

Pusher account setup

Head over to Pusher and sign up for a free account.

ruby-create-pusher-account

Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:

ruby-channels-dashboard

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:

ruby-new-pusher-app

Click the App Keys tab to retrieve your keys

ruby-app-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:

ruby-chat-widget-homepage

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:

ruby-chat-widget-admin-dashboard

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.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.