Build a Messaging System with Rails and ActionCable

Share this article

Build a Messaging System with Rails and ActionCable

About a year ago, I wrote an article about Mailboxer – a gem to send messages in Rails applications. This gem provides a nice set of functionality, however it has a bunch of issues:

  • It lacks proper documentation.
  • It is pretty complex, especially for junior programmers. Since it does not have extensive documentation, it is sometimes required to dive into the source code to understand how some method works.
  • It is not actively maintained.
  • It has some bugs and, as long as it is not actively evolving, who knows when these bugs will be fixed.

In the past few months, I’ve received many questions about Mailboxer and therefore decided to explain how to create a custom messaging system for Rails. Of course, this system will not provide all of the features Mailboxer has, but it will be more than enough for many application. When you fully understand how this system works, it’ll be much easier to further enhance it.

This article is going to be divided into two parts:

  • The first part will cover preparations, associations setup, and the creation of controllers and views. We will also add support for Emoji. By the end of this part, you will have a working messaging system.
  • The second part will explain how to utilize ActionCable to implement a real-time messaging and notification system. ActionCable is probably the most anticipated feature of Rails 5 and I already covered it some time ago. On top of that, we will also implement “user is online” functionality.

The source code is available at GitHub.

The working demo is available at sitepoint-custom-messaging.herokuapp.com.

Authentication

Take a deep breath and create a new Rails app:

$ rails new Messager -T

ActionCable was introduced in Rails 5, therefore some parts of this article do not apply to earlier versions. Still, the not-ActionCable concepts are valid for Rails 3 and 4.

We will need a way to authenticate users, so let’s go with Devise:

Gemfile

# ...
gem 'devise'
# ...

Run the following commands to install Devise and create the necessary files:

$ bundle install
$ rails generate devise:install
$ rails generate devise User
$ rails db:migrate

Now the User model is in place and equipped with Devise’s magic. Let’s add a simple partial displaying flash messages and a couple of links:

shared/_menu.html.erb

<% flash.each do |key, value| %>
  <div>
    <%= value %>
  </div>
<% end %>

<% if user_signed_in? %>
  <p>Signed in as <%= current_user.name %> | <%= link_to 'log out', destroy_user_session_path, method: :delete %></p>
<% end %>
<ul>
  <li><%= link_to 'Home', root_path %></li>
</ul>

Render this partial in the layout:

layouts/application.html.erb

<%= render 'shared/menu' %>

Our users don’t actually have a name, but Devise does introduce an email column so let’s utilize it instead:

models/user.rb

def name
  email.split('@')[0]
end

Creating Models and Establishing Associations

Next, we will require two more models and here are their main fields:

Conversation

  • author_id – indexed field specifying who started the conversation
  • recevier_id– indexed field specifying who is the receiver. We will use Facebook’s conversation system as an example, sothere can be only one conversation between two specific users.

PersonalMessage

  • body – the actual text of the message
  • conversation_id – indexed field specifying the parent conversation
  • user_id – indexed field specifying who the author of the message

That’s it. Generate these two models and the corresponding migrations:

$ rails g model Conversation author_id:integer:index receiver_id:integer:index
$ rails g model PersonalMessage body:text conversation:belongs_to user:belongs_to

Tweak the first migration by adding yet another index ensuring the uniqueness of the author_id and receiver_id combination:

db/migrate/xyz_create_conversations.rb

# ...
add_index :conversations, [:author_id, :receiver_id], unique: true
# ...

Apply migrations:

$ rails db:migrate

Now the tricky part. Conversations should belong to an author and receiver, but in reality those two are the same User model. This requires us to provide a special option for the belongs_to method:

models/conversation.rb

# ...
belongs_to :author, class_name: 'User'
belongs_to :receiver, class_name: 'User'
# ...

To learn more about Rails association read this article. On the user side, we also need to establish two relations:

models/user.rb

# ...
has_many :authored_conversations, class_name: 'Conversation', foreign_key: 'author_id'
has_many :received_conversations, class_name: 'Conversation', foreign_key: 'received_id'
# ...

In this case, it is required to specify which foreign key to use because, by default, Rails uses the association name to infer the key’s name.

Let’s also create a validation to ensure that there cannot be two conversations between the same two users:

models/conversation.rb

# ...
validates :author, uniqueness: {scope: :receiver}
# ...

PersonalMessage also needs to be tackled. Set up a has_many relation on the conversation side, specifying the default sorting rule (the oldest one comes first):

models/conversation.rb

# ...
has_many :personal_messages, -> { order(created_at: :asc) }, dependent: :destroy
# ...

Personal message belongs to conversation and user:

models/personal_message.rb

# ...
belongs_to :conversation
belongs_to :user
# ...

Also, while we are here, let’s add a simple validation rule:

models/personal_message.rb

# ...
validates :body, presence: true
# ...

Lastly take care of the User model:

models/user.rb

# ...
has_many :personal_messages, dependent: :destroy
# ...

Great, all associations are now set. Let’s proceed to the controllers, views, and routes.

Displaying Conversations

First of all, add a global before_action enforcing users to authenticate (this method is provided by Devise):

application_controller.rb

# ...
before_action :authenticate_user!
# ...

On the main page of our app I want to list all conversations in which the current user participates. The problem, however, is that “participates” means that they are either an author or a receiver:

conversations_controller.rb

class ConversationsController < ApplicationController
  def index
    @conversations = Conversation.participating(current_user).order('updated_at DESC')
  end
end

To make this work, introduce a new scope called participating:

models/conversation.rb

# ...
scope :participating, -> (user) do
  where("(conversations.author_id = ? OR conversations.receiver_id = ?)", user.id, user.id)
end
# ...

Nice. Add the root route:

config/routes.rb

# ...
root 'conversations#index'
# ...

Create the view

views/conversations/index.html.erb

<h1>Your conversations</h1>

<div id="conversations">
  <%= render @conversations %>
</div>

Add the partial to render a conversation:

views/conversations/_conversation.html.erb

<div>
  Chatting with <%= conversation.with(current_user).name %>
  <br>
  <em><%= conversation.personal_messages.last.body.truncate(50) %></em>
  <br>
  <%= link_to 'View conversation', conversation_path(conversation) %>
  <hr>
</div>

conversation_path will be tackled later. with is a method that we’ll create now that returns the other participant of a conversation:

models/conversation.rb

# ...
def with(current_user)
  author == current_user ? receiver : author
end
# ...

Add the show action for ConversationsController. Before calling this action, however, we must make sure that the user is actually authorized to view the requested conversation:

conversations_controller.rb

# ...
before_action :set_conversation, except: [:index]
before_action :check_participating!, except: [:index]

def show
  @personal_message = PersonalMessage.new
end

private

def set_conversation
  @conversation = Conversation.find_by(id: params[:id])
end

def check_participating!
  redirect_to root_path unless @conversation && @conversation.participates?(current_user)
end
# ...

Inside the show action we instantiate the @personal_message variable, as it will be used inside the view to render a form. participates? is yet another instance method:

models/conversation.rb

# ...
def participates?(user)
  author == user || receiver == user
end
# ...

Now here is the view:

views/conversations/show.html.erb

<h1>Chatting with <%= @conversation.with(current_user).name %></h1>

<div id="conversation-main">
  <div id="conversation-body">
    <%= render @conversation.personal_messages %>
  </div>

  <%= form_for @personal_message do |f| %>
    <%= hidden_field_tag 'conversation_id', @conversation.id %>
    <%= f.label :body %>
    <%= f.text_area :body %>

    <%= f.submit %>
  <% end %>
</div>

This view is actually pretty simple. First, render the list of the already existing messages and then provide a form to send a new message.

Create a partial to display the actual message:

views/personal_messages/_personal_message.html.erb

<p><%= personal_message.body %></p>
<p>at <strong><%= personal_message.created_at %></strong><br>
  by <strong><%= personal_message.user.name %></strong></p>
<hr>

Responding to Conversations

Now, of course, we need a new controller to handle the personal messages, so create one:

personal_messages_controller.rb

class PersonalMessagesController < ApplicationController
  before_action :find_conversation!

  def create
    @personal_message = current_user.personal_messages.build(personal_message_params)
    @personal_message.conversation_id = @conversation.id
    @personal_message.save!

    flash[:success] = "Your message was sent!"
    redirect_to conversation_path(@conversation)
  end

  private

  def personal_message_params
    params.require(:personal_message).permit(:body)
  end

  def find_conversation!
    @conversation = Conversation.find_by(id: params[:conversation_id])
    redirect_to(root_path) and return unless @conversation && @conversation.participates?(current_user)
  end
end

Inside before_action, try to find a conversation by its id. Then, if found and the user participates in it, build a new personal message and save it. Finally, redirect back to the conversation’s page.

Next add the routes:

config/routes.rb

# ...
resources :personal_messages, only: [:create]
resources :conversations, only: [:index, :show]
# ...

Starting a New Conversation

Currently there is no way to start a new conversation with a user, so let’s fix it now. Create a controller to manage users:

users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all
  end
end

An index view:

views/users/index.html.erb

<h1>Users</h1>

<ul><%= render @users %></ul>

and the partial:

views/users/_user.html.erb

<li>
  <%= user.name %> | <%= link_to 'send a message', new_personal_message_path(receiver_id: user) %>
</li>

Tweak the routes:

config/routes.rb

# ...
resources :users, only: [:index]
resources :personal_messages, only: [:new, :create]
# ...

Also present a new link in the menu:

shared/_menu.html.erb

# ...
<ul>
  <li><%= link_to 'Home', root_path %></li>
  <li><%= link_to 'Users', users_path %></li>
</ul>

Now add the new action for the PersonalMessagesController:

personal_messages_controller.rb

# ...
def new
  @personal_message = current_user.personal_messages.build
end
# ...

There is a problem, however. When the find_conversation! method is called as a part of before_action, we say @conversation = Conversation.find_by(id: params[:conversation_id]), but there is no :conversation_id when a user clicks the “send a message” link. Therefore, we need to introduce a bit more complex logic:

  • If the :receiver_id is set (that is, the “send a message” link was clicked), we try to find the other user to address the message.
  • If the user was not found, redirect to the root path (of course, you may display an error of some kind).
  • If the user was found, check if the conversation between him and the current user already exist.
  • If the conversation does exist, redirect to the “show conversation” action.
  • If it does not exist, render the form to start a conversation.
  • Lastly, if the :receiver_id id is not set, we try to find an existing conversation by :conversation_id.

Here is the updated find_conversation! method and the new action:

conversations_controller.rb

# ...
def new
  redirect_to conversation_path(@conversation) and return if @conversation
  @personal_message = current_user.personal_messages.build
end

private

def find_conversation!
  if params[:receiver_id]
    @receiver = User.find_by(id: params[:receiver_id])
    redirect_to(root_path) and return unless @receiver
    @conversation = Conversation.between(current_user.id, @receiver.id)[0]
  else
    @conversation = Conversation.find_by(id: params[:conversation_id])
    redirect_to(root_path) and return unless @conversation && @conversation.participates?(current_user)
  end
end
# ...

between is a scope that returns a conversation for two users:

models/conversation.rb

# ...
scope :between, -> (sender_id, receiver_id) do
  where(author_id: sender_id, receiver_id: receiver_id).or(where(author_id: receiver_id, receiver_id: sender_id)).limit(1)
end
# ...

Here is the view:

views/personal_messages/new.html.erb

<h1>New message to <%= @receiver.name %></h1>

<%= form_for @personal_message do |f| %>
  <%= hidden_field_tag 'receiver_id', @receiver.id %>

  <%= f.label :body %>
  <%= f.text_area :body %>

  <%= f.submit %>
<% end %>

The create action also requires a small change. Currently we don’t take into account that the conversation might not exist, so fix it now:

personal_messages_controller.rb

# ...
def create
  @conversation ||= Conversation.create(author_id: current_user.id,
                                        receiver_id: @receiver.id)
  @personal_message = current_user.personal_messages.build(personal_message_params)
  @personal_message.conversation_id = @conversation.id
  @personal_message.save!

  flash[:success] = "Your message was sent!"
  redirect_to conversation_path(@conversation)
end
# ...

This is it! Our messaging system is done and you can see it in action!

A Bit of Styling

As long as we display the newest messages at the bottom, let’s style the conversation page a bit to make it more user-friendly:

application.scss

#conversation-body {
  max-height: 400px;
  overflow-y: auto;
  margin-bottom: 2em;
}

In most cases, the user is interested in recent messages, so scrolling to the bottom of the messages box is a good idea as well:

javascripts/conversations.coffee

jQuery(document).on 'turbolinks:load', ->
  messages = $('#conversation-body')
  if messages.length > 0
    messages_to_bottom = -> messages.scrollTop(messages.prop("scrollHeight"))

    messages_to_bottom()

We basically define a function and call it as soon as the page is loaded. If you are not using Turbolinks, the first line should be

jQuery ->

Lastly, require this CoffeeScript file:

javascripts/application.js

//= require conversations

Adding Support for Emoji

Smileys make conversation on the Internet a bit more colorful (well, unless someone is heavily abusing them). Therefore, why don’t we add support for Emoji in our app? This is easy with the emoji gem. Drop it into the Gemfile:

Gemfile

# ...
gem 'emoji'
# ...

and install by running:

$ bundle install

Add a new helper method found here:

application_helper.rb

# ...
def emojify(content)
  h(content).to_str.gsub(/:([\w+-]+):/) do |match|
    if emoji = Emoji.find_by_alias($1)
      %(<img alt="#$1" src="#{image_path("emoji/#{emoji.image_filename}")}" style="vertical-align:middle" width="20" height="20" />)
    else
      match
    end
  end.html_safe if content.present?
end
# ...

This method can be used in any view or partial:

views/personal_messages/_personal_message.html.erb

<p><%= emojify personal_message.body %></p>
<p>at <strong><%= personal_message.created_at %></strong><br>
  by <strong><%= personal_message.user.name %></strong></p>
<hr>

You may also present a link to the Emoji cheat sheet somewhere in your app.

Conclusion

OK, the first version of our messaging application is done and working pretty well. In the next part, we will make it more modern by utilizing web sockets powered by ActionCable and implement a “user is online” feature. Meanwhile, if you have any questions, don’t hesitate to contact me.

I thank you for staying with me and see you soon!

Frequently Asked Questions (FAQs) about Building a Messaging System with Rails and ActionCable

How can I add emoji support to my Rails and ActionCable messaging system?

To add emoji support to your Rails and ActionCable messaging system, you need to use a gem like ’emoji_picker’. First, add gem 'emoji_picker' to your Gemfile and run bundle install. Then, you can use the emoji_picker helper method in your form. For example, if you have a form for creating messages, you can add emoji_picker :message, :content to your form. This will add an emoji picker to your form, allowing users to select emojis when creating messages.

How can I integrate Twilio with my Rails and ActionCable messaging system?

Twilio can be integrated into your Rails and ActionCable messaging system to send SMS notifications when a new message is received. First, you need to add the Twilio Ruby gem to your Gemfile with gem 'twilio-ruby' and run bundle install. Then, you need to set up a Twilio account and get your Account SID and Auth Token. You can use these credentials to send SMS notifications from your Rails application. For example, you can create a new Twilio client with client = Twilio::REST::Client.new(account_sid, auth_token) and use client.messages.create to send an SMS.

How can I store emoji in a Rails app with a MySQL database?

To store emoji in a Rails app with a MySQL database, you need to set the correct character set and collation for your database. You can do this by adding encoding: utf8mb4 and collation: utf8mb4_bin to your database.yml file. Then, you need to run rake db:drop db:create db:migrate to recreate your database with the new settings. This will allow you to store emoji in your database.

How can I build a simple chat messaging system in Rails?

Building a simple chat messaging system in Rails involves creating a new Rails application, setting up ActionCable for real-time messaging, creating a Message model and controller, and creating views for displaying and creating messages. You can use the rails new command to create a new Rails application, and rails generate model Message content:text and rails generate controller Messages to create a Message model and controller. Then, you can set up ActionCable by adding mount ActionCable.server => '/cable' to your routes.rb file and creating a new channel for messages.

How can I use the ChatGem in my Rails and ActionCable messaging system?

The ChatGem is a Ruby gem that provides a simple and easy-to-use API for building chat applications. To use the ChatGem in your Rails and ActionCable messaging system, you need to add gem 'chat_gem' to your Gemfile and run bundle install. Then, you can use the ChatGem’s API to create chat rooms, send messages, and manage users. For example, you can use ChatGem::Room.create(name: 'My Chat Room') to create a new chat room, and ChatGem::Message.create(content: 'Hello, world!', room: my_chat_room) to send a message.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

GlennGmessagingRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week