Build a chat app with Ruby on Rails

  • Christian Nwamba
May 20th, 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

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.

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:

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:

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

Go ahead and change directory into the newly created folder:

    # change directory
    $ cd pusher-chat

In the root of your pusher-chat 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’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:

    # config/database.yml
    ...
    development:
      <<: *default
      database: pusher-chat_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-chat_development database. After that, run the following code to setup the database:

    # setup database
    $ 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:

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:

You can retrieve your keys from the App Keys tab:

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:

    # generate a chat model
    $ rails g model chat message:text username:string

    # generate a chats controller with actions
    $ rails g controller Chats create new show index

    # run database migration
    $ 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:

    # config/routes.rb

    Rails.application.routes.draw do
      resources :chats
      root 'chats#index'
    end

Add the following code to your chat controller:

    # app/controllers/chats_controller.rb

    class ChatsController < ApplicationController
      def index
        @chats = Chat.all
        @chat = Chat.new
      end

      def new
        @chat = Chat.new
      end

      def create
        @chat = Chat.new(chat_params)
        respond_to do |format|
          if @chat.save
            format.html { redirect_to @chat, notice: 'Message was successfully posted.' }
            format.json { render :show, status: :created, location: @chat }
          else
            format.html { render :new }
            format.json { render json: @chat.errors, status: :unprocessable_entity }
          end
        end
      end

      private
        def chat_params
          params.require(:chat).permit(:username, :message)
        end
    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:

    <%# app/views/chats/index.html.erb %>

    <div class="container-fluid">
      <div class="row">
        <div class="col-3 col-md-2 bg-dark full-height sidebar">
          <div class="sidebar-content">
            <input type="text" class="form-control sidebar-form" placeholder="Enter a username" required />
            <h4 class="text-white mt-5 text-center username d-none">Hello </h4>
          </div>
        </div>
        <div class="col-9 col-md-10 bg-light full-height">
          <div class="container-fluid">
            <div class="chat-box py-2">
              <h4 class="username d-none mb-3"></h4>
              <% @chats.each do |chat| %>
                <div class="col-12">
                  <div class="chat bg-secondary d-inline-block text-left text-white mb-2">
                    <div class="chat-bubble">
                      <small class="chat-username"><%= chat.username %></small>
                      <p class="m-0 chat-message"><%= chat.message %></p>
                    </div>
                  </div>
                </div>
              <% end %>
            </div>
            <div class="chat-text-input">
              <%= form_with(model: @chat, remote: true, format: :json, id: 'chat-form') do |form| %>
                <% if @chat.errors.any? %>
                  <div id="error_explanation">
                    <h2><%= pluralize(@chat.errors.count, "error") %> prohibited this chat from being saved:</h2>
                    <ul>
                      <% @chat.errors.full_messages.each do |message| %>
                        <li><%= message %></li>
                      <% end %>
                    </ul>
                  </div>
                <% end %>
                <div class="field position-relative">
                  <%= form.text_field :message, id: :message, class: "form-control", required: true, disabled: true %>
                  <%= form.hidden_field :username, id: :username %>
                </div>
              <% end %>
            </div>
          </div>
        </div>
      </div>
    </div>

Next, we add some Bootstrap styling. 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=Josefin+Sans');

    body {
      font-family: 'Josefin Sans', sans-serif;
    }

    .full-height {
     height: 100vh;
     overflow: hidden;
    }

    input.form-control.sidebar-form {
      position: absolute;
      bottom: 0;
      left: 0;
      border: 0;
      border-radius: 0;
    }

    .chat-box {
      height: 94vh;
      overflow: scroll;
    }

    .chat {
      border-radius: 3px;
      padding: 0rem 2rem 0 1rem;
    }

    .chat-username {
      font-size: 0.7rem;
    }

    .chat-message {
      font-size: 0.85rem;
    }

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 .

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'

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

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:

    // app/views/chats/show.json.jbuilder

    json.extract! @chat, :id, :username, :message
    json.url chat_url(@chat, format: :json)

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

    # app/assets/javascripts/chats.coffee

    $(document).ready =>
      username = ''

      $('.sidebar-form').keyup (event) ->
        if event.keyCode == 13 and !event.shiftKey
          username = event.target.value
          $('.username').append(username)
          $('#username').val(username)
          $('.username').removeClass('d-none')
          $('.sidebar-form').addClass('d-none')
          $('#message').removeAttr("disabled")
          $('#message').focus()
        return

      $('#chat-form').on 'ajax:success', (data) ->
        $('#chat-form')[0].reset()
        updateChat data.detail[0]
        return

      updateChat = (data) ->
        $('.chat-box').append """
          <div class="col-12">
            <div class="chat bg-secondary d-inline-block text-left text-white mb-2">
              <div class="chat-bubble">
                <small class="chat-username">#{data.username}</small>
                <p class="m-0 mt-2 chat-message">#{data.message}</p>
              </div>
            </div>
          </div>
        """
        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:

    # 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 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:

    # app/models/employee.rb

    class Chat < ApplicationRecord
      after_create :notify_pusher, on: :create

      def notify_pusher
        Pusher.trigger('chat', 'new', self.as_json)
      end
    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 chat.coffee.erb and replace the code there with the following:

    # app/assets/javascripts/chats.coffee

    $(document).ready =>
      username = ''

      updateChat = (data) ->
        $('.chat-box').append """
          <div class="col-12">
            <div class="chat bg-secondary d-inline-block text-left text-white mb-2">
              <div class="chat-bubble">
                <small class="chat-username">#{data.username}</small>
                <p class="m-0 chat-message">#{data.message}</p>
              </div>
            </div>
          </div>
        """
        return

      $('.sidebar-form').keyup (event) ->
        if event.keyCode == 13 and !event.shiftKey
          username = event.target.value
          $('.username').append(username)
          $('#username').val(username)
          $('.username').removeClass('d-none')
          $('.sidebar-form').addClass('d-none')
          $('#message').removeAttr("disabled")
          $('#message').focus()
        return

      $('#chat-form').on 'ajax:success', (data) ->
        $('#chat-form')[0].reset()
        return

      pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
        cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
        encrypted: true)
      channel = pusher.subscribe('chat')
      channel.bind 'new', (data) ->
        updateChat data
        return
      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.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.