Gmail Like Chat Application in Ruby on Rails
30 Jul 14
In this tutorial, I will guide you on how to implement a Gmail/Facebook like real-time chat application in a Ruby on Rails application.
We are all fond of the Gmail and Facebook inline chat modules. About a week ago, I came across a tutorial
on how to replicate hangouts chat in pure css and html and this really inspired me. I thought to myself, "why not give life to this beautiful Gmail-like chat UI?". With this, I challenged myself to try and come up with a real-time chat in a Rails application. I wanted to have a near-realtime kind of chat functionality and in my research I found out that the publish/subscribe model was the most efficient, fast and scalable way to go about this.
As a framework, Rails is not very good at handling asynchronous events and therefore establishing a socket connection to a Rails application is almost impossible. There are many solutions that are designed to handle this kind of problem. Frameworks such as
Node.js with Socket.IO, or, if you want to stick with Ruby, Cramp, async_sinatra, or Goliath framework are all great solutions but it would be super awesome to continue using Rails for our application's logic and also have the benefits of some kind of asynchronous event handling with publishing and subscribing when we need that. To achieve this, we will use Private Pub Gem which is built on top of Faye and makes it dead simple to publish and subscribe to real-time events in a Rails app.
Check out The Demo to have a feel of what we will be creating. Create an account on one separate browser window and on another log in using the default user provided on the login page then try simulating a conversation between yourself and the "chatty bot".
We’re going to add the Instant Messaging feature to an existing Rails application. Below is a screenshot from a simple rails app (chatty) that uses
Devise to authenticate users. It's current functionality is very basic. On the home page, we are just looping the list of all users except the currently logged in user.
We want to enable the users of our app to chat with each other. Let's start by modeling up the logic of our chat application. Our logic is very simple. A
user will have many conversations and a conversation will have many messages. Here is the relationship diagram
Let's get started by creating the
conversation model. A conversation will hold the sender_id and the recipient_id both of which are instances of a user. The sender_id will hold the id of the user starting the conversation and the recipient_id will hold the id of the other user. On your terminal
$ rails g model Conversation sender_id:integer recipient_id:integer
It's a good idea to add an index to both sender_id and recipient_id as we will use this fields while searching.
After migrating the database, lets update our user model. Since we don't have the
user_id column in our conversation table, we must explicitly tell rails which foreign key to use.
Next, lets edit our
conversation model. A conversation will belong to both a sender and a recipient all of which are instances of a user.
As you can see, our conversation will have many messages. We are then validating uniqueness of the sender_id and passing a scope of the recipeint_id. What this does is that it ensures that the
sender_id and the recipient_id are always unique so that we only have unique conversations in our application. For instance, a conversation with (sender_id: 1, recipient_id: 2) and another with (sender_id: 2, and recipient_id: 1) should never occur since the conversation is essentially between the same users.
We have also added two scopes
involving and between. The first scope will help us retrieve all conversations of the currently logged-in user while the last scope will help us check if a conversation exists between any given two users before we create the conversation.
Now that we have our conversation model, lets proceed and create our message model. Run the following command on your terminal and run
$ rails g model Message body:text conversation:references user:references
Our messages table will have a
body, conversation_id, and user_id columns. The conversation_id column will keep track of which conversation a message belongs to and the user_id column will keep track of the user who sent the message during chat. Adding some validations
Now that our models are in place, I want to explain the general flow of how the inline chat application will work. On our home page, we will add a
send message button along each user's name. The button will hold two data attributes i.e. the id of the current user and another id of the reciever id. When a user clicks the button, we will send an asynchronous request to our rails app with the current user's id and the recipient id. If a conversation exists, we return the conversation id immediately, otherwise we create the conversation and return the id of the newly created conversation.
We will then pick, the
conversation_id returned by the server using jQuery. Using this conversation_id, we will then request the respective show page of that conversation which will have a list of all its associated messages. For instance, if the server returns conversation_id 3, we will request the html page at conversations/3. We will then append this conversation data in our home page in a popup div.
Adding the message button
For each user we are displaying on our home page, we associate a send message button with him or her. The
data-sid attribute will hold our current users' id while data-rip attribute will hold the recipient's id. We will send this values through an ajax request to our server which should create a conversation if necessary.
To keep this tutorial, short I have created most of the jQuery logic in a single file which I will link here. I have tried to make it as modular as possible and also provide comments on each method so it shouldn't be hard to comprehend what's cooking. Lets create a file
If using rails 4 and turbolinks, require this file before requiring turbolinks.
//= require chat
users.js we'll listen for various events on our home page document and call respective methods inside our chat.js file
$ rails g controller conversations
Our conversations controller will have only to actions, the
create and the show actions.
You will notice, the
layout false directive. This is because we don't want the show view inheriting from application.html layout. We will be appending this view on our home page.
In the create action, we start off by searching if a conversation exists between the
sender_id and recipient_id. Recall our between scope we defined earlier in our conversation model. If a conversation is found, we return it and assign it to the @conversation instance variable. If no conversation was found between the two users, we create a new conversation. We then eventually return a json response with the id of the conversation.
It would be nice if we installed
private_pub at this point as our next set of task, we will be utilizing functionality offered by the gem. Add private_pub gem in your Gemfile and run bundle. Installing it will also install Faye and its dependencies. We will also include the thin gem as we will be using it to serve Faye.
Next up, we need to run the generator provided by private_pub to generate the configuration file and a Rackup file for starting the Faye server.
rails g private_pub:install
We can now start up that Rack server by running this command on a separate terminal
rackup private_pub.ru -s thin -E production
//= require private_pub
With private_pub installed, we can now proceed to make our first view that uses private_pubs functionality. This will be the show view of our conversation's controller
What stand's out here is the
suscribe_to method. Here we subscribe to a channel ( the conversation's path ). We will use this same path to publish update notifications to this channel from our controller. We do this by calling subscribe_to and passing in the name of a channel which takes a format of a path. In our case we pass the current conversation's path.
Our last controller is the messages controller.
$ rails g controller messages
We only have the
create action for the messages controller. We store the conversation's path in the @path instance variable. We will use this path to publish push notifications to our view. Remember we suscribed to the same url in our conversation's show view. This will always be unique for any two users in our app.
var reciever_id = $('meta[name=user-id]').attr("content");
We need a way to identity who is the recipient when publishing the notifications. A simple way around this is to create a meta tag in our application.html layout file that store's the id of the currently logged in user. In the head section of our layout file add
You will also note that we are rendering a
message partial. We have not created that yet. In our messages folder
Note, we have used to helper methods
self_or_other and message_interlocutor. They both take a message as an argument. Lets go ahead and define them. In our messages_helper file
Since we created our conversation's controller and messages controller, we have not yet defined their respective routes. Let's add them right away
Notice we don't have yet any stylings for our chat box. Create a file
chat.css in our stylesheets folder and paste the contents of chat.css. Let's also add the fon't awesome css in our application.html layout. This will provide us with some nice icon stylings for the minimize and close buttons.
Testing our chat app
Here comes the most interesting part of the tutorial. Our application should now be capable of handling near-realtime instant messages between any two users. In one browser window, I'm logged in as "Joseph" and in another i'm logged in as "Kevin". As "Joseph", clicking on Kevin's send message button, creates a new conversation between us and pops up a beautiful chat window. On my other browser as "Kevin" clicking on Joseph's send message button pops up a similar window and we can now chat instantly!. Check it out
And that's it for this tutorial. Hope this gives you an overview on how to go about implementing an instant message type chat for your rails application. Have any questions and or comments? Feel free to use the comments section below. Something not working out? feel free to also clone this application on my Github repo and compare.
For those of you looking to setup your own instance of FAYE server on Heroku, take a look at an example FAYE app