Build a chat app with Ruby on Rails

Introduction

Introduction

In this post, we will be building a realtime chat application. This application will be built using Ruby on Rails and Pusher.

A quick look at what we’ll be building:

Chat applications have become very popular on virtually all web applications in today’s world. One very important feature of all chat applications is Instant Messaging. It is usually one of the basis for the success of any chat application.

To have the best chat experience, there must be a seamless realtime update of new messages.

Prerequisites

A basic understanding of Ruby and CoffeeScript will help you get the best out of this tutorial. It is assumed that you already have Ruby, Rails and PostgreSQL installed. Kindly check the PostgreSQL, Ruby and Rails documentation for installation steps.

Setting up the application

Open your terminal and run the following commands to create our demo application:

1# create a new rails application
2    $ rails new pusher-chat -T --database=postgresql

Go ahead and change directory into the newly created folder:

1# change directory
2    $ cd pusher-chat

In the root of your pusher-chat directory, open your Gemfile and add the following gems:

1# Gemfile
2    
3    gem 'bootstrap', '~> 4.1.0'
4    gem 'jquery-rails'
5    gem 'pusher'
6    gem 'figaro'

In your terminal, ensure you’re in the project directory and install the gems by running:

    $ bundle install

Next, we have to set up a database for our demo chat application. Check out this article on how to create a PostgreSQL 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:

1# config/database.yml
2    ...
3    development:
4      <<: *default
5      database: pusher-chat_development // add this line if it isn't already there
6      username: database_user // add this line
7      password: user_password // add this line
8    ...

The username and password in the code above should have access to the pusher-chat_development database. After that, run the following code to setup the database:

1# setup database
2    $ rails db:setup

Start the application

After setting up the database, 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

You can retrieve your keys from the App Keys tab:

ruby-app-keys

Bootstrap the application

Now that we have our Pusher credentials, we will go ahead and generate our model and controller. In your terminal, while in the project’s directory, run the following code:

1# generate a chat model
2    $ rails g model chat message:text username:string
3    
4    # generate a chats controller with actions
5    $ rails g controller Chats create new show index
6    
7    # run database migration
8    $ rails db:migrate

Refreshing our home page should still show us the default Rails landing page.

Building the homepage

With our controller in place, we route our homepage to the index action of the chat controller and add actions for creating and viewing our chats. Replace the code in your routes file with the following:

1# config/routes.rb
2    
3    Rails.application.routes.draw do
4      resources :chats
5      root 'chats#index'
6    end

Add the following code to your chat controller:

1# app/controllers/chats_controller.rb
2    
3    class ChatsController < ApplicationController
4      def index
5        @chats = Chat.all
6        @chat = Chat.new
7      end
8      
9      def new
10        @chat = Chat.new
11      end
12      
13      def create
14        @chat = Chat.new(chat_params)
15        respond_to do |format|
16          if @chat.save
17            format.html { redirect_to @chat, notice: 'Message was successfully posted.' }
18            format.json { render :show, status: :created, location: @chat }
19          else
20            format.html { render :new }
21            format.json { render json: @chat.errors, status: :unprocessable_entity }
22          end
23        end
24      end
25    
26      private
27        def chat_params
28          params.require(:chat).permit(:username, :message)
29        end
30    end

Reloading our homepage, we should see a not too pleasing view. Let’s fix that by adding the following code to our index.html.erb file:

1<%# app/views/chats/index.html.erb %>
2    
3    <div class="container-fluid">
4      <div class="row">
5        <div class="col-3 col-md-2 bg-dark full-height sidebar">
6          <div class="sidebar-content">
7            <input type="text" class="form-control sidebar-form" placeholder="Enter a username" required />
8            <h4 class="text-white mt-5 text-center username d-none">Hello </h4>
9          </div>
10        </div>
11        <div class="col-9 col-md-10 bg-light full-height">
12          <div class="container-fluid">
13            <div class="chat-box py-2">
14              <h4 class="username d-none mb-3"></h4>
15              <% @chats.each do |chat| %>
16                <div class="col-12">
17                  <div class="chat bg-secondary d-inline-block text-left text-white mb-2">
18                    <div class="chat-bubble">
19                      <small class="chat-username"><%= chat.username %></small>
20                      <p class="m-0 chat-message"><%= chat.message %></p>
21                    </div>
22                  </div>
23                </div>
24              <% end %>
25            </div>
26            <div class="chat-text-input">
27              <%= form_with(model: @chat, remote: true, format: :json, id: 'chat-form') do |form| %>
28                <% if @chat.errors.any? %>
29                  <div id="error_explanation">
30                    <h2><%= pluralize(@chat.errors.count, "error") %> prohibited this chat from being saved:</h2>
31                    <ul>
32                      <% @chat.errors.full_messages.each do |message| %>
33                        <li><%= message %></li>
34                      <% end %>
35                    </ul>
36                  </div>
37                <% end %>
38                <div class="field position-relative">
39                  <%= form.text_field :message, id: :message, class: "form-control", required: true, disabled: true %>
40                  <%= form.hidden_field :username, id: :username %>
41                </div>
42              <% end %>
43            </div>
44          </div>
45        </div>
46      </div>
47    </div>

Next, we add some Bootstrap styling. Rename your application.css file to application.scss and add the following code:

1// app/assets/stylesheets/application.scss
2    
3    @import "bootstrap";
4    @import url('https://fonts.googleapis.com/css?family=Josefin+Sans');
5    
6    body {
7      font-family: 'Josefin Sans', sans-serif;
8    }
9    
10    .full-height {
11     height: 100vh;
12     overflow: hidden;
13    }
14    
15    input.form-control.sidebar-form {
16      position: absolute;
17      bottom: 0;
18      left: 0;
19      border: 0;
20      border-radius: 0;
21    }
22    
23    .chat-box {
24      height: 94vh;
25      overflow: scroll;
26    }
27    
28    .chat {
29      border-radius: 3px;
30      padding: 0rem 2rem 0 1rem;
31    }
32    
33    .chat-username {
34      font-size: 0.7rem;
35    }
36    
37    .chat-message {
38      font-size: 0.85rem;
39    }

In your application.js file, add the following code just before the last line:

1// app/assets/javascripts/application.js
2    
3    .....
4    //= require jquery3 # add this line
5    //= require popper # add this line
6    //= require bootstrap # add this line
7    //= require_tree .

If you encounter a RegExp error while trying to set up Bootstrap, In config/boot.rb, change the ExecJS runtime from Duktape to Node.

1# config/boot.rb
2    ENV['EXECJS_RUNTIME'] ='Node'

If we reload our homepage now, we should see this majestic view of our chat application:

ruby-chat-app-1

Sending chat messages

To send chat messages in this demo app, first, we enter a username in the bottom left corner and then our messages in the text field on the bottom of the page. Updating the page with new messages will be handled with JavaScript.

In the views/chats folder, create a show.json.jbuilder file and add the following code:

1// app/views/chats/show.json.jbuilder
2    
3    json.extract! @chat, :id, :username, :message
4    json.url chat_url(@chat, format: :json)

In our chat.coffee file, we add the following code:

1# app/assets/javascripts/chats.coffee
2    
3    $(document).ready =>
4      username = ''
5      
6      $('.sidebar-form').keyup (event) ->
7        if event.keyCode == 13 and !event.shiftKey
8          username = event.target.value
9          $('.username').append(username)
10          $('#username').val(username)
11          $('.username').removeClass('d-none')
12          $('.sidebar-form').addClass('d-none')
13          $('#message').removeAttr("disabled")
14          $('#message').focus()
15        return
16    
17      $('#chat-form').on 'ajax:success', (data) ->
18        $('#chat-form')[0].reset()
19        updateChat data.detail[0]
20        return
21    
22      updateChat = (data) ->
23        $('.chat-box').append """
24          <div class="col-12">
25            <div class="chat bg-secondary d-inline-block text-left text-white mb-2">
26              <div class="chat-bubble">
27                <small class="chat-username">#{data.username}</small>
28                <p class="m-0 mt-2 chat-message">#{data.message}</p>
29              </div>
30            </div>
31          </div>
32        """
33        return

In the above code, we add attach an ajax:success event listener to our chat form courtesy of jQuery-ujs. Whenever we add chat messages, we get our messages as a response and append them to already existing messages on the page.

Realtime service with Pusher

We now have a functional chat application and all that’s left is to make our chats appear realtime. We will go ahead and integrate Pusher into our chat application.

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:

1# config/initializers/pusher.rb
2    
3    require 'pusher'
4    Pusher.app_id = ENV["PUSHER_APP_ID"]
5    Pusher.key = ENV["PUSHER_KEY"]
6    Pusher.secret = ENV["PUSHER_SECRET"]
7    Pusher.cluster = ENV["PUSHER_CLUSTER"]
8    Pusher.logger = Rails.logger
9    Pusher.encrypted = true

In your terminal, run figaro install to generate an application.yml file. In the application.yml file add your Pusher keys:

1# config/application.yml
2    
3    PUSHER_APP_ID: 'xxxxxx'
4    PUSHER_KEY: 'xxxxxxxxxxxxxxxxx'
5    PUSHER_SECRET: 'xxxxxxxxxxxxxx'
6    PUSHER_CLUSTER: 'xx'

Add the Pusher library inside the head tag in the application.html.erb file just before the javascript_include_tag:

1<%# app/views/layouts/application.html.erb %>
2    
3    <head>
4      ....
5      <script src="https://js.pusher.com/4.1/pusher.min.js"></script> // add this line
6      <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
7    </head>

For our chat to be realtime, we publish new chat messages to a channel and subscribe to it on the frontend of our app. In the chat model, we add an after_create callback, which calls a method that publishes the new record.

Add the following code to the chat model:

1# app/models/employee.rb
2    
3    class Chat < ApplicationRecord
4      after_create :notify_pusher, on: :create
5      
6      def notify_pusher
7        Pusher.trigger('chat', 'new', self.as_json)
8      end
9    end

Updating the UI realtime

In order to receive the chat messages in realtime, we’ll use the subscribe() method from Pusher to subscribe to the new event in the created chat channel. Rename your chats.coffee file to chats.coffee.erb and replace the code there with the following:

1# app/assets/javascripts/chats.coffee
2    
3    $(document).ready =>
4      username = ''
5    
6      updateChat = (data) ->
7        $('.chat-box').append """
8          <div class="col-12">
9            <div class="chat bg-secondary d-inline-block text-left text-white mb-2">
10              <div class="chat-bubble">
11                <small class="chat-username">#{data.username}</small>
12                <p class="m-0 chat-message">#{data.message}</p>
13              </div>
14            </div>
15          </div>
16        """
17        return
18      
19      $('.sidebar-form').keyup (event) ->
20        if event.keyCode == 13 and !event.shiftKey
21          username = event.target.value
22          $('.username').append(username)
23          $('#username').val(username)
24          $('.username').removeClass('d-none')
25          $('.sidebar-form').addClass('d-none')
26          $('#message').removeAttr("disabled")
27          $('#message').focus()
28        return
29      
30      $('#chat-form').on 'ajax:success', (data) ->
31        $('#chat-form')[0].reset()
32        return
33      
34      pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
35        cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
36        encrypted: true)
37      channel = pusher.subscribe('chat')
38      channel.bind 'new', (data) ->
39        updateChat data
40        return
41      return

In the code above, we subscribed our Pusher client to the chat channel and updated our chat with the data we got from it.

Bringing it all together

Restart the development server if it is currently running. Check your page on http://localhost:3000 and open it in a second tab. Add a few chat messages and see them pop up on the second tab.

Conclusion

So far, we have been able to build a basic chat application with realtime functionality as powered by Pusher. Feel free to explore more by visiting Pusher’s documentation. Lastly, the complete source code of this demo application is on Github.