Skip to main content

58 Group Chat with ActionCable: Part 1

Episode 129 · July 19, 2016

In this series, we're building a clone of Slack using Rails 5 and ActionCable

ActionCable


Transcripts

What's up guys? This episode is the beginning of a series where we're going to be building a chat application using rails 5 and ActionCable. What we're going to try to do is build something a little bit more robust, like Slack for example. We're going to try to build an application where you log in and you see all the channels on the sidebar and those in real time update when there's a new message and all of that stuff. This is going to be a lot more complex than the DHH posts example that we saw with the ActionCable example chatroom that he built before, and it's also going to be more complicated than the notifications thing that I did which is just a tiny piece of your website. This is going to be the core-focus of the application, so we'll have the sidebar with chat, it will get updated in real time. We'll also keep track of new messages and show you where you last read that channel at, and all that stuff. So it's going to be interesting and there's going to be a lot to it, but we can break that up into a handful of episodes, and we'll go tackle all of those and we'll go build an actual chat application so you can get the idea of what it takes to build this bigger applications with ActionCable.

Without further ado, this episode we're just going to dive into building out all the foundational stuff at a high level, so we're going to talk about building the users, the channels, the connections between users and channels so next time you log in we remember where you were at. We'll talk a little bit about recording when you last read, we'll do maybe the basic set up for bootstrap and stuff like that, but we won't get into ActionCable until next episode. If all this is new to you, then keep watching. Otherwise you can skip forward to the next episode if you want to dive into the deep end. Without further ado, let's keep going. First thing is first, let's make sure we're using rails 5. This is officially been released now so we can dive into that and we have the latest version installed so we can go and create our chat application so we'll say rails new chat, and once that's installed, let's cd into chat, let's open up our gemfile, install a couple gems.

Gemfile

gem 'bootstrap-sass'
gem 'devise'

bundle install

Then we'll go and generate our models, so let's think about the models that we want to use for this. Number one we'll have a user model. Number two, we're going to have a chat room, we'll probably just call them chat room or room, or whichever you want to call it, and while we're on that topic, you might consider not naming your model "channel", which is kind of a common term to describe a chat room, especially if you're in IRC or something like that, those channels are now also being used as a term for ActionCable channels, so these communication channels over the websockets is now a word that ActionCable is using which means that you might have a little bit of a confusing thing when you glance at the code and see that there's a channel model but there's also a channel in ActionCable. As long as those two don't conflict, you can use the word channel for it in your database models, but it's a little confusing if you're using the same term to describe two different things. Now the ActionCable thing I believe is namespaced, so you would be fine with that but just something to keep in mind as you go forward, so let's generate rails g devise:install and let's just get back into building this, so let's generate users, but let's have a username for the users so we can show that next to the chat messages instead of an email address and let's talk about generating that chat room model. We need to generate the chat room model, I'm going to generate a scaffold for the sake of doing that, so the scaffold will be a lot easier for us to automatically have all those views, we can go edit all of them, but we will have something to see got to play with out of the box, so we'll use a scaffold for: Let's call them chat room, and each chat room is going to have a name and that's probably it. Each chat room is going to have many messages, and it will also have many users through a join table, so we'll have that and that's going to define the connection between when you sign in, you join a bunch of channels, when you sign out you come back, you'll be able to see those same channels that you joined, and then that way all these channels don't have to always be connected to the user, or you don't have to choose one every single time, you'll automatically go back to the channels that you were already previously interested in. I'm just going to run the scaffold for chatroom, and if we decide we need to change it later we can change it, so we'll just modify that as necessary but for now this is good let's also generate that model in between the chat room and the user's, so this is probably just going to be really simple, and we'll say: rails g model ChatroomUser chatroom:references user:references That is the join table we'll create the resources controller and routes in order for you to click on a channel and join it, that will of course need to be a feature so that we can save that permanently, but that's really as simple as those are. One feature that we're going to delay until talking about in a future episode is the idea of marking the channels and the user's last read_at time stamp. This is nice because if you are reading a chat that has many users in it there's a lot of conversation happening if you see a line in there that says: You saw everything up until here but then you logged out, and everything below it is new, that's a super useful feature, and we can probably add something in here into these models so that we could say: Well, this user last read this message, we could update that as we go, and we can put that in here, and then the UI front-end could determine that as well. This is important that it needs to be server-side because that should propagate to all your devices for example, so we don't want it to be stored client-side in Javascript in like a cookie or something, because then that way we don't actually get to persist that and if you sign on on your iPhone, you wouldn't actually know what you last read at. So it needs to be something that's done server side, but we'll talk more about this in depth in a future episode, and we'll also merge that in as a feature all on it's own and so if we need to make adjustments to this model, we can go do it and then later on, and this feature can be separate, because really we're trying to set up the bare bones structure for everything right now. We do need messages, because what is a chat room without messages? We have users, but those messages are kind of the key crucial piece, so let's generate a model rails g model message chatroom:references user:references body:text We'll display the username, we'll display the text and probably somewhere on the side we'll show where the time was when it was posted, so that we can show up on the side but can use the created_at method for that. It's already baked in all of your models, so let's run rake db:migrate and get all of those in our database. What I like to do is after we create our models, let's make sure we have all the associations set up, and most of those we're going to be the has_manys because the references will actually insert belongs_to for you, which is pretty nifty, so let's go into our models folder and take a look at what we've got.

chatroom.rb

class Chatroom < ApplicationRecord 
    has_many :chatroom_users
    has_many :users, through: :chatroom_users
    has_many :messages 
end 

chatroom_users.rb

class ChatroomUser < ApplicationRecord 
    belongs_to :chatroom 
    belongs_to :user
end 

message.rb

class Message < ApplicationRecord

belongs_to :chatroom 
belongs_to :user 

end

**user.rb** 
class User < ApplicationRecord 
    devise 

    has_many :chatroom_users 
    has_many :chatrooms, through: :chatroom_users 
    has_many :messages
end

We'll also need to set up a little bit of the UI, so I'm going to paste in a command that basically, this is from the bootstrap-sass README if you're reading that, when you set this up. application.css needs to be moved to application.scss because we want to use SCSS in order to parse this file. This doesn't affect any of the asset compilation from the manifest because you know the comment at the top is the manifest stuff. All this is allowing us to do is say we can import bootstrap here at the top and we can also import bootstrap sprockets, and so this is just going to say: Well, if we make this file SCSS, we can also use this file to import bootstrap and use it as a main place where all of our CSS gets included. The reason why we need it as SCSS and we're using import instead of require is that this import allows you to define things like $brand-primary up here and you can set it to "red" or whatever, and this would go to apply to all of bootstrap, so it has variables in your SCSS that can get applied and so you could customize bootstrap this way, whereas if you did a require, there's no way for you to pass in SCSS variables into bootstrap in order to customize it, so we need to do that in order to make all of that work, and then I'm going to paste in some code for the application template, so that we can have two columns basically. First, let's open up the application.html.erb and let's paste in the navbar that we want to use, this is just the regular navbar that I've stolen from another site of mine, and this is just some bootstrap html for a navbar that goes at the top of the page and we'll just have this link to chat as root path, and I've already included some HTML for the navigation, so you can click on your username and edit your account or log out, and links to sign up and log in will replace that if you're not signed in. This should work, and let's run our rails server just to check it out, and because we have the chatroom scaffold we can use that as the route, so we can say root to: "chatrooms#index". Now we can run rails s and open up our localhost:3000. Once this is booted there we go, we have our navigation, we can click "Sign up", we can sign up, and we need to add in the username field in the signup, so we'll do that, just in a second, but right now you can click the dropdown Javascript so let's do that as well. We need //= require bootstrap-sprockets in application.js, let's do that after turbolinks, and paste that in, and so now if we refresh our chat app, we should see: Settings, we can edit our user and we can log out and everything should work just as expected. Next step let's add that username field into registration so that we can keep track of people's usernames.

Editing those views with devise is really simple but because we're using bootstrap it's also something we have to go customize to fit with the bootstrap html for the forms and stuff, so actually this past weekend, one of the Gorails community members, Andrew Fomera was working with me and we we're kind of poking at the idea of what if you could just generate the views for devise that were already precompatible with bootstrap? Because both of us recognized that we would go do it once, and then go copy and paste it from another application. So he took advantage of that, and built a gem called devise-bootstrapped, which you can install in your gemfile, and this is super nice because you can say: Let's run bundle to install it, rails g devise:views:bootstraped, this will install all the views for devise but with tweaks for bootstrap so they look nice there, and then once you're done with that, you can actually remove it from your Gemfile so you don't have to keep it around, because chances are you're never going to run those generators ever again, so voila, you're done, and you have nice views, and you can see that if we now go edit settings, this is now automatically styled with some basic bootstrap stuff and even centered on the page as well, so this is pretty nifty and gives you a good starting ground for going and modifying your views. Now the view we're specifically looking for are the registration ones, and we're not going to modify the sessions to allow you to log in with either an email or username, but you can also do that, there's a link in the devise wiki for how to do log in with email or username and it's really simple, but you can go do that, but if you're really interested, let me know, we can do an episode on it, but the instructions in the wiki are really simple and of course are going to be up to date whereas the video might get outdated. So let's go edit this view, and so now we have this email field here in a form group. We can change this to a username in both cases, and let's also say that it's a text field, and our email should not be autofocused, so with that, we can take this and also put it in the edit so that you have the ability to modify both of those and there you go, so maybe you don't want the user to edit their username after the fact. That's up to you on how you want to control that. We're going to allow it just to keep it succinct. That's the view portion of this, now we're going to go edit the application controller to add the permitted parameters. The permitted parameters in devise have changed a bunch over the years, and the ways of doing that have kind of adjusted into this version four that's compatible with rails 5 has changed once again, and so it's always good just to be able to go look this up in the README and make sure that you're doing the right way. So for example, we're just going to grab this snippet here and paste this into our application controller, and so this is basically saying: Well if you're in a devise controller, then we'll run this method, and this is saying: Let's modify the devise permitted params, they have the to call it a sanitizer. For the sign up action let's also add in the username key, and we can do the same thing for account_update, and that way we'll have that available to us when we update our account. So this is basically just inserting that item into the parameters that are allowed and you don't have to specify email and password and password confirmation for any of this stuff because it's already kind of assumed that you're going to have to have that anyways. So with this installed, we should be able to go and create a new user once we log out of this one, with a username. Now you see "Gorails" at the top, because it did get the username and save it to the model so that is the only two things that you need to modify with devise if you want to add that in. We're getting pretty close to having all of our foundational stuff set up. The one thing left is really to make the chatroom sidebar, so we want to have the ability to see all the chatrooms on the side, and then the main content of the website will actually be a view of that chatroom that you're actively looking at.

Because we've already covered quite a lot in this episode, I'm going to split this into two parts, and the next part will be building out that layout, and then also adding the join and leave buttons, so you can join and leave those channels, and once we have that ready, we'll be ready to go in building out the message functionality in our app, so until then, I will talk to you in the next episode. Peace ✌️

Transcript written by Miguel

Discussion


Gravatar
Andrew Chirvase (10 XP) on

Great tutorial. Can't wait to see Part 2.


Gravatar
Serguei Cambour (50 XP) on

Thanks a lot, Chris, good job, looking forward to the Part II.


Gravatar
stacia on

Nice job. Look forward to #2.


Gravatar
Nick McNeany (6,930 XP) on

Awesome! Thanks Chris. Looking forward to the rest of the series.


Gravatar
Nick Vaughn (20 XP) on

Can you post the block of modified navbar code that you inserted into application.html.erb? I wasn't able to catch all of it on the screen to see the changes made.

Gravatar
Shawn Kearney (20 XP) on

You can find the code for the navbar here: https://github.com/excid3/f...

Gravatar
Nick Vaughn (20 XP) on

Thanks! I can't get the username to display in the navbar. The line where you define current_user.short_name throws an undefined error for the 'short_name'. I couldn't find that defined explicitly anywhere else in the program.

Gravatar
Shawn Kearney (20 XP) on

It should be current_user.username.

Gravatar
Andrew Fomera (4,860 XP) on

https://github.com/gorails-... You can find the code for the layouts from the episode there.

If you're wondering short_name is probably a method on the user.rb file in the model folder and it's probably setting short_name to whatever the users first name is. But like @ineptsoftware:disqus said, if you change it to current_user.username or current_user.email it should work :)


Gravatar
Kendall Weihe (10 XP) on

This was great Chris (edit sorry wrong name lol)! Thanks! I can't wait until you dive into the actual ActionCable. What is the ETA on the second episode?

Gravatar
Chris Oliver (169,610 XP) on

Just published it! https://gorails.com/episode...

I'll be publishing about one a week so I have time to get through everything.


Gravatar
Zulhilmi Zainudin (270 XP) on

Chris, don't forget to validate uniqueness of the username.

Gravatar
Chris Oliver (169,610 XP) on

Thanks! I'll try to make sure I include that.


Gravatar
Jay Cole (10 XP) on

Hey im new to rails and still getting quite comfortable with Ruby. But I seen that version 5.0 of Rails is out. How do I upgrade/install it? Thanks!!

Gravatar
Chris Oliver (169,610 XP) on

You can just run "gem install rails" and that should install Rails 5. I'll be doing a screencast on upgrading an existing app to Rails 5 pretty soon.

Gravatar
Ariff Munshi (10,770 XP) on

Hey has this up yet? @excid3:disqus

Gravatar
Chris Oliver (169,610 XP) on

Hey Ariff,

Haven't recorded that yet, it kinda slipped my mind. Let me get on that this week!

Gravatar
Ariff Munshi (10,770 XP) on

Thanks Chris!


Gravatar
Chris Oliver (169,610 XP) on

I'm using Monokai in just about all places, vim, zsh, etc.


Gravatar
SURESHKUMAR RAMAIAH (50 XP) on

Hi Chris,
How are you getting those icons like padlock in the text_field?

Gravatar
Chris Oliver (169,610 XP) on

Those are actually just the LastPass extension trying to save my passwords.

Gravatar
SURESHKUMAR RAMAIAH (50 XP) on

Oh thanks Chris. They make the forms look cool. Will check if it's possible to add them to forms.


Gravatar
Ferran Cabrer i Vilagut on

Thanks Chris, I was following building this awesome app, but at (Part-2 at 8:19) I got stuck by this error: Undefined method @chatroom_user = @chatroom.chatroom_users.where(user_id: current_user.id)
How can I fix it in order to continue the app?
Thanks in dvance


Gravatar
Fritz Rodriguez Jr. on

Hey Chris! I know I'm late to the party...haha! I found a way of adding custom fields with Devise that I like...

def configure_permitted_parameters
added_attrs = [:username, :email, :password, :password_confirmation, :remember_me]
devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
devise_parameter_sanitizer.permit :account_update, keys: added_attrs
end

This makes it super easier to add additional fields in the future. Anyway, just a quick tip! :)

Gravatar
Chris Oliver (169,610 XP) on

I like the added_attrs array. It's nice to reuse since I had always just defined the items in each place.


Gravatar
Mathias Spurr (50 XP) on

Hello Chris,
thank you for the tutorials. I was wondering if you could make a tutorial for setting up MacVim just like you have? I have tried myself, however, I can not seem to figure out how to do it.

Thanks,

Gravatar
Chris Oliver (169,610 XP) on

Absolutely. Check this out: https://gorails.com/episode...

Gravatar
Mathias Spurr (50 XP) on

Oh thank you! I just found the video on youtube in the same moment


Gravatar
Daneil Cardenas (1,650 XP) on

That devise-bootstrapped gem is a time saver ;)

Gravatar
Chris Oliver (169,610 XP) on

Credit for that goes to @andrew_fomera:disqus. :D

Gravatar
Andrew Fomera (4,860 XP) on

Thanks for the kind words! If you have any feedback for improvements feel free to hit up the github issues!


Gravatar
loveisnuclear on

Hi, this is a great tutorial so far! However, i'm having some issues near the end. After I add the Devise permitted parameters to the application controller and I try to sign up, I can no longer access the site (even after refreshing and trying to go back to the main page. It gives me a NoMethodError in Chatrooms#index. It says "undefined method `public_channels' for #<activerecord::associations::collectionproxy []="">
Did you mean? public_send" and that the error is from application.html.erb. I replaced my code in that file with yours from the github repo, but i'm still getting the same error. Can someone please point me in the right direction? Thanks!
https://uploads.disquscdn.c...

Gravatar
Chris Oliver (169,610 XP) on

Hey! I believe that should be just

><% current_user.chatrooms.each do |chatroom| %>

because we didn't build anything called "public_channels" I don't believe.

You can find that line in the github repo here: https://github.com/gorails-...


Gravatar
Evelyn on

Hello @excid3:disqus
First of all wonderful tutorial. I have a question, please see if you can help out,

I have a question model and a conversation model, a student asks a question and based on that question a teacher chats with that student. Please note only one teacher and one student are allowed in conversation per question. A student can only ask one question per email address. Now we wanna introduce a live chat support system for our support team to be able to chat with students. Now this chat room should be unique to each question and any number of support users can be present but only one student. How should I go about creating models for chat room based on your tutorial?


Login or create an account to join the conversation.