Skip to main content
26 Realtime Group Chat With ActionCable:

Group Chat with ActionCable: Part 3

Episode 131 · August 2, 2016

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

ActionCable


Transcripts

What's up guys? This episode we're going to be taking where we left off and starting into the messaging aspects of the chatroom, so this chat is going to have multiple chatrooms, and we should be able to click on these and go to the chatroom, so the first thing I want to do is modify our sidebar, so instead of linking to an empty hash, we can link to the chatroom itself. Now the reason I originally did this was because I was going to have JavaScript handle that click, and we can definitely do that as well but it's kind of nice to have turbolinks handle that, I realized, and so if we refresh the page and we click on this and have turbolinks do the navigation for us, and actually that will keep the websocket open because that is already running on the previous page and because this is not a browser transition to a new page, the websocket will continue to be uninterrupted, which is super nice, so that means that we don't have to rerun all the websocket setup stuff, and you won't miss out on any messages which is a good thing. This is going to allow us to navigate to the chatroom itself, and we're going to need to modify that view so that we can see what's going on here. So really we probably want some sort of header thing at the top but that header thing probably should be somewhat fixed and this is where our CSS from bootstrap is kind of going to break down a little bit, we're going to need to actually build a fair amount of this stuff and kind of tweak the way that it works with bootstrap, so there's a lot of things that bootstrap is good at, but it's not really designed for building a really custom application like this where we probably want the main content being scrollable, a static sidebar, a footer that's static, that sort of thing. They support a marginal amount of this stuff, so we're going to dive in to a little bit of tweaks on right now, so let's actually go and say: What if we created this chatroom thing? Let's get rid of these links, and what if we created a div in here that had data-behavior of messages, and let's imagine this is where the messages will show up, so we'll have a navbar that will show the channel that you're currently in, and we'll have the messages div, and what if this div had like 1000 fake messages in it. So that's quite a lot, so we'll do this, and we'll just say the username and the message, so we'll just have some example in here, we'll see what this looks like and how the UI works.

First thing is first, now our chatroom sidebar is disappearing as soon as you start to scroll, which isn't ideal, and then the navbar itself is disappearing as well and the name of the channel is disappearing too, so we kind of want to position those three fixed on the page, so let's go do some tweaks to that now that we know that we need to do that. One thing that we could do is go into application.html.erb, and we can go up to our navbar here and add in which is a bootstrap class that will make the navbar on the top fixed of course, so that will position that so that it's always there and is always available. Of course that means that all of our content gets pushed up and the navbar sits on top of some of it, so you can see that you can't really see the top of the page anymore when you scroll to the very top, so this means that our fluid container needs to have

so that it pushes the main content down because the navbar itself is a certain number of pixels tall, it is 51 pixels tall, and then it has a 20 pixel margin or something, so what that's going to say is like: Well, it's about 71 pixels tall so if we put a margin on top of our content, then it kind of evens it out and we can see all of our stuff again, so the next thing that we want to do is say
and if we add this in there, we should see that while the main content shifts over, the chatroom stuff stays there as we scroll, which means that we just need to bump over the main content by the same amount of space which is two columns in bootstrap columns. These are all kind of hacks to the bootstrap CSS, which aren't ideal, and of course you can take the CSS that we did inline and pull them out into classes, but I would really encourage you to go the extra distance if you actually want to make this a real application because all the CSS is just really hacks on top of bootstrap to get it working well enough for a usable UI for now just for this example, so I'm not building out the full UI, and if I did, I would probably not use bootstrap for a lot of this layout stuff, I'd build it all custom because we're wanting to collapse all this in different ways, and chances are you would want these chatrooms to be like a button in the navigation if you're on mobile, so while you have this menu thing on the right side, you'd probably want the chatrooms to go into a menu on the left side and all of that is just kind of extra stuff that we're not worried about in this series, but if you are you can go take a look at a lot of the CSS tutorials online, they'll show you how to do that and build this out responsively whereas we're definitely not doing a good job building this responsively, but it's not too hard if you go look up some tutorials on that. So skipping on, we want to be able to now add a form at the bottom here for creating a new message in our chatroom so when we go to it, of course the only way that we can interact with the chatroom is to add a message, so what we'll want to do is go into our chatroom

app/views/chatrooms/show.html.erb

<%= form_for [@chatroom, Message.new] do |f| %>
  <%= f.text_area :body, rows: 1, class: "form-control", autofocus: true %>
<% end %>

We also need to add

routes.rb

resources :messages 

We'll refresh the page and now at the bottom we should have a text area that only has one like of text which is good. We could also add autofocus= true to it, so that when you load the page it automatically scrolls you down to the bottom and focuses that form field, so once you load that page you can just immediately start typing, which is super cool. We're making some progress, but as you might have noticed, the autofocus doesn't actually scroll you down if you switch chatrooms. Maybe this is working, maybe it's not, you might also need to add in some JavaScript in order to do the scroll to the bottom if you don't add the autofocus in or if it's not working appropriately, because you have to remember, we're using turbolinks for the navigation, which doesn't necessarily mean all of those things that the browser would do on a brand new page will work exactly the same way, so that's one of those gotchas that you might run into, with little things here and there because turbolinks is faking the new page and your browser is not actually doing everything that it normally would, so you might get little differences there, but it looks like it's working so that's cool.

We want it to be able to say "Hello" and we want to be able to capture the Enter on that form and automatically submit that message, so we'll need some JavaScript to intercept the Enter key when you hit that, and so let's go and do that in our JavaScript. So this is really simple, basically all you need to do is say app/assets/javascripts/chatrooms.coffee and inside this file we're simply going to say:

$(document).on "turbolinks:load", ->
  $("#new_message").on "keypress", (e) ->
    if e && e.keyCode == 13
      e.preventDefault()
      $(this).submit()

This did submit it, now we have to build out our messages controller in order to create that message

app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_chatroom

  def create
    message = @chatroom.messages.new(message_params)
    message.user = current_user

    message.save
    redirect_to @chatroom
   end

  private

    def set_chatroom
      @chatroom = Chatroom.find(params[:chatroom_id])
    end

    def message_params
      params.require(:message).permit(:body)
    end
end

Let's just make sure that this works and redirects us back to the chatroom, now we don't have it being displayed there, so if we go to chatrooms show, we can remove this fake loop that we had, and instead we can say

app/views/chatrooms/show.html.erb

<div data-behavior='messagees'>
    <% @chatroom.messagees.limit(100).each do |messages| %>
    <div><strong><%= message.user.username %></strong> <%= message.body %></div>
    <% end %>
</div>

That should take us now back to the Gorails users, and if we log in to localhost:3000 with a different user and we join the general channel, we can go to that channel and we should be able to see that message here but we did not. Let's go back to the he page, let's go to random, and we see the message there, and I should be able to type in "Hi", and I don't have a username so let's go set that, because I've hadn't updated that user from when I created it before we added usernames, so now if we go in there, you can see that user said "Hi" but it hasn't updated in real time because we need to add ActionCable for that but if we refresh the page we do ge that message, so we have a basic chat going on right now, and you can jump between the channels, and the other user should be able to navigate to it and see it, and all that should be working, and the messages are also in order by default because the normal query pattern is that you have the id numbers in your database and you start from one and go up to whatever number you have and it will always query those in order where it should, but you're going to want to make sure that you set the order on your messages, and really you probably want to say created_at is the timestamp that you want to order by, so you want them to be descending, so you get the most recent ones, but you're going to want to reverse them, so what will happen is you want to grab the most recent one hundred, and then that will be the most recent messages at the top, but when you insert them onto the page you actually want to insert them in reverse order, so you wan the last one to go first and then the next one to go under that, so you kind of want to flip this array around and then print it out in the html on the page, so you have to query with the newest items first and grab those hundred, otherwise you won't get the right ones, and then you have to reverse that array to get the right ones to print it out on the page correctly, so the way we're going to do that is we're going to add the reverse method here, and this will take the results that we got from ActiveRecord, and then reverse them before we do each, so if you see in your query, you should still get these SELECT messages with the order created_at as descending, and that shouldn't affect the results that you get, so this should display now.

If you spell it right, when you see the messages, you should get the most recent at the very bottom, and if we were to go to the same chatroom on both and we went to limit that number to one, we should see that the most recent message is the one that says "recent", so that way you can know that you got it right by the filtering and the reverse by seeing the most recent when you limit down to one. If you didn't have the order(created_at.desc) here and you removed it, and you refreshed the random channel, you would see that the gorails message that was first actually shows up as if it were the last one, and that's not correct, so you have to make sure you have your ordering down, you have your limiting down to the correct number that you want, and then you reverse it so it shows up on the page appropriately. We have all those pieces in place, you can move this out into a helper method inside your model if you want to say: recent_messages instead of adding the order and the limits here. I'd recommend doing that because if you don't, you're going to have to make sure you always have that order and the limit in as the appropriate and you might forget to do that sometimes, or you could also set this as messages inside of your controller, and you could use something like chatrooms controller, and when you do your show, you have @messages, and you do this instead. This could be one way of doing that and it really doesn't need to go in your model as a helper method, you could absolutely do it here. The thing is if you end up doing this a few more places it's probably better to pull it out into its own method on the chatroom model so that you don't have to duplicate this code all around your application. Now we only reference this one time here, so I'm going to use this instance variable, and we'll have @messages here and inside the controller, and that will take care of any sort of the complexity that would be putting in the view that it doesn't really belong in the views. So with that said, the next piece of course is that we want to be able to type into these and have them update in real time, so we want to submit this form with JavaScript so it doesn't navigate away, we can keep our websockets live and running, and then when a message comes across, we want to be able to see it injected into the page in real-time using ActionCable, so the next step for us is to dive into all of this and the JavaScript required to make that happen. So in the next episode we'll do all that, and I will talk to you then. Peace ✌️

Transcript written by Miguel

Discussion


Fallback

Chris,

Would it be possible to have one on Map APIs like mapbox or google maps api please :)

Omar


Fallback

Chris,

(1) With regards to APIs, I would like to learn more about authenticating users across multiple apps. Let's say you have a core Rails API app that is consumed by several other Rails or client-side applications. Figuring out "who" is doing what always seemed to be the tricky part for me. Just passing a token in the URL or POSTed in the body never seemed very secure to me.

(2) One feature I would especially like to see is how to include associations in an API via the URL.

For instance, let's say you have Users, Posts and Comments, and you want to retrieve a single Post but you also want the User (author) and the related Comments in a single API call instead of having to make 3 calls.

In Rails I would do: Post.includes(:user, :comments).find(1)

In an API I would "like" to do: api/v1/posts/1?includes=[user,comments]

I had to build this kind of functionality into an API I worked on recently because the consumers of the API needed that flexibility (especially if you have 3+ consumers of the API, you can't make custom endpoints for all of them so have to give the consumer some control). But the code I had to write to make it work felt very clunky and difficult to manage.


Fallback

Chris, can you also show how to upload images inside the channel in real-time using ActionCable?

Fallback

I'm not sure I would use ActionCable for uploading files. I would probably just send them like normal using AJAX.


Fallback

Hi Chris,

At 10:54 you write in the .coffee file $(#new_message), but in how I'm implementing this my form looks like <%= form_for :message, url: group_chat_path %> so I don't think the js can grab an id of #new_message. Do you have any suggestions for how I should update this piece of coffee code? Otherwise I'll just to try rewrite my forms successfully.

Fallback

You can add your own ID or class to any element you have in your form and change that query to match. I just use that because it's auto-generated and easy to use, so you can use the autogenerated one for your form, or specify your own and that should do the trick. 👍

Fallback

Cool! Another question on this. I'm just doing the console log part and I'm getting error:

Uncaught ReferenceError: e is not defined

Do you know why that'd be? I have this in my coffee file.

$(document).on "turbolinks:load", ->
$("#new_message").on "keypress", (e) ->
console.log e.keyCode

Fallback

Hmm, not sure. It looks correct assuming each of those lines is tabbed over properly

$(document).on "turbolinks:load", ->
$("#new_message").on "keypress", (e) ->
console.log e.keyCode
Fallback

Note to self. Tabs matter. Thanks! Is it a js thing or coffee thing?

Fallback

Just coffeescript because they removed curly braces. They use tabbing to denote which block it is a part of instead (like Python).

Fallback

Much appreciated!


Fallback

This is definitely a noob mistake, but if you're not following along exactly with the app Chris is building, make sure you include the actioncable related JS files in application.js so that things actually load. That took longer than it should have to figure out...


Fallback
Ishita Singh Bhadoria

@excid3:disqus Hi , the enter thing is not working i am even not able to get the values of character in the console.


Fallback
Ishita Singh Bhadoria

@excid3:disqus i am getting the following error when trying to get the enter thing working :
Uncaught ReferenceError: $ is not defined at line 2 and line 10 of :
(function() {
$(document).on("turbolinks:load", function() {
return $("#new_message").on("keypress", function(e) {
if (e && e.keyCode === 13) {
return e.preventDefault();
}
});
});

}).call(this);

Fallback

@ishitasinghbhadoria:disqus
Your code is not coffee script.
1. do you have jquery-rails gemin your project?
2. Do you loaded jquery in application.js?

I also use jquery and it work for me. I have uploaded the screenshot.

https://uploads.disquscdn.c...


Fallback

@excid3:disqus
I think we don't need (created_at) and reverse
https://github.com/gorails-...

@chatroom.messages.limit(100).each
it will work the same as default.


Fallback

This may help some of you.

If you find that this code doesn't work:

$(document).on "turbolinks:load", ->
  $("#new_message").on "keypress", (e) ->
    if e && e.keyCode == 13
      e.preventDefault()
      $(this).submit()

Try changing the last line to reflect an ID, like this:

$("#new_chat_message").submit()

For some reason, using (this) didn't seem to work for me, but using the ID worked great.


Login or create an account to join the conversation.