Build a chat app with Ruby on Rails
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 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:
# 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 chats.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.
20 May 2018
by Christian Nwamba