What's up guys? This episode we're talking about how to send messages back and forth between users in your rails application. We're going to use a gem called Mailboxer for this. This gem has been around for like five years. Still continuously getting updated, but I haven't seen a lot of updates on rubygems, so we're going to use the master branch for this. Mailboxer is actually designed to give you an interface that's actually a lot more similar to say: email clients, then regular old messaging systems. It might surprise you a little bit, but there's actually an inbox, there's a sendbox, there's an all conversations area, there's a trash area. That is not common in things like Twitter direct messages, but even Facebook's messaging is going more in this direction as they were experimenting with going after email. So Mailboxer adds that functionality, but you don't have to use it, so it's all entirely up to you. We're going to install this gem into our rails application here, and this rails application is just very simple. All it has is devise and one route, so let's open up that real quick. There's just a homepage, that's all it is, and it doesn't do anything. I just have devise installed, and it's as simple as that. Let's add mailboxer in from the mailboxer GitHub repository because the last time that this gem has been updated was in like 2005, like July or something.
Using this from master is probably the best idea, and hopefully it's pretty stable, I don't see it changing that often, so if you want to make sure that you're on a very stable version, you can probably use the latest version on rubygems, but if there are any fixes that came into the master branch, you might want to take a look at that. That up to you to choose, but really the installation is pretty simple. You run
rails g mailboxer:install, that adds an initalizer and some migrations. You run your migrations and if you want to, you can generate mailboxer views, which are just the email notification templates. They're not views for anything related to the mail or messaging system that's built in. Last but not least, you have to do
acts_as_messageable inside the file to add all the mailbox methods, the send message, like the
reply_to_conversation methods. All of those come from this method that you'll add in there. First thing is first, let's open up the rails console and test to make sure that this is working. I've got two users in the database already, and we can load up the first user, and let's just assign that to the variable
u should have a message which allows you to send a message to another user, and this starts a conversation. This is going to say: Let's pass in the user that we want to talk to, and this could be an array if you want to support multiple users, we'll talk about that in the next episode, and then it takes the body, and a subject. When you send this, you'll see that a bunch of things get added to the database. The first one is that you create a new conversation, then you create some notifications and a receipt, and then another receipt down here. What this is doing is setting up a conversation, and then returning a receipt object, and this is important because we'll use that in the controller in the future, so we're basically going to set up a controller that will call this method for us, and that is how we're going to integrate this with our rails application. This mailboxer gem basically gives you these associations and methods on your models, and it's up to you to set up the controllers and the views for everything. With that said, let's dive into building our routes. We're going to need routes for all the conversations that you have. We're going to make a
resources :conversations. Feel free to name this whatever you would like, but conversations is probably going to be the easier ways to organize this, because we're going to also have a nested resources for messages in order to send a message to a specific conversation. Keep that in mind, you'll need to have both of these nested resources here, and those will be associated with one another. Your naming probably it needs to be careful about, maybe you don't want to call the top level one messages because you'll also need that nested one and that makes it a little harder to name that one. Let's go create our conversations controller
class ConversationsController < ApplicationController def index @conversations = current_user.mailbox end end
mailbox is talked about in the README, this is what every user will have, they will have a mailbox, which is a list of all the available mailboxes. That might be a little confusing, but basically you have
inbox here, you have
sentbox, you have
thrash, and also just all your conversations. We're going to use that by default, you can use the inbox if you want your application to work that way. Let's also make the directory for
<h1>All Conversations</h1> <% @conversations.each do |conversation| %> <div> <%= link_to conversation.subject, conversation_path(conversation) %> </div>
You're going to have to explicitly say conversation_path here, because if you leave that out, it's going to look for Mailboxer::mailboxer_conversation_path, which would mean that your resources routes have to be different. That is what I would recommend is to always just pass in the path explicitly. Here, we should be able to go to /conversations now, and we'll see that conversation, which is awesome. We can't click on it yet, otherwise we'll get this missing action and missing template error, so now we have to go back to conversations controller, and set up the show action. This one is pretty much going to be the same, we'll look up the conversation, we'll
class ConversationsController < ApplicationController def index @conversations = current_user.mailbox.conversations end def show @conversation = current_user.mailbox.conversation.find(params[:id]) end end
The important thing is that you use that association so that you don't allow users to just change the id in the url and read other people's conversations. That wouldn't be good, so you want to scope that to the current user for that reason, and then we can add
<h1><%= @conversation.subject %></h1> <% @conversation.receipts.each do |receipt| %> <div> <%= receipt.message.body %> </div> <% end %>
You'll see that while in our controller we only typed this in one time, but it's showing duplicate, and if we go to this, we also have reference to the sender on the message. The message has a method called
sender and that is a user object, so
sender would be a user or any of the models that you set
acts_as_messageable could be used on users, admin users, any other user object that you would like, and mailboxer is polymorphic, so this could be a user object, an admin user object and so on. So long as they all share the same method name, you'll be able to say: That user commented, and put that in a div before your text. You'll see that user one commented two times, but we definitely didn't do that. We only did it one time, and that's because these receipts have to go and be created for every single person in the conversation. You are in the conversation, yours is automatically going to get marked as read, and you don't need to see your own posts, so you actually have to scope this and say
receipts_for(current_user). This will scope down all of the receipts, so you'll only see the ones that you should be seeing. That allows each user to mark these as read, so when we refresh it we're only going to see it one time now, but the other user, user number two is going to be seeing the exact same thing. Only one reference of this, but you and that other user will be able to mark these as read independently. That's the important piece here, but you don't have to take advantage of that. That's a feature of mailboxer that we'll cover in a future episode, but that's the way it's defined, so you actually have to make sure that you use this
receipts_for method in order to scope these so you don't get duplicates. Now if we were to go to another user's account and look up conversations, this is user number two, this one is user number one, we can both see the same conversation, which is great, but now we need to be able to comment in there, which is where we need to go build that messages routes. We have this defined in our routes, and we just need to create the app/controllers/messages_controller.rb file.
class MessagesController < ApplicationController before_action :set_conversation def create receipt = current_user.reply_to_conversation(@conversation, body) redirect_to receipt.conversation end private def set_conversation @conversation = current_user.mailbox.conversations.find(params[:conversation_id]) end end
<%= form_tag conversation_messages_path(@conversation) %>
We need this form to send over the body, and we don't need to create a message object for that, because we're not creating a new ActiveRecord object, which is a little frustrating because it would be nice to be able to just use strong parameters to say we want the body, and then our form_for could use that, but we just don't have that the way that mailboxer is currently set up as far as I understand, so we're going to set it up this way.
<%= form_tag conversation_messages_path(@conversation), method: post do %> <div> <%= text_area_tag :body %> </div> <%= submit_tag %> <% end %>
What this will be is just params body, and we don't need to do the strong params here, because we're just passing it in directly and we're signing it directly as the body if this
reply_to_conversation method, so this is the way tha we'll do that. I'd like to see the gem get refactored so that it could be a little bit nicer, so that you could just say: Let's create a new message for this conversation and that takes care of it, but that's how it works right now, so this form tag should do the trick, and so now you'll see the form tag and you'll be able to say: If we inspect this, it should go to the conversations messages path, as a post, and then if we type in here, and click "Save changes", we'll see that we get far enough in that we reply to the conversation, but we forgot to wrap this
receipt.conversation in that same conversation path as we had before. We say conversation_path around that. That should fix that trouble, and I'm going to post this again so we'll see a duplicate of this message, but we'll verify that that did correctly redirect this time. We see that duplicate because before, it worked, it just didn't do the redirect correctly. The second time it did both correctly, so this is cool. The use number one can refresh, and we can see that it's working, and then we can jump into the other user and see that that's working as well. Mailboxer is connected, and that's really all the controller and view code that we had to write for that. Of course the last piece that we need to get all the basics set up is we need a new action, and we need a create action for the conversations. These are going to be the last two pieces, and we'll jump into the conversations index and create and we'll create the
<%= link_to "New Conversation", new_conversation_path %>
<h1>New Conversations</h1> <%= form_tag conversations_path, method: :post do %> <div> <%= select_tag :user_id, options_from_collection_for_select(User.all, :id, :name) %> </div> <div> <%= text_field_tag :subject, nil, placeholder: "Subject" %> </div> <div> <%= text_area_tag :body, nil, placeholder: "Leave a comment" %> </div> <%= submit_tag %> <% end %>
When we go back to conversations, we'll be able to click "New Conversation", this is going to now list out the users, and so you can create a conversation with one of those users. The thing is, I'm currently user number one, and I don't really want to allow myself to have a conversation with myself on a website. It's kind of crazy, so what we'll need to do for now is go into the conversations controller, and in the new action we'll set
def new @recipients = User.all - [current_user] end
That's going to just drop it down to user number two. The trouble with this, of course is that that UI is not the best, but also it's going to load up every single user in the database, and then subtract you out of that, and that's fine, but it's usually better in production to have like a search box, so you would have like Facebook does, where it auto completes users and loads those up via AJAX and loads all that, so in a future episode we'll talk about refactoring this so it's not such a gnarly little text box, but until then, we'll just have this like other create conversation, and so we'll submit this, and the same thing will happen here. We can look up the recipient by the same method that we did with the messages controller, so we can have a find, and this will just be a regular old
User.find(params[:user_id]), because these aren't scoped to a conversation or anything because we're not using a form_for, and so we can get that, and then we can have
def create recipient = User.find(params[:user_id]) receipt = current_user.send_message(recipient, params[:body], params[:subject]) redirect_to conversation_path(receipt.conversation) end
If we wired all that up correctly, clicking "Save Changes" redirects us to conversations number two, and going back to conversations index for that person, we can see that that conversation is there as well. This is all that you actually have to do to get the bare minimum of messaging working with mailboxer. I kind of wish they would give you this stuff, the views and the controllers are provided generator for that. Maybe they do and I haven't noticed it, but that is all you really need to do to set this up. You're just passing around data into the send_message directly, it would be nice if they had a way of a helper class that you could instantiate and save or whatever that would allow you to go do that, but this works pretty well and allows you to get conversations in a basic level going within about 20 minutes. I'm pretty impressed with that, there's a whole lot of other functionality that it provides like read receipts and email notifications, and all of those things that we'll talk about in a future episode but for basic messaging between any type of users in your system, it's about 20 minutes, so that's not bad at all, and if you were to roll this yourself, you can do the same thing in about 20 minutes. The trick is that mailboxer will give you all those read receipts and email notifications, a lot of those things. Unless you really need to customize it or make it super efficient for your applications, mailboxer is a good choice if you don't need to do all of that. Until the next episode where we dive into advanced mailboxer, I hope you enjoyed this. Leave a note in the comments if there's any specifics that you would like to see around messaging and notifications and whatever around messaging, and yeah. I hope you enjoyed this, and I will talk to you next week. Peace
Thanks for another great episode Chris! I have two questions for you.
Is there a way to make Mailboxer work with ActionCable?
And another question is, what key mapping are you using to escape INSERT mode in vim? I'm mapping "jk" and "kj" to "<esc>" key and I was wondering if there's any other good key mappings for that.
I'm looking forward to the next episode, thanks Chris!
Yep of course! The same reason that you have to build your own controllers and views with mailboxer is the same reason why it will work perfectly with ActionCable. It really just handles the database side of things, which ties in perfectly with ActionCable. You'll still need to build the channels and everything, but it should work without a problem. Going to be covering this in a future episode as well.
I actually just continue hitting escape. It's a bit easier to reach on the Mac keyboard, so I haven't remapped it. I know a lot of people will play around with replacing caps lock for things like that or the leader key.
As I understand it, Mailboxer uses Carrierwave for processing attachments.
Does it make it hard to use Refile instead?
I don't entirely know how the attachments feature in Mailboxer works so I can only give you a rough idea of the direction to go. I think you could probably override the Mailboxer::Conversation model in your app and then add Refile into it if you didn't want to use Carrierwave. That should let you do what you want there.
Nice episode Chris , worthy mentioning mailboxer doesnt work with sqlite3 you have to use postgres or mysql
Interesting topic and certainly well presented.
On the mailboxer github page there is a command similar to devise
>rails g mailboxer:views
Maybe that saves one having to do it from scratch
In my show view for a song: when I click 'Message Me' I would like to generate a new conversation and grab that user's id (the one who created that song) so it sends to that user (Instead of choosing from a list of users). Any help would be great, thanks!
Just pass in the user_id in the URL on the link, and then set it as a hidden field inside the form rather than a select tag. That should do the trick!
Another fantastic tutorial.
a) If we install mailboxer and down the line want to give users the option to add multiple receipients, i.e. a 'private group chat', does mailboxer allow this? If so, this looks like a great option. *UPDATE: I see that video 3 discusses this. *
b) I installed notifications using your other tutorial. Should I use that same line of code here to create notifications in our bell, or is it best to use the mailboxer notifications? Or is it a combination of both?
c) I saw one person ask about how a user can trash/delete messages, but it doesn't appear you've yet responded. Is that covered as well?
Thanks ever so much.
I posted these questions in Slack, but also posted them here, because I'm sure the answer will help someone else:
I am working on the in-app messaging, and I have two issues:
u = User.alland then
u.conversations.destroy_all, it says it can't find that model.
Thank you for your help!
Chris just answered my question in the Slack group. Here are the two answers:
1) @blatz was right about calling the association on a relation, User.all wouldn't be an individual record with associations, you could only call scopes on it. if you did User.first.conversations you could destroy_all. Or you can just use Mailboxer::Conversation.destroy_all.
Mailboxer models are namespaced, you can see them in the gem. It's one of those gems that referencing the source code comes in handy pretty often
2) The extraneous prinout is because you have an equals in your loop <%= when you don't want it. It should be <%
And... both answers were spot on (as if Chris could provide anything less).
Thank you to Blatz too for your help. Onward!