Ruby
Article

Messaging with Rails and Mailboxer

By Ilya Bodrov-Krukowski

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!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
RubyLover

Thank you for creating a real solution on top of the mailboxer gem. As you stated earlier, the documentation is so concise that you can't utilize it's full capabilities without understanding every aspect of it. What I wanted to ask you is how exactly would I allow two different user models to communicate with each other. What you've done so far works great for the normal user interaction, but I created another model with devise biz_users. Must I redo all of the code with a different controller just to allow such a thing or is their an easier way to get both models speaking.

bodrovis

I am glad that this post was useful smile

Well, communicating between the two models should not be much harded that with only one (https://github.com/mailboxer/mailboxer#preparing-your-models). What will cetrainly change is this line https://github.com/bodrovis/SitePoint-Mailboxer/blob/master/app/controllers/messages_controller.rb#L8 because you'll need to add support for another model. But all in all most of the codebase will stay intact. For example, this helper https://github.com/bodrovis/SitePoint-Mailboxer/blob/master/app/helpers/messages_helper.rb#L4 should be changed as well (I am not sure if you want to present both users and biz_users in that list).

So there is no need to create additional controllers - those two are perfectly fit for working with messages and conversations.

RubyLover

Can you give me a quick solution that would work for two user models with the messages_controller and messages_helper. I don't want to end up breaking the one you already have in place.

bodrovis

Well, I've pinpointed lines that need replacing - so just go ahead and modify them so that two user models are being used. Like User.where(id: params['recipients']) || BizUser.where(id: params['recipients']) for example. This way we just allow selecting either User or BizUser. Of course helper should be also changed accordingly (users = User.all + BizUser.all ; users.each do ....).

If you don't want to break the existing code just create a new git branch (like git checkout -b my_branch) and work there.

RubyLover

Great, great. Thank you.

everaldo

Loved the article! You're right, the documentation was too concise. and this is a great help. I've been having some problems implementing the trash method though. I've checked the website you deployed and it seems it's having the same problem as well i.e. can't move a conversation to trash. Currently trying to find a solution for this.

bodrovis

Interesting. I am going to check this one but indeed it was working when I deployed the demo. Thank you for letting me know!

bodrovis

Okay, got it. I've added a piece of code by mistake. conversations_controller.rb, line 2

before_action :get_mailbox, except: [:destroy, :restore]

should be

before_action :get_mailbox

because otherwise there is no mailbox and therefore nowhere to find conversation. Going to fix this.

Levi_Melvin

Great Article. The code your provided works great when using it with two user models. I did run into one small problem. When using mailboxer between two user models, the user id's will conflict (ie. two user may have an id of 5). Is there any way around that?

bodrovis

That's interesting. Unfortunately, I haven't tried to implement such setup. Do you have your code somewhere so that I can a look?

RubyLover

I'm getting errors when I try to start new conversations. Can you please help me figure out exactly why this is happening?

SyntaxError app/views/messages/new.html.erb:5: syntax error, unexpected keyword_class, expecting keyword_do or '{' or '('
'.freeze;@output_buffer.safe_append='

syntax error, unexpected tSTRING_BEG, expecting keyword_end
...=( label_tag 'message[subject]', 'Subject' );@output_buffer....

Started GET "/messages/new" for 127.0.0.1 at 2015-01-21 15:52:18 -0500
Processing by MessagesController#new as HTML
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT 1 [["id", 1]]
User Load (0.0ms) SELECT "users".* FROM "users"
BizUser Load (1.0ms) SELECT "biz_users".* FROM "biz_users"
Rendered messages/new.html.erb within layouts/application (10.0ms)
Completed 500 Internal Server Error in 50ms

ActionView::Template::Error (undefined method firstuser@hotmail.com_attacher' for nil:NilClass):
13:
14: <div class="form-group">
15: <%= label_tag 'recipients', 'Choose recipients' %>
16: <%= select_tag 'recipients', recipients_options, multiple: true, class: 'form-control chosen-it' %>
17: </div>
18:
19: <%= submit_tag 'Send', class: 'btn btn-primary' %>
app/helpers/messages_helper.rb:6:in
block in recipients_options'

I'm getting these errors inside of the messages\new.html.erb form

<h1>Start Conversation</h1>

<%= 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 chosen-it' %>
    </div>

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


def recipients_options
    s = ''
    users = User.all + BizUser.all; users.each do |user|
      s << "<option value=' #{user.id}' data-img-src='#{attachment_url(@user, user.email, :profile_avatar, :fill, 30, 30)}'> #{user.username}</option>"
    end
    s.html_safe
  end

I'm using refile gem btw for image attachments.

bodrovis

I can't find error in this piece of code but that may be somewhere else. Do you have your project somewhere on GitHub or is it private?

RubyLover

I followed the same pattern with refile gem to add functionality for user avatars. But, that approach doesn't work. Instead, I stayed with your method, and I will use refile for other elements in the application.

RubyLover

I wonder if there's a cleaner way to resolve the issues with adding both user models to the mailboxer system. I'm currently getting a long reload time.

UPDATE: It started loading faster. I don't know, maybe it was preparing the db. But, still, this approach of loading all users into the pull down list will become a problem when the user base grows?

Suresh_Kumar

Omg Ilya Bodrov you fulfilled my request. Thank you sooooo much.
Just saw the post. I am gonna try it out now.

bodrovis

It started loading faster. I don't know, maybe it was preparing the db.
But, still, this approach of loading all users into the pull down list
will become a problem when the user base grows?
- yes, of course. If you have lots of users that form with dropdown is going to take much time to load.

You can either use model caching or implement a bit more advanced solution with AJAX to lazy load only the records that match the entered sample. Also it is a nice idea to send AJAX request when at least two (or three maybe) letters are entered in the field. You may also employ a jQuery plugin that fires an event when user stopped typing (or create such plugin yourself as it shouldn't be hard).

bodrovis

Well, I liked your idea and the topic really appeared to be interesting smile

awkward_clam

Awesome guide. I tried a few others and this is by far the best. Thanks

jcoder

This was fantastic! Super helpful and rapid way to get mailboxer up and running with components for customization. Much appreciated!

bodrovis

Oh, thanks a lot!

vrihlea

Firstable I must say that it was difficult for me to find a way to leave a comment.

in views/messages/new.html.erb

<% page_header "Send Message to #{@recipient.name}" %> # @recipient here is nil

I wonder how comes that I'm the only one encountering this issue.

Thank you for the great article, you might want to add some of this stuff to their docs on github. I think they'll appreciate the help.

bodrovis

I'm really sorry, this piece of code was not updated in the tutorial. There should be no @recipient of course: https://github.com/bodrovis/SitePoint-Mailboxer/blob/master/app/views/messages/new.html.erb#L1 I will update this asap. Well noted!

P.S. https://github.com/mailboxer/mailboxer/issues/332 smile

Amit_Joki

It would be useful if the UI of the chat was similar to that of facebook's. Can this be done? The options of trash can all be arranged in the pop up box.

Also the current version can be maintained so it can be used in mobiles(as pop up like facebook in a mobile won't be responsive and clutters the minimal space.)

bodrovis

Well, of course this all can be done, though that would require some time. smile

mattshanklin

Awesome tutorial!!!! I've been trying to integrate this with my app where after a buyer makes a purchase from a seller, they both get routed to a messenger just been them.

How can this be done?

bodrovis

That's an interesting question however I do not think Mailboxer provides such functionality out of the box. Probably what you could do is store buyer's and seller's ids and denote that only those two can participate in some conversation, so that no one else may add messages there.

mattshanklin

Yes, that is exactly what I'm looking for but have no idea on how to implement it. I have the buyer id and seller id stored already.

bodrovis

Hm, okay. I will try to research this one and write you back, as this requires me to do some testing first.

mattshanklin

Thank you very much just for researching it. You're definitely going above and beyond most others smile

winst43

Once again an extremely helpful article. Thank you very much! What I have seen of mailboxer seems to assume that the sender of the message is picking the recipient out of a list of already-registered users. Does mailboxer include an option where, instead of picking from a list, the sender can just enter a name or address, and then the message is sent to that name or address? This would work like, say, gmail--as a sender, you do not need to see a dropdown of all gmail users (which would be a lot!) before sending. Rather, you just type in the address you know, the system then locates the recipient, and the message is sent, or you get an error if the address you entered is incorrect. Does mailboxer allow for something like this? If not, no worries, I will just have to make some custom methods on top of mailboxer I suppose, but just wanted to check. Thanks!

bodrovis

Unfortunately I stil can't start working on this but hopefully today will have some time

bodrovis

Thank you!

Well, of course - this dropdown is how I implemented the whole thing however we could use another approach. Actually this dropdown was borrowed from social networks - normally you see a list of friends there smile

So for e-mail you'd just re-write this line https://github.com/bodrovis/SitePoint-Mailboxer/blob/master/app/controllers/messages_controller.rb#L9 to find users by e-mail, not by id and get rid of dropdown completely.

winst43

Awesome. Thanks for your quick reply. I will give it a shot.

bodrovis

So, here is how I think this process might look:

  1. You already have buyer's and seller's ids saved
  2. You redirect user to the new message page where recipient's id (seller's id) is already pre-populated in the hidden field. If the purchase has its own id of some kind, you may also save it in another hidden field. What would be great is to tie together the exact purchase, the buyer and the seller in some other table. Purchase id should be unique.
  3. Then in the create method https://github.com/bodrovis/SitePoint-Mailboxer/blob/master/app/controllers/messages_controller.rb#L8 you simply check those hidden attributes and if this exact buyer really made that purchase and if that purchase was processed by the provided seller. If yes - send the message.
  4. Pretty much the same can be done when replying to conversation. Once again you just check that the user who wishes to post a message there is a valid buyer or seller. Other guys won't be able to participate in that conversation.

This is pretty basic idea but hopefully it will help you.

winst43

The tutorial goes along way, so thanks again. One thing I am struggling with is customizing the conversation and message objects a bit. Seems difficult to access the message model in order to customize it. For instance, if I want to add a few more potential fields to it, perhaps (i) a field marking the message as important or not, or (ii) a category label the user can add to a message. If it were a normal object, I could just add these items to the database table for the message (ie, t.boolean :important and/or t.string :category or something like that, and run a migration), and/or work with the message.rb file. But I do not seem to be able to access the message table or rb file. Are you aware of how to customize messages and conversations?

mopkaloppt

EDIT: I got it to work now. Nothing's wrong with it actually I just had to reconnect to the server. So go crtl+c then rails s

I have a problem running it.
I've followed your tutorial up until the point before creating Avartar.
Somehow I got the error saying

undefined local variable or method `acts_as_messageable'

Have tried several ways to fix it like adding

include Mailboxer::Models::Messageable
extend Mailboxer::Models::Messageable::ActiveRecord

But nothing works. If you know how to fix this issue I'd really appreciate. Awesome tutorial btw!

bodrovis

Okay, great smile

mopkaloppt

Hey but I have one more issue. I tried to integrate your code for View into my existing application, however nothing shows up even when the code exists in the view. It can compile though so no errors but I just can't see anything at all. This time I already tried disconnecting the server also. No luck so far. Any ideas what could've gone wrong? Thank you so much!

bodrovis

I'd wish to explain what went wrong but without seeing any code that would be hard. Could you share some relevant pieces of code?

Frank004

Great perfect and simple. I update my version with this and workout great.

bodrovis

Glad to be helpful!

Frank004

Can you do a how to do in app notification from mailboxer(Facebook like). Love to see how to use that part of mailboxer.

bodrovis

That depends on what you mean. By all means I'd love to research this topic, moreover, I already has something like this in pipeline (though it will take some time, because currently I am working on authorization and file uploads). Are we talking about notifications in mailboxer (like someone has written you a message)?

Frank004

Yes, like a notification icon on the header of the app so when you login you can view the notification.

mattshanklin

Thank you for explaining the process and sorry for not replying earlier!

I know how to redirect a buyer to the new message page after an order has been charged.

Not sure on how to set the seller's id in the hidden field or how to check the attributes in the messages controller

bodrovis

Well, once again withot seeing the code I can't give some exact advices, but if you already have those ids, then you just utilize hidden_field_tag (http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-hidden_field_tag) to save those values, and then in controller just access params hash and look for the key with the proper name.

hidden_field_tag 'buyer_id', id

I can't really say more here. You can contact me via e-mail and send some code pieces so that we can discuss more

mattshanklin

I figured it out. Thank you again for you kind help. You have been fantastic and so has this tutorial. Keep up the great work.

jedihacks

Hi There,

I've spent a couple hours knocking myself in the head. I've followed the tutorial as exact as I could, adding in conversations and messages. I'm right before the Add Gravatar section....

When I try to go http://localhost:3000/conversations/new, I get an error

undefined local variable or method `get_mailbox' for #<ConversationsController:0x007fb64e862548>

Is there something that's been updated or missing from the tutorial?

Thanks!

bodrovis

Hrm. Could you please check your ConversationsController with the one presented here https://github.com/bodrovis/SitePoint-Mailboxer/blob/master/app/controllers/conversations_controller.rb ? If everything is the same, please open a new issue https://github.com/bodrovis/SitePoint-Mailboxer/issues and provide the full error stack trace, I'll look into it.

alfromas

Great tutorial.
Thanks for sharing it.
I just found some detail missing concerning I18n in the conversation helper.
I had to add t() to title in link_to. All the rest remaining the same.

... link_to(t(title).capitalize ...

With this, I only had to add the 'inbox', 'sent', trash' keys to my translations file and everthing worked fine.
thumbsup

bodrovis

Oh, thanks for pointing that out!

shalafister

Amazing tutorial!!.

Ryan_Davis

Am I the only person that doesn't have the outgoing messages showing up in the sent folder or am I just dumb?

bodrovis

Well, no one has reported such issue, but by all means let's look intro it. Any errors? Does your code match the one that is provided on GitHub https://github.com/bodrovis/SitePoint-Mailboxer? Maybe using other version of Mailboxer?

Kiem_Pinon

I think Mailboxer is a large solution. Recommend beginning from scrap. There is a tutorial for Rails two.0 here which ought to give you an idea . please follow this link http://web.archive.org/web/20100823114059/http://www.novawave.net/public/rails_messaging_tutorial.html

bodrovis

Well, I am sorry, but using Rails 2 now that Rails 5 is announced is probably not the best idea smile

Ryan_Davis

Hi Ilya,

Sorry to pester. I have everything working except the empty button will not work despite having the same code as you frowning

When I try to use the empty button I get a message that says "Resource not found". Any idea where I can focus my attention?

bodrovis

That's interesting. Full error stack trace would be nice. You may open a new issue on GitHub if that's more convenient for you smile

rubygemguy

How to Write a Migration which can add on location attribute to a column in Mailbox messages : right now conversations have body and subject - i want to add location to it smile thanks in advanced!

bodrovis

I believe this is the migration you are looking for https://github.com/bodrovis/SitePoint-Mailboxer/blob/master/db/migrate/20141208140641_create_mailboxer.mailboxer_engine.rb#L23 (just alter it after running rails g mailboxer:install).

rubygemguy

You can add: rails g migration add_admin_to_user admin:string this would mean you would have to manually authorize someone in the console

rubygemguy

Thanks for the quick response!

I ran // rails g migration add_fields_to_mailboxer_notification location:string // added params[:message][:location] to the message_controller.rb // then added the fields in the new conversation message form fields. I don't get an error - Then in the conversation show it dose not show the added fields - nothing shows or happens

Gitrepo: https://github.com/Alloffices/MailboxApp

In the rails c how do ie: conversations.last or messages.find(1)

Angela

Great tutorial, thank you for writing. Though I ran into two issues.

  1. I get an error when sending messages: wrong number of arguments (1 for 0)
    It points to this line in the messages_controller:
    conversation = current_user.send_message(recipients, params[:message][:body], params[:message][:subject]).conversation

Here is a stack trace:

Started POST "/messages" for 10.0.2.2 at 2015-10-10 16:23:39 +0000
Processing by MessagesController#create as HTML
 Parameters: {"utf8"=>"✓", "authenticity_token"=>"YiqT94X0gvp2W9aT1PNTx6iRM3E40ozMlkgZsU6LGpfoBxTygHFdBSkeyUYHHwGwWX+x8QpjJTEj4HYyrqhxng==", "message"=>{"subject"=>"work", "body"=>"pleas work this time!"}, "recipients"=>["2"], "commit"=>"Send"}
User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1  ORDER BY "users"."id" ASC LIMIT 1  [["id", 1]]
User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 2
(0.2ms)  BEGIN
SQL (0.7ms)  INSERT INTO "mailboxer_conversations" ("subject", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["subject", "work"], ["created_at", "2015-10-10 16:23:39.424583"], ["updated_at", "2015-10-10 16:23:39.424583"]]
SQL (0.3ms)  INSERT INTO "mailboxer_notifications" ("type", "sender_id", "sender_type", "body", "subject", "created_at", "updated_at", "conversation_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id"  [["type", "Mailboxer::Message"], ["sender_id", 1], ["sender_type", "User"], ["body", "pleas work this time!"], ["subject", "work"], ["created_at", "2015-10-10 16:23:39.424583"], ["updated_at", "2015-10-10 16:23:39.424583"], ["conversation_id", 10]]
SQL (0.2ms)  INSERT INTO "mailboxer_receipts" ("mailbox_type", "receiver_id", "receiver_type", "notification_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["mailbox_type", "inbox"], ["receiver_id", 2], ["receiver_type", "User"], ["notification_id", 12], ["created_at", "2015-10-10 16:23:39.436761"], ["updated_at", "2015-10-10 16:23:39.436761"]]
(0.8ms)  COMMIT
(0.3ms)  BEGIN
SQL (0.2ms)  INSERT INTO "mailboxer_receipts" ("mailbox_type", "receiver_id", "receiver_type", "is_read", "notification_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id"  [["mailbox_type", "sentbox"], ["receiver_id", 1], ["receiver_type", "User"], ["is_read", "t"], ["notification_id", 12], ["created_at", "2015-10-10 16:23:39.441001"], ["updated_at", "2015-10-10 16:23:39.441001"]]
(0.5ms)  COMMIT
(0.2ms)  SELECT COUNT(*) FROM "mailboxer_conversation_opt_outs" WHERE "mailboxer_conversation_opt_outs"."conversation_id" = $1 AND "mailboxer_conversation_opt_outs"."unsubscriber_type" = $2 AND "mailboxer_conversation_opt_outs"."unsubscriber_id" = $3  [["conversation_id", 10], ["unsubscriber_type", "User"], ["unsubscriber_id", 2]]
Completed 500 Internal Server Error in 23ms (ActiveRecord: 3.8ms)

ArgumentError (wrong number of arguments (1 for 0)):
app/controllers/messages_controller.rb:9:in `create'

Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/actionpack-4.2.4/lib/action_dispatch/middleware/templates/rescues/_source.erb (4.5ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/actionpack-4.2.4/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (3.0ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/actionpack-4.2.4/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (1.2ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/actionpack-4.2.4/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb within rescues/layout (23.1ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/_markup.html.erb (0.3ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/_inner_console_markup.html.erb within layouts/inlined_string (0.2ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/_prompt_box_markup.html.erb within layouts/inlined_string (0.2ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/style.css.erb within layouts/inlined_string (0.2ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/console.js.erb within layouts/javascript (12.8ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/main.js.erb within layouts/javascript (0.4ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/error_page.js.erb within layouts/javascript (0.4ms)
Rendered /usr/local/rvm/gems/ruby-2.2.3/gems/web-console-2.2.1/lib/web_console/templates/index.html.erb (27.3ms)
  1. I am getting the "Resource Not Found" message when I try to empty the trash.

    Started DELETE "/conversations/empty_trash" for 10.0.2.2 at 2015-10-10 16:21:49 +0000
    Processing by ConversationsController#empty_trash as HTML
    Parameters: {"authenticity_token"=>"mhCTp4FfbBlr573VGWT8PKM16Lu5pAKo93nHVfNMF075wWvnIUMkpO+ipat7rsP3dLNWf7WyYmXxphcQK6C+0w=="}
    User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]
    Redirected to http://localhost:3000/conversations?box=trash
    Completed 302 Found in 4ms (ActiveRecord: 0.3ms)

Started GET "/conversations?box=trash" for 10.0.2.2 at 2015-10-10 16:21:49 +0000
Processing by ConversationsController#index as HTML
Parameters: {"box"=>"trash"}
User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1  ORDER BY "users"."id" ASC LIMIT 1  [["id", 2]]
(0.4ms)  SELECT DISTINCT COUNT(DISTINCT "mailboxer_conversations"."id") FROM "mailboxer_conversations" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."conversation_id" = "mailboxer_conversations"."id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') INNER JOIN "mailboxer_receipts" ON "mailboxer_receipts"."notification_id" = "mailboxer_notifications"."id" WHERE "mailboxer_notifications"."type" = 'Mailboxer::Message' AND "mailboxer_receipts"."mailbox_type" = $1 AND "mailboxer_receipts"."trashed" = $2 AND "mailboxer_receipts"."deleted" = $3 AND "mailboxer_notifications"."type" = 'Mailboxer::Message' AND "mailboxer_receipts"."receiver_id" = $4 AND "mailboxer_receipts"."receiver_type" = $5 AND "mailboxer_receipts"."is_read" = $6  [["mailbox_type", "inbox"], ["trashed", "f"], ["deleted", "f"], ["receiver_id", 2], ["receiver_type", "User"], ["is_read", "f"]]
(0.8ms)  SELECT DISTINCT COUNT(DISTINCT "mailboxer_conversations"."id") FROM "mailboxer_conversations" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."conversation_id" = "mailboxer_conversations"."id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') INNER JOIN "mailboxer_receipts" ON "mailboxer_receipts"."notification_id" = "mailboxer_notifications"."id" WHERE "mailboxer_notifications"."type" = 'Mailboxer::Message' AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Conversation Load (0.7ms)  SELECT  DISTINCT "mailboxer_conversations".* FROM "mailboxer_conversations" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."conversation_id" = "mailboxer_conversations"."id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') INNER JOIN "mailboxer_receipts" ON "mailboxer_receipts"."notification_id" = "mailboxer_notifications"."id" WHERE "mailboxer_notifications"."type" = 'Mailboxer::Message' AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  ORDER BY mailboxer_conversations.updated_at DESC LIMIT 10 OFFSET 0  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
(0.7ms)  SELECT COUNT(*) FROM "mailboxer_receipts" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."id" = "mailboxer_receipts"."notification_id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') WHERE "mailboxer_notifications"."conversation_id" = 9 AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Message Load (0.4ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at LIMIT 1  [["conversation_id", 9]]
Mailboxer::Receipt Load (0.2ms)  SELECT "mailboxer_receipts".* FROM "mailboxer_receipts" WHERE "mailboxer_receipts"."notification_id" = $1  [["notification_id", 11]]
User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 2]]
User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Rendered conversations/_participants.html.erb (6.2ms)
Mailboxer::Message Load (0.2ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at DESC LIMIT 1  [["conversation_id", 9]]
(0.5ms)  SELECT COUNT(*) FROM "mailboxer_receipts" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."id" = "mailboxer_receipts"."notification_id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') WHERE "mailboxer_notifications"."conversation_id" = 8 AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Message Load (0.3ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at LIMIT 1  [["conversation_id", 8]]
Mailboxer::Receipt Load (0.1ms)  SELECT "mailboxer_receipts".* FROM "mailboxer_receipts" WHERE "mailboxer_receipts"."notification_id" = $1  [["notification_id", 10]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 2]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Rendered conversations/_participants.html.erb (5.6ms)
Mailboxer::Message Load (0.3ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at DESC LIMIT 1  [["conversation_id", 8]]
(0.3ms)  SELECT COUNT(*) FROM "mailboxer_receipts" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."id" = "mailboxer_receipts"."notification_id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') WHERE "mailboxer_notifications"."conversation_id" = 7 AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Message Load (0.2ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at LIMIT 1  [["conversation_id", 7]]
Mailboxer::Receipt Load (0.1ms)  SELECT "mailboxer_receipts".* FROM "mailboxer_receipts" WHERE "mailboxer_receipts"."notification_id" = $1  [["notification_id", 9]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 2]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Rendered conversations/_participants.html.erb (5.2ms)
Mailboxer::Message Load (0.4ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at DESC LIMIT 1  [["conversation_id", 7]]
(0.5ms)  SELECT COUNT(*) FROM "mailboxer_receipts" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."id" = "mailboxer_receipts"."notification_id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') WHERE "mailboxer_notifications"."conversation_id" = 6 AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Message Load (0.3ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at LIMIT 1  [["conversation_id", 6]]
Mailboxer::Receipt Load (0.2ms)  SELECT "mailboxer_receipts".* FROM "mailboxer_receipts" WHERE "mailboxer_receipts"."notification_id" = $1  [["notification_id", 8]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 2]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Rendered conversations/_participants.html.erb (6.1ms)
Mailboxer::Message Load (0.5ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at DESC LIMIT 1  [["conversation_id", 6]]
(0.4ms)  SELECT COUNT(*) FROM "mailboxer_receipts" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."id" = "mailboxer_receipts"."notification_id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') WHERE "mailboxer_notifications"."conversation_id" = 4 AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Message Load (0.3ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at LIMIT 1  [["conversation_id", 4]]
Mailboxer::Receipt Load (0.2ms)  SELECT "mailboxer_receipts".* FROM "mailboxer_receipts" WHERE "mailboxer_receipts"."notification_id" = $1  [["notification_id", 4]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 2]]
Rendered conversations/_participants.html.erb (5.2ms)
Mailboxer::Message Load (0.2ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at DESC LIMIT 1  [["conversation_id", 4]]
(0.5ms)  SELECT COUNT(*) FROM "mailboxer_receipts" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."id" = "mailboxer_receipts"."notification_id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') WHERE "mailboxer_notifications"."conversation_id" = 2 AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Message Load (0.3ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at LIMIT 1  [["conversation_id", 2]]
Mailboxer::Receipt Load (0.2ms)  SELECT "mailboxer_receipts".* FROM "mailboxer_receipts" WHERE "mailboxer_receipts"."notification_id" = $1  [["notification_id", 2]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 2]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Rendered conversations/_participants.html.erb (6.2ms)
Mailboxer::Message Load (0.6ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at DESC LIMIT 1  [["conversation_id", 2]]
(0.7ms)  SELECT COUNT(*) FROM "mailboxer_receipts" INNER JOIN "mailboxer_notifications" ON "mailboxer_notifications"."id" = "mailboxer_receipts"."notification_id" AND "mailboxer_notifications"."type" IN ('Mailboxer::Message') WHERE "mailboxer_notifications"."conversation_id" = 1 AND "mailboxer_receipts"."receiver_id" = $1 AND "mailboxer_receipts"."receiver_type" = $2 AND "mailboxer_receipts"."trashed" = $3 AND "mailboxer_receipts"."deleted" = $4  [["receiver_id", 2], ["receiver_type", "User"], ["trashed", "t"], ["deleted", "f"]]
Mailboxer::Message Load (0.3ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at LIMIT 1  [["conversation_id", 1]]
Mailboxer::Receipt Load (0.2ms)  SELECT "mailboxer_receipts".* FROM "mailboxer_receipts" WHERE "mailboxer_receipts"."notification_id" = $1  [["notification_id", 1]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 2]]
CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
Rendered conversations/_participants.html.erb (5.7ms)
Mailboxer::Message Load (0.3ms)  SELECT  "mailboxer_notifications".* FROM "mailboxer_notifications" WHERE "mailboxer_notifications"."type" IN ('Mailboxer::Message') AND "mailboxer_notifications"."conversation_id" = $1  ORDER BY created_at DESC LIMIT 1  [["conversation_id", 1]]
Rendered conversations/_conversation.html.erb (163.7ms)
Rendered conversations/index.html.erb within layouts/application (186.6ms)
Rendered layouts/_nav.html.erb (3.0ms)
Rendered layouts/_alert.html.erb (0.7ms)

Completed 200 OK in 637ms (Views: 621.1ms | ActiveRecord: 12.2ms)

If you need any more information, please let me know. I have been staring at this code for days now trying to figure out what's wrong.

Thanks

bodrovis

I'll look into it!

bodrovis

Do you have this project on GitHub?

bodrovis

Well, unfortunately I wasn't able to boot the app, because it contains too many specific configuration that I have to alter. If you could make this more straighforward (like using sqlite3), I'd try to play with it a bit more. All in all, I don't really understand what could be the cause, as I my demo app is working correctly with new mailboxer. What I would recommend is just compare your code with mine to see any differences.

Angela

Thanks for giving it a shot. I did fix the empty trash issue. I forgot to add :empty_trash to this line in the conversations_controller.rb

 before_action :get_conversation, except: [:index, :empty_trash]

I will continue to compare your code with mine and post the answer, just in case someone else runs into this.

luisbraga

Thank you for this awesome tutorial!

This is probably easy but I'm totally lost.
Basically I need to let a user send a message to multiple users but keep conversations 'filtered' in a way that recipients can only see the messages from the originator and the ones they own.

Does it make sense? Is this suitable for this?

UPDATE
I've edited the show view like this:

  <div class="panel-body">
    <% @conversation.messages.each do |message| %>
      <% if message.sender == @conversation.originator or message.sender == current_user %>
      <div class="messages">
        <div class="media">
          <div class="media-left">
            <%= gravatar_for message.sender, 45, message.sender.full_name %>
          </div>
          <div class="media-body">
            <h6 class="media-heading">
              <%= message.sender.full_name %> (<%= message.created_at.strftime("%-d %B %Y, %H:%M") %>)
            </h6>
            <p><%= message.body %></p>
          </div>
        </div>
      </div>
      <hr>
      <% end %>
    <% end %>
  </div>

Is there something more I should take into account?
Is this the best way to do it?

bodrovis

Well, you might extract those conditions somewhere from the views, but all in all this seems to be okay!

luisbraga

Thank you! Actually this won't work for what I'm trying to achieve.

Is it possible to split a new message in different conversations depending on the number of recipients chosen?

luisbraga

Is this ok?

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

Of course, I haven't tested, but at the first glance it seems okay, though I'm not sure why would you need this :smile:

luisbraga

I need some way a user can send the same message to multiple users but keep the conversations separated.
I guess this may sound weird and it misses the all idea of conversations.

I was thinking I could make this the default behavior and add a checkbox on new message view to set the message to be a private conversation (splitting it) or grouped.

Does it make sense? Is it possible or am I thinking it the wrong way and should build something from scratch?

Angela

I'm not sure what's wrong, but my issue was fixed by copying the initializer from the demo code into my project. Go figure. Thanks for the tutorial!

kjb513

Thanks so much for taking the time to write this. It really helped in getting mailboxer setup. I couldn't find any solid docs on implementing the Message#create method for multiple models, since the recipients array is just id's (might be possible/easier if looking up by unique usernames). I ended up doing it this way for anyone that needs some direction.

In the recipients_options helper, I updated the value to include the class (User, Duck, Cylon, etc.):

<option value='#{user.class.name}:#{user.user_id}'>#{user.name}</option>

Then in the Messages controller, I just had to break it apart to figure out what type of user it is:

def create
    recipients = []
    params[:recipients].each do |r|
      if r.split(':').first === 'User'
        recipient = User.find(r.split(':').last)
        recipients << recipient
      elsif r.split(':').first === 'Duck'
        recipient = Duck.find(r.split(':').last)
        recipients << recipient
      elsif r.split(':').first === 'Cylon'
        recipient = Cylon.find(r.split(':').last)
        recipients << recipient
      end
    end
    conversation = @user.send_message(recipients, params[:message][:body], params[:message][:subject]).conversation
    flash[:success] = "Message has been sent!"
    redirect_to conversation_path(conversation)
  end

@bodrovis - Would love to hear your thoughts on this execution.

bodrovis

I'll try to look into it.

bodrovis

I believe you can stick with eval here.

recipient = eval("#{r.split(':').first}.find(r.split(':').last)"

Just add some error checkings and whitelist models that can be referenced.

kjb513

@bodrovis That certainly looks cleaner. I'll take a look at implementing. Thanks again for the update and for writing this great resource.

Bingo2

Hi there

Is this possible to create more than two conversations between two same users?
When I try to do this, it throws:

Validation failed: Notification base This message must be added to existing conversation (#)
bodrovis

Hi! That depends on what code are you using. Generally, yes it should be possible.

Bingo2

@bodrovis, I am calling

user.send_message(recipient, 'body', 'subject')

each time for same user and recipient. So, for each message, should be created a new conversation.

Can you please tell me how can I do this?

bodrovis

If you were following my tutorial, this should work right away. Doesn't it work in the demo app on Heroku? If does, but you are experiencing errors with the same code, that's most likely a bug and should be reported to the dev team I believe.

Pyemg

Hi, Great tutorial, thanks.

You mentionned that attachments can be added to messages.
Could you point me the right direction to do it?

Cheers

Pyemg

Thank you! That was easy :smile:

ryan_s

Hi, I've tried using @chosen_recipients to fill the recipient value with user_id. It is able to grab the id i.e. "new?=2" but it does not populate recipient value when the message is being created and sent. any thoughts?

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Ruby, once a week, for free.