Messaging with Rails and Mailboxer

red mail box

Recently I’ve written a series of articles about crafting chat applications: Mini-Chat with Rails, Realtime Mini-Chat with Rails and Faye, and Mini-chat with Rails and Server-Sent Events. These posts have gained some popularity among readers and I’ve received a lot of feedback in the comments, as well as by email. This is great and, once again, I wanted to thank you guys for sharing your thoughts.

One of the readers mentioned Mailboxer, a nice solution to implement messaging systems. So, I decided to research it and share the results with you.

Mailboxer is a Rails gem that is a part of the social_stream framework for building social networks. It is a generic messaging system that allows any model to act “messageable”, equipping it with some versatile methods. With Mailboxer, you can create conversations with one or more recipients (messages are organized into folders – sentbox, inbox, trash) and send notifications via email. It is even possible to send messages between different models and add attachments! The only drawback is the lack of documentation, so I hope this post will be useful.

We are going to discuss a demonstration app that:

  • Uses basic authentication with Devise
  • Allows users to manage avatars with Gravatar
  • Integrates Mailboxer
  • Creates a GUI for starting new conversations, as well as replying to the existing ones (using Bootstrap’s styles and the Chosen jQuery plugin)
  • Displays folders and allows easily switching between them
  • Allows marking conversations as read, trashing conversations, and restoring them. It’ll clear the trash bin too.
  • Sets up email notifications

Rails 4 will be used for this demo, but nearly the same solution can be implemented with Rails 3.2 (version 3.1 is not supported by Mailboxer anymore).

The source code is available on GitHub.

The working demo can be found at sitepoint-mailboxer.herokuapp.com.

Preparing the App

Suppose we have to create a private messaging system for internal use where co-workers can discuss different topics. This system should allow users to create conversations with an unlimited number of recipients, provide a notification system, and allow the deletion of outdated conversations.

Let’s call it Synergy and start off by creating a new Rails app without the default testing suite:

$ rails new Synergy -T

Drop these gems into your Gemfile (I am going to stick with Bootstrap, but you can use any other CSS framework, employ your own design, or skip prettifying the website completely):

Gemfile

[...]
gem 'bootstrap-sass'
gem 'bootstrap-will_paginate'
gem 'will_paginate'
[...]

Run

$ bundle install

and drop in Bootstrap’s files if you wish to follow along:

application.css.scss

@import 'bootstrap';
@import 'bootstrap/theme';

Next tweak the layout a bit:

layouts/application.html.erb

[...]
<div class="container">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <%= value %>
    </div>
  <% end %>

  <div class="page-header">
    <h1><%= yield :page_header %></h1>
  </div>

  <%= yield %>
</div>
[...]

Let’s also add a helper method to render the page header easily:

application_helper.rb

[...]
def page_header(text)
  content_for(:page_header) { text.to_s }
end
[...]

Authentication

Before implementing messaging functionality, we need a model that will message. Create the User model:

$ rails g model User name:string
$ rake db:migrate

You can use any type of authentication, but I like Devise. Devise’s basic set up is very simple and there are plenty of docs to help you further customize things.

Drop a new gem:

Gemfile

[...]
gem 'devise'
[...]

and install it:

$ bundle install

Now we can take advantage of Devise’s code generator to do some work for us:

$ rails generate devise:install

Be sure to read the post-install message to complete some additional steps. Specifically, you will need to tweak the config.action_mailer.default_url_options setting for development and production, as it will be used to send emails to the users (to help them restore forgotten passwords, for example). Please note that email won’t be sent in development unless you set config.action_mailer.perform_deliveries = true in environments/development.rb.

Here are some examples on how to configure ActionMailer.

When you are ready, run the following command to create the User model with Devise:

$ rails generate devise User
$ rake db:migrate

You may want to check the generated migration before applying it to add some more fields to your table (to enable Confirmable or Lockable modules). You will also need then to tweak the model, accordingly.

Lastly, run

$ rails generate devise:views

to copy Devise’s views directly into your project so they can be modified it a bit. Users will be able to change their names, so add a new field to the registration form:

views/devise/registrations/new.html.erb

[...]
<%= f.label :name %>
<%= f.text_field :name %>
[...]

Drop the same code to the views/devise/registrations/edit.html.erb (or refactor it into a partial) so users can provide their name when registering and, later, edit it.

This is Rails 4, so strong_params are in play. Permit the new :name parameter to be passed:

application_controller.rb

[...]
before_action :configure_permitted_parameters, if: :devise_controller?

protected

def configure_permitted_parameters
  devise_parameter_sanitizer.for(:sign_up) << :name
  devise_parameter_sanitizer.for(:account_update) << :name
end

The devise_controller? method is provided by Devise. Here, we simply permit the :name attribute for account creation and editing. If you forget to do this, users won’t be able to set their names.

At this point, you may also style the views a bit. I will not cover this step, as it’s not too hard and will highly depend on your setup (whether you are using Bootstrap or not). If you’ve decided to use Bootstrap, then flash messages generated by Devise won’t be styled. To fix this, use the SASS @extend method, like this:

application.css.scss

[...]
.alert-notice {
  @extend .alert-success;
}

.alert-alert {
  @extend .alert-warning;
}

Integrating Mailboxer

Great, at last we are ready to proceed with the main task – integrating and setting up Mailboxer.

First of all, add the new gem:

Gemfile

[...]
gem "mailboxer"
[...]

and install it:

$ bundle install

Generate and apply all the required migrations and create an initializer file:

$ rails g mailboxer:install
$ rake db:migrate

Take a look at config/initializers/mailboxer.rb to see which options you can modify. For now, let’s leave this file as is – later we will set up sending email notifications.

Our model requires a small change to equip it with Mailboxer functionality:

models/user.rb

[...]
acts_as_messageable
[...]

Displaying Conversations

As suggested in this small guide on creating a GUI for Mailboxer, the optimal way is to create two controllers: one for messages and one for conversations. Individual messages are grouped into conversations. Later, you’ll see that conversations can be different types.

Begin by creating a new controller for conversations:

conversations_controller.rb

class ConversationsController < ApplicationController
  before_action :authenticate_user!
  before_action :get_mailbox

  def index
    @conversations = @mailbox.inbox.paginate(page: params[:page], per_page: 10)
  end

  private

  def get_mailbox
    @mailbox ||= current_user.mailbox
  end
end

Every user has its own mailbox which, in turn, is divided into the inbox, sentbox, and trash. Currently we are working only with the inbox.

The authenticate_user! method is part of Devise. We only want authenticated users to access our app, so it’s set as the before_action. If a user is not authenticated, she will be redirected to the sign in page.

As you see, I am also using the paginate method provided by will_paginate. The styling for pagination is provided by bootstrap-will_paginate.

Add some routes (the other controller methods will be added soon):

config/routes.rb

[...]
resources :conversations, only: [:index, :show, :destroy]
[...]

Now the view:

views/conversations/index.html.erb

<% page_header "Your Conversations" %>

<ul class="list-group">
  <%= render partial: 'conversations/conversation', collection: @conversations %>
</ul>

<%= will_paginate %>

page_header is our helper method created earlier. will_paginate displays pagination controls (only if there are more than one page). We could write it as will_paginate @conversations but the gem is clever enough to understand what we want to paginate in this case (convention over configuration!).

We have to specify the partial argument for render because @conversations is an instance of Mailboxer::Conversation::ActiveRecord_Relation and therefore Rails will look for the _conversation partial located inside the mailboxer/conversations directory by default.

Now the actual partial:

views/conversations/_conversation.html.erb

<li class="list-group-item clearfix">
  <%= link_to conversation.subject, conversation_path(conversation) %>
</li>

Each conversation has a subject and a handful of messages which will be rendered on the show page. The .clearfix CSS class will be required soon.

Add a menu to the layout:

layouts/application.html.erb

[...]
<body>
  <nav class="navbar navbar-inverse">
    <div class="container">
      <div class="navbar-header">
        <%= link_to 'Synergy', root_path, class: 'navbar-brand' %>
      </div>
      <ul class="nav navbar-nav">
        <% if user_signed_in? %>
          <li><%= link_to 'Edit Profile', edit_user_registration_path %></li>
          <li><%= link_to 'Your Conversations', conversations_path %></li>
          <li><%= link_to 'Log Out', destroy_user_session_path, method: :delete %></li>
        <% else %>
          <li><%= link_to 'Log In', new_user_session_path %></li>
        <% end %>
      </ul>
    </div>
  </nav>
</body>
[...]

The user_signed_in? method, as well as most of the routes, are provided by Devise.

Next is the show action:

conversations_controller.rb

class ConversationsController < ApplicationController
  before_action :authenticate_user!
  before_action :get_mailbox
  before_action :get_conversation, except: [:index]

  def show
  end

  private

  def get_conversation
    @conversation ||= @mailbox.conversations.find(params[:id])
  end
end

I’ve added a new before_action and tweaked the existing one.

You probably know that the find method raises an exception when no record is found. This is what we want, but the exception should be rescued. To keep things simple let’s use the rescue_from method:

application_controller.rb

[...]
rescue_from ActiveRecord::RecordNotFound do
  flash[:warning] = 'Resource not found.'
  redirect_back_or root_path
end

def redirect_back_or(path)
  redirect_to request.referer || path
end
[...]

We are simply redirecting the user back with a warning message. If the referer field is not set (for example if a user has installed an add-on to clear this field), they are redirected to the root_path.

Now the view:

views/conversations/show.html.erb

<% page_header "Conversation" %>

<div class="panel panel-default">
  <div class="panel-heading"><%= @conversation.subject %></div>

  <div class="panel-body">
    <div class="messages">
      <% @conversation.receipts_for(current_user).each do |receipt| %>
        <% message = receipt.message %>

        <%= message.sender.name %>
          says at <%= message.created_at.strftime("%-d %B %Y, %H:%M:%S") %>
        <%= message.body %>
      <% end %>
    </div>
  </div>
</div>

We are rendering each message, showing the sender’s name, date of creation, and the body. Let’s style the .messages container a bit so that it does not become too tall:

application.css.scss

[...]
.messages {
  max-height: 400px;
  overflow-y: auto;
  margin-bottom: 1em;
  margin-top: 1em;
}

Nice, some basic views are present, however, we still lack important bits of the app:

  • User should know whom is he chatting with
  • Users need to be able to start new conversations
  • Users should be able to respond to conversations
  • Sentbox and trash should be displayed on the conversations page
  • Users should be able to mark conversations as read

Displaying User Avatars

While this is not related to Mailboxer, I thought that showing avatars would make our app look prettier. However, allowing users to upload their avatars directly into the app would be overkill, so let’s use Gravatar and gravatarimagetag to integrate it with Rails.

Drop a new gem into the Gemfile:

Gemfile

[...]
gem 'gravatar_image_tag'
[...]

and run

$ bundle install

Also, add a helper method to easily render avatars:

application_helper.rb

[...]
def gravatar_for(user, size = 30, title = user.name)
  image_tag gravatar_image_url(user.email, size: size), title: title, class: 'img-rounded'
end
[...]

Create a separate partial to render avatars of a conversation’s participants (except for the current user):

views/conversations/_participants.html.erb

<% conversation.participants.each do |participant| %>
  <% unless participant == current_user %>
    <%= gravatar_for participant %>
  <% end %>
<% end %>

Modify the following views:

views/conversations/show.html.erb

<% page_header "Conversation" %>

<p>Chatting with
  <%= render 'conversations/participants', conversation: @conversation %>
</p>

<div class="panel panel-default">
  <div class="panel-heading"><%= @conversation.subject %></div>

  <div class="panel-body">
    <div class="messages">
      <% @conversation.receipts_for(current_user).each do |receipt| %>
        <div class="media">
          <% message = receipt.message %>
          <div class="media-left">
            <%= gravatar_for message.sender, 45, message.sender.name %>
          </div>

          <div class="media-body">
            <h6 class="media-heading"><%= message.sender.name %>
              says at <%= message.created_at.strftime("%-d %B %Y, %H:%M:%S") %></h6>
            <%= message.body %>
          </div>
        </div>
      <% end %>
    </div>
  </div>
</div>

views/conversations/_conversation.html.erb

<li class="list-group-item clearfix">
  [...]
  <p><%= render 'conversations/participants', conversation: conversation %></p>
</li>

While we are here, display the last message of the conversation and its creation date:

views/conversations/_conversation.html.erb

<li class="list-group-item clearfix">
  [...]
  <p><%= render 'conversations/participant', conversation: conversation %></p>

    <p><%= conversation.last_message.body %>
      <small>(<span class="text-muted"><%= conversation.last_message.created_at.strftime("%-d %B %Y, %H:%M:%S") %></span>)</small></p>
</li>

We’ve finished with avatars. It’s high time we allow users to start conversations.

Creating Conversations

Creating a conversation actually means creating a new message while providing a subject (this is optional though). This means that a new controller will be needed:

messages_controller.rb

class MessagesController < ApplicationController
  before_action :authenticate_user!

  def new
  end

  def create
    recipients = User.where(id: params['recipients'])
    conversation = current_user.send_message(recipients, params[:message][:body], params[:message][:subject]).conversation
    flash[:success] = "Message has been sent!"
    redirect_to conversation_path(conversation)
  end
end

In the create action, find an array of users (stored in the params['recipients']) and utilize Mailboxer’s send_message method, passing in the recipients, body, and the subject. Later, we will enable email notifications so users know when a new message is received.

Now the view:

views/messages/new.html.erb

<% page_header "Start Conversation" %>

<%= form_tag messages_path, method: :post do %>
  <div class="form-group">
    <%= label_tag 'message[subject]', 'Subject' %>
    <%= text_field_tag 'message[subject]', nil, class: 'form-control', required: true %>
  </div>

  <div class="form-group">
    <%= label_tag 'message[body]', 'Message' %>
    <%= text_area_tag 'message[body]', nil, cols: 3, class: 'form-control', required: true %>
  </div>

  <div class="form-group">
    <%= label_tag 'recipients', 'Choose recipients' %>
    <%= select_tag 'recipients', recipients_options, multiple: true, class: 'form-control' %>
  </div>

  <%= submit_tag 'Send', class: 'btn btn-primary' %>
<% end %>

recipients_options is a helper method that we need to create:

messages_helper.rb

module MessagesHelper
  def recipients_options
    s = ''
    User.all.each do |user|
      s << "<option value='#{user.id}'>#{user.name}</option>"
    end
    s.html_safe
  end
end

Don’t forget about the routes:

config/routes.rb

[...]
resources :messages, only: [:new, :create]
[...]

Let’s present a “Start conversation” link on the conversations#index page:

views/conversations/index.html.erb

<% page_header "Your Conversations" %>

<p><%= link_to 'Start conversation', new_message_path, class: 'btn btn-lg btn-primary' %></p>
[...]

Technically, everything is ready to post your first message. You can either have a nice chat with yourself or register another account to emulate situation with two users.

However, selecting recipients is not very convenient. Currently, a basic select field is rendered so, if there are many users, finding someone in the list can be a tedious task. We can enhance this field with some superpowers using Chosen, a jQuery plugin that makes selects more user-friendly. There is a chosen-rails gem that makes integrating this plugin into Rails app easier.

Add this gem to your Gemfile:

Gemfile

[...]
gem 'chosen-rails'
[...]

I also had to specify versions for sass-rails and coffee-rails, as I was getting errors related to the application.css.scss file, which is a known bug):

Gemfile

[...]
gem 'chosen-rails'
gem 'sass-rails', '~> 4.0.5'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-turbolinks'
[...]

Also, I am using the jquery-turbolinks gem to bring back the default jQuery page load event when using Turbolinks.

Don’t forget to run

$ bundle install

and add Chosen to application.js and application.css.scss:

javascripts/application.js

[...]
//= require jquery.turbolinks
//= require chosen-jquery
[...]

stylesheets/application.css.scss

[...]
@import 'chosen';
[...]

Now let’s add the class .chosen-it to our select tag:

views/messages/new.html.erb

[...]
<div class="form-group">
  <%= label_tag 'recipients', 'Choose recipients' %>
  <%= select_tag 'recipients', recipients_options, multiple: true, class: 'form-control chosen-it' %>
</div>
[...]

and equip all elements of this class with Chosen’s magic:

javascripts/messages.coffee

jQuery ->
  $('.chosen-it').chosen()

javascripts/application.js

[...]
//= require messages
[...]

Now reload the server, navigate to conversations/new, and behold the new shiny select tag. It is much more convenient to use, isn’t it?

We could go further and display avatars along with users’ names inside the select tag. There is an Image-Select extension for Chosen. Just hook up ImageSelect.jquery.js and ImageSelect.css files into your project and require them in application.js and application.css.scss, respectively. Then, modify the helper method a bit:

messages_helper.rb

module MessagesHelper
  def recipients_options
    s = ''
    User.all.each do |user|
      s << "<option value='#{user.id}' data-img-src='#{gravatar_image_url(user.email, size: 50)}'>#{user.name}</option>"
    end
    s.html_safe
  end
end

Then reload the server and check the results. Very cool!

Replying to a Conversation

Now, users can create conversations, but there is no option to reply! To fix this, we need another form and a controller method, as well as a new route:

views/conversations/show.html.erb

[...]
<%= form_tag reply_conversation_path(@conversation), method: :post do %>
  <div class="form-group">
    <%= text_area_tag 'body', nil, cols: 3, class: 'form-control', placeholder: 'Type something...', required: true %>
  </div>
  <%= submit_tag "Send Message", class: 'btn btn-primary' %>
<% end %>

You can allow users to add a subject as well by adding another text field. Consider that homework. :)

conversations_controller.rb

[...]
def reply
  current_user.reply_to_conversation(@conversation, params[:body])
  flash[:success] = 'Reply sent'
  redirect_to conversation_path(@conversation)
end
[...]

The Mailboxer method reply_to_conversation makes this a snap. It accepts a conversation to reply to, a message body, an optional subject, and a handful of other arguments. Note that, if the conversation was moved to trash (which we will handle shortly) it will be restored by default. Take a look at the source code for more info.

Now the route:

config/routes.rb

[...]
resources :conversations, only: [:index, :show, :destroy] do
  member do
    post :reply
  end
end
[...]

Very nice, out the basic chat system is up and running!

Implementing Sentbox and Trash

Currently, we are showing only the user’s inbox. However, it’s a good idea to display the sentbox and trash folders, as well.

Probably the easiest way to mark which folder to render is using a GET parameter, so let’s tweak the controller accordingly:

conversations_controller.rb

[...]
before_action :get_box, only: [:index]

def index
  if @box.eql? "inbox"
    @conversations = @mailbox.inbox
  elsif @box.eql? "sent"
    @conversations = @mailbox.sentbox
  else
    @conversations = @mailbox.trash
  end

  @conversations = @conversations.paginate(page: params[:page], per_page: 10)
end

private

def get_box
  if params[:box].blank? or !["inbox","sent","trash"].include?(params[:box])
    params[:box] = 'inbox'
  end
  @box = params[:box]
end
[...]

The new private get_box method is introduced to fetch the requested folder.

On to the view. If you are using Bootstrap, I suggest using vertical navigational pills to render the list of folders. Also, the current folder should be highlighted. Create a helper method for this:

conversations_helper.rb

module ConversationsHelper
  def mailbox_section(title, current_box, opts = {})
    opts[:class] = opts.fetch(:class, '')
    opts[:class] += ' active' if title.downcase == current_box
    content_tag :li, link_to(title.capitalize, conversations_path(box: title.downcase)), opts
  end
end

This method takes the title of the link (which is also used to specify a GET parameter), the currently opened folder, and a hash with options passed directly to the content_tag method. Then, check if the opts hash already has a class key. If not, set it to an empty string and then append an active class if this is the current box.

Change the view:

views/conversations/index.html.erb

<% page_header "Your Conversations" %>

<div class="row">
  <div class="col-sm-3">
    <ul class="nav nav-pills nav-stacked">
      <%= mailbox_section 'inbox', @box %>
      <%= mailbox_section 'sent', @box %>
      <%= mailbox_section 'trash', @box %>
    </ul>
  </div>

  <div class="col-sm-9">
    <ul class="list-group">
      <%= render partial: 'conversations/conversation', collection: @conversations %>
    </ul>

    <%= will_paginate %>
  </div>
</div>

The next step is adding an “Add to trash” button for each conversation that is not yet trashed. For trashed conversations, the “Restore” button should be displayed.

views/conversations/_conversation.html.erb

<li class="list-group-item clearfix">
  <%= link_to conversation.subject, conversation_path(conversation) %>

  <div class="btn-group-vertical pull-right">
    <% if conversation.is_trashed?(current_user) %>
      <%= link_to 'Restore', restore_conversation_path(conversation), class: 'btn btn-xs btn-info', method: :post %>
    <% else %>
      <%= link_to 'Move to trash', conversation_path(conversation), class: 'btn btn-xs btn-danger', method: :delete,
                  data: {confirm: 'Are you sure?'} %>
      <% end %>
    <% end %>
  </div>

  <p><%= render 'conversations/participant', conversation: conversation %></p>

  <p><%= conversation.last_message.body %>
    <small>(<span class="text-muted"><%= conversation.last_message.created_at.strftime("%-d %B %Y, %H:%M:%S") %></span>)</small></p>
</li>

The corresponding methods:

conversations_controller.rb

[...]
before_action :authenticate_user!
before_action :get_mailbox
before_action :get_conversation, except: [:index]
before_action :get_box, only: [:index]

[...]
def destroy
  @conversation.move_to_trash(current_user)
  flash[:success] = 'The conversation was moved to trash.'
  redirect_to conversations_path
end

def restore
  @conversation.untrash(current_user)
  flash[:success] = 'The conversation was restored.'
  redirect_to conversations_path
end
[...]

Once again I’ve tweaked before actions a bit so that they take place only when needed. move_to_trash and untrash are the two methods presented by Mailboxer and they are pretty self-explanatory.

Now the routes:

config/routes.rb

[...]
resources :conversations, only: [:index, :show, :destroy] do
  member do
    post :restore
  end
end
[...]

How about an “Empty trash” button. Easy:

views/conversations/index.html.erb

[...]
<div class="col-sm-9">
  <% if @box == 'trash' %>
    <p><%= link_to 'Empty trash', empty_trash_conversations_path, class: 'btn btn-danger', method: :delete,
                   data: {confirm: 'Are you sure?'} %></p>
  <% end %>
  <ul class="list-group">
    <%= render partial: 'conversations/conversation', collection: @conversations %>
  </ul>

  <%= will_paginate %>
</div>
[...]

The corresponding method:

conversations_controller.rb

before_action :get_conversation, except: [:index, :empty_trash]
[...]
def empty_trash
  @mailbox.trash.each do |conversation|
    conversation.receipts_for(current_user).update_all(deleted: true)
  end
  flash[:success] = 'Your trash was cleaned!'
  redirect_to conversations_path
end
[...]

and the route:

config/routes.rb

resources :conversations, only: [:index, :show, :destroy] do
  collection do
    delete :empty_trash
  end
end

Marking Conversation as Read

Let’s allow user to mark conversations as read. To implement it, we will need yet another method, route, and button:

views/conversations/_conversation.html.erb

[...]
<div class="btn-group-vertical pull-right">
  <% if conversation.is_trashed?(current_user) %>
    <%= link_to 'Restore', restore_conversation_path(conversation), class: 'btn btn-xs btn-info', method: :post %>
  <% else %>
    <%= link_to 'Move to trash', conversation_path(conversation), class: 'btn btn-xs btn-danger', method: :delete,
                data: {confirm: 'Are you sure?'} %>

    <% if conversation.is_unread?(current_user) %>
      <%= link_to 'Mark as read', mark_as_read_conversation_path(conversation),
                  class: 'btn btn-xs btn-info', method: :post %>
    <% end %>
  <% end %>
</div>
[...]

The is_unread? method is used here (a user has to be specified). There is another method is_read? that does the opposite.

conversations_controller.rb

[...]
def mark_as_read
  @conversation.mark_as_read(current_user)
  flash[:success] = 'The conversation was marked as read.'
  redirect_to conversations_path
end
[...]

Lastly, the route:

config/routes.rb

[...]
resources :conversations, only: [:index, :show, :destroy] do
  member do
    post :mark_as_read
  end
end
[...]

Brilliant! You can also tweak the show action so that the conversation is marked as read when it is opened…more homework…

E-mail Notifications

Remember, Mailboxer can send email notifications each time the user receives a message. This feature is enabled in the initializer:

config/initializers/mailboxer.rb

Mailboxer.setup do |config|
  #Configures if you application uses or not email sending for Notifications and Messages
  config.uses_emails = true

  #Configures the default from for emails sent for Messages and Notifications
  config.default_from = "no-reply@mailboxer.com"

  #Configures the methods needed by mailboxer
  config.email_method = :mailboxer_email
  config.name_method = :name
  [...]
end

The config.email_method and config.name_method tell Mailboxer how to get the email and name, respectively. name is already present for our User model, however there is no mailboxer_email. You could try to change this value to just email as this method was added by Devise, but that would result in an error because Mailboxer passes an argument to it which contains the received message. The choices are either redefine this method or create a new one. I will stick with the second option:

user.rb

[...]
def mailboxer_email(object)
  email
end
[...]

Email notifications are now enabled (make sure to set up ActionMailer as described earlier. Also, don’t forget that by default mail will not be sent in development. And yes, I’ve disabled this functionality in the demo app.)

Conclusion

Whew. That was quite a lot to discuss, wasn’t it? We looked at the basic features of Mailboxer, including messages, different types of conversations, managing them, and setting up email notifications. We’ve also integrated Devise into the app and taken advantage of Gravatar to make things look a bit prettier.

I hope this post was useful for you. By the way, you may be interested in this page on the Mailboxer wiki and this sample app presenting the basic features of Mailboxer.

As always, your feedback is welcome. If you want me to cover a specific topic, please don’t hesitate to ask. Happy coding!

UPDATE: 2015/03/29

I’ve received a lot of feedback and questions from the readers – it is really great to know that my articles are useful. There is a question that was asked several times – “How can I add a button to send a message to the specific user”? I believe this is a pretty common feature and decided to add it as an update to the article.

This can be done pretty easily. The specified user should be automatically chosen from a dropdown list on the “Start conversation” page. I think that the best way to provide the user is by using a GET parameter. So modify the MessagesController like this:

messages_controller.rb

def new
  @chosen_recipient = User.find_by(id: params[:to].to_i) if params[:to]
end

Now the @chosen_recipient either contains a user record or a nil value.

The view:

views/messages/new.html.erb

<div class="form-group">
  <%= label_tag 'recipients', 'Choose recipients' %>
  <%= select_tag 'recipients', recipients_options(@chosen_recipient), multiple: true, class: 'form-control chosen-it' %>
</div>

We just pass the @chosen_recipient to the helper method.

messages_helper.rb

def recipients_options(chosen_recipient = nil)
  s = ''
  User.all.each do |user|
    s << "<option value='#{user.id}' data-img-src='#{gravatar_image_url(user.email, size: 50)}' #{'selected' if user == chosen_recipient}>#{user.name}</option>"
  end
  s.html_safe
end

Here is an updated version of the recipients_options helper method. Just set the selected attribute for an option if the user is equal to the selected one.

Basically, that’s it. To demonstrate how this works, add a separate page with a list of users and a “Send message” button next to each one.

config/routes.rb

resources :users, only: [:index]

users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.order('created_at DESC').paginate(page: params[:page], per_page: 30)
  end
end

views/users/index.html.erb

<% page_header "Users" %>

<%= will_paginate %>

<ul>
  <% @users.each do |user| %>
    <li>
      <strong><%= user.name %></strong>
      <% unless current_user == user %>
        <%= link_to 'Send message', new_message_path(to: user.id), class: 'btn btn-default btn-sm' %>
      <% end %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

There you have it. Keep the feedback coming!

Sponsors

No Reader comments