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