In the last episode, we actually built the foundation for our chat application, built all the database models, we installed devise, we used devise-bootstrapped and that gem in order to add user names to log in with, but the next piece we need is actually to build the layout of our page, we want the sidebar to show the channels that you have joined, and then we want the main content of the page to be all the messages in that active channel, so we're going to be building the UI for that just on a basic level, and then adding in the ability for you to join and leave channel, so if you join a channel, it should show up in the sidebar, if you leave a channel it should remove it from the sidebar and that will give it a field like IRC or Slack or any of those chat applications like that. So without further ado, let's get started.
If we hop back into application.html.erb, we can add a container around the main content of the pager
<div class="fluid-container"> <div class="row"> <div class="col-sm-2"></div> <div class="col-sm-10"> <%= yield %> </div> </div> </div>
Once we have all these divs in here we should see things move around a little bit, and what we'll do is put the chatroom stuff on the left side once the user is signed in, we'll display that stuff there, so here we can just simply say
<% if user_signed_in? %> <h5>Chatrooms</h5>
This will be all the chatrooms that the user is connected to, so we'll have the chatrooms here in the middle, let's remove rm app/assets/stylesheets/scaffold.css because that conflicts with bootstrap a little bit. So we'll have these in, and we'll have our chatrooms here but let's also display them on the left side, we're going to want to display
<% current_user.chatrooms.each do |chatroom| %> <li><%= link_to chatroom.name, "#" %></li> <% end %>
Now we need to build the link to actually go join this, so the link is really just going to create the association between the user and the chatroom that join table record, and we'll be off to the races, so what I'm going to do here is I'm going to go to the routes file.
resources :chatrooms do resource :chatroom_users end
This is actually the chatroom_users join table model which we'll be interacting with directly, and the reason for that is because this is going to be the easiest way to grab those by id, so we'll be able to associate those by id instead of having to look them up every time, but this should be pretty easy, so we can go into our
class ChatroomUsersController < ApplicationController before_action :authenticate_user! before_action :set_chatroom def create @chatroom_user = @chatroom.chatroom_users.where(user_id: current_user.id).first_or_create redirect_to @chatroom end def destroy @chatroom_user = @chatroom.chatroom_users.where(user_id: current_user.id).destroy_all redirect_to chatrooms_path end private def set_chatroom @chatroom = Chatroom.find(params[:chatroom_id]) end end
If for some reason there happen to be multiple in there we'll just destroy all of them just for the safety of that, so there should never be any more than one record, and if there ever was, we just destroy all of them. If there were two records and you left and it deleted one but not the other, then it would look like you left but you're still in the channel and you'd be confused as a user, so this is kind of a little safety net of that situation in case you ever had that. Of course you can also set your validations on the join table and unique index in the database table across those two columns so you could never insert those records as well. How you want to go about that, you could either go this route, you can do kind of a mixture, you can approach that in a couple ways, just for the safety and quickness of this example, we're going to do it this way, but I would recommend the database indexes for unique across two columns for the safety of your data anyways, so we'll go to the chatroom's index, and this just really needs <%= link_to "Join", chatroom_chatroom_users_path(chatroom), method: :post %> the reason for that is we want to hit the create action, and so if you click join here, and we might also need to not use that instance variable. Now you should be able to see "Join" here, and you see that "General" shows up on the left side which is awesome, it very very good. It works exactly as we would expect. We could put an X on the chatrooms on the left side, or we could also replace this "Join" with a "Leave" function, and so I'm just going to make a "Leave" action here for simplicity sake, but we'll be able to make that link and move it around when we have icons, so we can put a pretty looking X on the left side or whatever so that it shows up visually a little bit more nicely, so we'll do that later but for now we're just going to put a "Leave" link, and the leave is really simple, and this POST is going to get changed to a DELETE, and so you should be able to leave General--
This accidentally crashed as you saw because my routes were configured incorrectly, so what we want here is resource as a singular route, because this is the chatroom user record just for yourself, so the CREATE and the DESTROY actions are not actually scoped to an individual record, and really we shouldn't care about, you should never be able to create and assign someone else to a channel for you, so the reason why we want to change this back to resource as a singular one is that his should control your connection to and from that chat room by itself, so think of this controller is specifically for the current user, and it really determines weather or not the user is joining or leaving that chat room, so it's nested, but it only really does joining and leaving, and that's really it, so this resource is a little bit different than what you might be normally used to, but it allows us for moving all that join and leave functionality into two actions in its own controller. It can be customized on it's own and it just kind of works independently of the chatrooms themselves, which I think is really nice, so if we refresh, we should now be able to join General, we can go back and we also should be able to leave General, we can click "Leave" on rando, but it's not going to do anything, we should be able to join both of these, and we can. We can leave random, we can leave General, and if we try to leave any of the chatrooms that we're not already in, it doesn't crash, and that's the benefit of the chatroom_users controller that we did. So the destroy_all will just destroy all but if there's no record found, it will just not crash, so it's kind of a nicety of this approach. If you didn't find anything it's going to crash, so this kind of eases over those exceptions or those errors that would come up, and you can take care of it just by modifying your ActiveRecord query a little bit differently. So if that's useful to you, that's a little tip that you might be able to use in other places, it's just a habit I've gotten into because it allows for kind of a seamless experience, if for some reason a user tries to leave the channel that they're not in already the app doesn't crash where it normally would have previously in different approaches of coding. Now where we're at is that we have bootstrap set up, we have our users, we have our channels, we have the ability to join and leave channels, but we don't have anything of the channels themselves, so if you go click on the channel, you don't get to see any of the messages, it doesn't really feel like a chat app yet, but we're making progress, so this is where I'm going to leave this episode off, and we'll dive into actually building the message functionality in the next episode. So this is it for now, and next piece we'll dive into setting up the messages, and then we'll start connecting ActionCable in order to make that stuff real time, so that's it for now, I will talk to you in the next one. Peace ✌️
Transcript written by Miguel
Is it possible to make a REST API for this app and create a Mobile app using Jquery mobile or Android and do real time chat with people chatting on desktop?
I have an application with Rails 5, Action Cable and Devise. I am planning to create mobile app to let mobile users do realtime chat with users on desktop or any other device. My website is device responsive but I am wondering if we can create a mobile app with REST API and do real time chat with Action Cable? Or is Action Cable only applicable to desktop users?
Basecamp uses Turbolinks and the ActionCable JS on mobile to power their app, however I'm sure you could port the client over to a native Swift or Obj-C client that would allow you to build a native app. It'll probably be a decent amount of work to start, but this is an example of someone who build an ActionCable client in Ruby (rather than the default JS) so you could probably use it as a basis for building a Swift client.
If you just go with Turbolinks views on mobile, you'll have a really easy time making this all work because the mobile UI will be the exact same code you use on the desktop browser.
Hi, When I try to leave the second (and last) chatroom I joined, I get this error instead of returning me to the chatroom path: https://uploads.disquscdn.c...
My routes look fine to me (when comparing to the source code), so i'm not sure why it isn't working.
@excid3:disqus awesome tutorial series!
You've said around 6:15 it would be good to add indices to the chatroom_users table. I just can't figure out of to add an uniq index for the chatroom and user references. Do you got a tip for that?
Hi Chris, can you explain again what the "before_action :set_chatroom" in the ChatroomUsers controller does? Does it just point all the join and leave methods to the specific chatroom id?
Because this controller is used to manage who is actually subscribed to a chatroom, we always include the chatroom ID in the URL. Then we just look up the chatroom and permanently add or remove the join table records to denote who is in the room or not. So yes, basically what you were saying. It's auto-scoping the actions its taking by the chatroom ID in the URL.
Gotcha. I think I've been clumsily adding that code as the first line to all my controller actions in previous projects. Didn't even think to just write it once and set it for all controller actions for simple things, like you did here. Thanks!
Hah! Yeah, it's a fantastic improvement to keep things consistent. One protip I'd recommend is actually studying how Rails generates scaffolds and you can cherry pick ideas from what that generates to use in your own code.
I have implemented this system in my own project. A problem I have is that I do not know when to destroy_all users (as you have done) when the user closes the website for example. How can I detect if they have left the site, and therefore their record in ChatroomUsers needs to be destroyed? I am using this also to show how many active users are in the chatroom, and the number does not go down when the user leaves by closing their browser because of this. Thanks.