Skip to main content
Realtime Group Chat With ActionCable:

Group Chat with ActionCable: Part 4

23

Episode 132 · August 9, 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 picking up where we left off last episode in the chatroom stuff where we added messages into the application, so you can submit a message in this form, it will go and make a post request, it refreshes the page, you get to see the message, and then the other people in that same chatroom get to see the message but they have to refresh the page in order to get it. So you're not receiving real-time updates, and you're also not posting over JavaScript, so there's a lot of stuff that we need to do, but this episode we'll be diving into all of that in the ActionCable stuff we need to make all of this work nicely. I'm going to dive right in but the first thing that we actually need to do is make the form that we submit as Ajax, so we want to make sure that this submits a JavaScript request when we type in a new message, and this is actually going to work seamlessly with the JavaScript that comes from rails_ujs. So this "remote is true" comes from that, and the code that we wrote to listen to the Enter key will actually submit it and the Ajax request that rails_ujs does will automatically handle all of that. This will work in sync, that means that if we refresh this page now, we should still be able to type a test message in here, it will show up on the page and get submitted so it's still working, but if we look in our rails logs, we can see now that this is submitted as a JavaScript request. Now we didn't actually write any JavaScript as a response, and the reason for that is that is because the turbolinks 5 rails adapter that the rubygem ships with basically overrides the redirect_to method, so if you aren't familiar with that, all that's really doing is saying: If that comes across as a turbolinks JavaScript request, and you submit test there, your post request is going to return turbolinks JavaScript to go visit that new url to redirect instead of actually telling the browser to do a real redirect, it's faking it and then using turbolinks to complete that. This works nicely, except for the fact that we don't actually want to make a redirect at all, we want to actually just submit this to the server and have websockets injected on the page instead of doing a refresh in the browser, even with turbolinks, so we want to get around that and that means that we need to go replace the way that that works, so we'll open up our messages controller and remove the redirect chatroom, we don't wan that anymore, and that's going to allow us to write a create.js.erb file so at least we have some sort of response to send that to the browser, so let's go make a directory called app/views/messages/create.js.erb. If we simply save this file and leave it empty, when you make your test message now, nothing is going to happen, and you see that the test text is still in there, and that seems weird, right? Well the reason for that is because you use JavaScript to submit it over, we didn't clear out the form or any of that stuff because you've got no response. You've got an empty JavaScript response which means we're done. nothing to do here. So it looks weird as to what would happen, but if we call

create.js.erb

$("#new_message")[0].reset()

Let's refresh our page and type "test" now, and it works, but we don't see the new message in there, which is good, we don't want to because we haven't built the ActionCable stuff yet, and if we refresh this page we'll see that it showed up, so it got saved to the database, our JavaScript returned and ran, which cleared out the form, and a refresh of the page shows our message, so we're getting closer, and now we have to go build out the ActionCable channel for all of these messages to come across from. We should start by generating a channel rails g channel Chatrooms this will be the one that we'll connect to, and will connect all of the chatrooms the user is actively connected to, so that's going to connect to "General", "random", and if we add more in there, it will automatically connect to those when we restart the websocket connection, so to do that, we need to actually go build out the server side piece of the channel. So first is we need to do the whole identified

app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags "ActionCable", "User #{current_user.id}"
    end

    protected

      def find_verified_user
        if current_user = env['warden'].user
          current_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

The cool part about this is that all of your logs for ActionCable stuff will show up with those at the beginning, so you can actually pull out the user, kind of see what's going on if you ever need to track down what's going on there. The reason for that is that everything is getting a lot more complicated now because you don't have your rails logs always in line in sync because they're kind of not happening at the same time, you have a lot more going on all at once and this can be helpful for you to see what's happening. So this is good, and we should be able to refresh our page and our JavaScript subscription to the channel should initiate that, and we should be able to go to the terminal, and see that ActionCable user number one and user number two has been connected. This is cool, it's actually working and making connections to the ActionCable server, and it's tagging those as ActionCable and user along with the id number which is exactly what we want. So the next piece is to actually make sure that the server-side connection sets up to string from the appropriate channels.

Here's where things can be a little more complicated. Normally when you do this, you set up to stream from one channel most of the times, but because we want our browser to be able to collect messages from any of the channels that are active, not just the current one that you're looking at, we want to

chatrooms_channel.rb

class ChatroomsChannel < ApplicationCable::Channel
  def subscribed
    current_user.chatrooms.each do |chatroom|
      stream_from "chatrooms:#{chatroom.id}"
    end
  end

  def unsubscribed
    stop_all_streams
  end

Let's hop back into the browser, refresh the page, and then take a look at our rails logs and see what we've got. User number 2 joined the websocket connection, and you can see that they started streaming from chatroom number one and number two, which are the two chatrooms that we have set up, so if I were to go to the homepage and leave "Random" and we refresh this page. This is actually going to show us that now we're only streaming from chatroom number one. This is cool, it's adding in that ability for us to go and set up those chatrooms. Now in the future we're going to have to manage this when you leave or join so we can tell the server side stuff to either not send us anymore messages from that channel, or that chatroom, or to add in that and also stream from that channel. Now that's going to be a little bit more in the future, but for now we have the basic functionality set up and this is working just how we would like it. With that said, we're able to do all of this, but now we're going to start receiving messages and sending messages over the websockets. So let's do that now.

The most common thing that you're going to see is that you will have is a job to relay those messages over the websocket connection, and the reason for that is that when you send a message across to redis and everything, the connection time will be somewhat slow potentially, and so if you set up a background job and you just throw it on the queue, that's fast and doesn't have to talk to your redis server or any of that stuff. So you can toss these jobs on the queue and it can process them as soon as it can,so this way you're a little bit safer by throwing these things on the queue like your sidekiq queue or whatever, and then that's going to process them and send those messages out as fast as it can. So if things stack up or get delayed, you'll still be able to see them come over, they just won't happen instantaneously. This is a nice way for us to build up that buffer just in case we get overrun with things, and so you'll see that a lot of cases, and so for the most part, you're just going to say: Let's make a message relay job that we performed later on, and we pass in the message, and that's it. We'll send this over to the queue and we'll build this job and that job will send it over ActionCable to all of the browsers that are connected. Let's go generate that job so we can simply say rail g job MessageRelay and that will generate it for us, and then we can go into the jobs folder and in here we can accept the message, and really all this has to do is say:

app/jobs/message_relay_job.rb

class MessageRelayJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast "chatrooms:#{message.chatroom.id}", {
      message: MessagesController.render(message),
      chatroom_id: message.chatroom.id
    }
  end
end

That is that for this piece. Now we need to go and make that message template but we already sort of have that, so if you go to chatrooms/show.html.erb, we loop through the messages here and render this div for every message, I'm going to pull this out and we're just going to say: <%= render message %> here, and it will effectively do the same thing as what we're doing in that message relay job, that means that if we save this, and we edit app/views/messages/message.html.erb and paste this in. This will automatically get called from those views as well as ActionCable and the background worker and so this will be a way for us to reuse that html server-side when you load the page like so and when it comes across through ActionCable. Before we get too fai into this, I always forget to change the adapter for ActionCable to redis so that it can communicate between processes and everything and this is also good because in production probably going to use redis, and you probably want to make sure you use the same environment in development as you do in production, so you don't run into any weird bugs in that transition. If you do this, you're also going to have to add gem 'redis' to your Gemfile, close your server and run bundle and then restart your rails server, so that's going to make sure that you can have two browsers talking to each other, that sort of thing and this is going to allow us to finally focus on our JavaScript. Now I've just added one line to console.log the data out for this chatrooms channel and that's going to allow us to see that message that we get across, which comes from that message relay job, so it's really just going to give us this hash of data that we passed as a second parameter. One of the unfortunate things is that all of those chatrooms are going to come across at that same channel, so that's going to need to pass in the chatroom id so that we can filter that out client side. Even though server side we're going through a couple channels, it's being all wrapped up and being pumped over to the browser in one channel, and we have to add in that extra data in order to send it over.

The reason we separate this out server side is just because we can, and it also allows you to make sure you can separate out any messages from other groups or other channels that you shouldn't have access to. Because we've defined our chatrooms channel in the server side stuff to only allow you to do the ids that you've already joined, this is going to make sure that you can never inject and start listening in on a channel that you haven't technically joined. The reason why we do that is so that's all grouped together, then you're not getting messages from other organizations or other users or any of that type of thing. We have to add in this little bit of overhead for the chatroom id here so that our browser can determine what we're currently looking at and what's coming over, and filter out the ones for the current chatroom and display those messages, and if it's for any of the other ones, it can actually just add them either to a variable in memory, so it could automatically load those, or it can just use those to just change the flag on one of the other channels, so it shows that they're unread messages which is what we're going to do. With that little bit of a deviation. Let's take a look at this, so let's restart our rails server, we're using redis now, we have two browsers open and in the dark browser we should be able to say "test" and the light browser we should see the message come across.

We didn't get the message to come across right away and the reason for that is because the message relay job actually has a typo in. So I had this plural here, but when I went to the chatroom channel, this actually was not plural here, and that was causing a problem. Now I also wanted to use this as an example to say that you can do a strem_for chatroom and this will figure out a chatroom name for you so you wouldn't have to define those strings there, but I'm not fully up to spec on how all that works so I'm going to be defining our own strings for the channel names and you should be able to do that if a different way where rails will generate the string for you, but I actually don't know how you would define that outside of this, so we'll cover that in a future episode I'm sure, but make sure that you do your spelling correctly otherwise you won't get those correct messages. So here I did a test message in the console just to make sure that this was working. That's where I discovered the typo. Now we should be able to see, if we switch between browsers that we'll be able to see the messages show up in the console. I also made a typo here where I said message controller instead of messages (plural) controller, and that was causing the background worker to fail when it executed while I was trying to test this so make sure that you have that working as well and if you refresh the browser will reset up all of our websocket connections, make sure that everything is going correctly and we should be able to write tests in general, and if we open up our console in the other tab we should see that we see tests come across and we do, which is perfect so we see that our messages are coming across and now we need to filter these out because if we were to simply do the basic thing here, saying: Let's go to the chatrooms/show.html.erb, we have that data-behavior messages, we could just say:

$("[data-behavior='messages']").append(data.message)

We could so that, but you're going to notice a problem if I navigate to random over here, and I say "test", you're going to see that test shows up there and if I say "random" here it will also show up in the same channel in general because the client-side is not filtering out those appropriately, so we're going to need a couple of things here. Number one is that we're going to need to add a data chatroom id. This way, we can have the messages behavior but we can also look for the data-chatroom-id=#{data.chatroom_id} and so we'll be able to add two selectors in there and it will look for both of those, and that should now only accept messages in the general channel. If we refresh this page, it will have the new JavaScript. We'll refresh this one, and now if I say: random, it should not show up in the other page and it does not. But if we go to ActionCable, the requests here. You can say that it did come across and we can do see that messaged that came across that websocket connection you can see the frame for it but the JavaScript knew that we're not actively looking at the correct channel so we shouldn't do anything with that. Now one other thing that you can do is start to use that in that case, and that will be an else, so we can check to see

chatrooms.coffe

received: (data) ->
    active_chatroom = $("[data-behavior='messages'][data-chatroom-id='#{data.chatroom_id}']")
    if active_chatroom.length > 0
    active_chatroom.append(data.message)
    else 

application.html.erb

<ul>
    <% current_user.chatrooms.public_channels.each do |chatroom| %>
        <li><%= link_to chatroom.name, chatroom, data: {behavior: "chatroom-link", chatroom_id: chatroom.id} %></li>
    <% end %>
</ul>

Now if we refresh this page and we inspect this, we'll see that this has the data behavior and the data chatroom id which gives the JavaScript something to do when there is not the active chatroom. Here we can look up that data-behavior

chatrooms.coffe

received: (data) ->
    active_chatroom = $("[data-behavior='messages'][data-chatroom-id='#{data.chatroom_id}']")
    if active_chatroom.length > 0
    active_chatroom.append(data.message)
    else 
     $("[data-behavior='chatroom-link'][data-chatroom-id='#{data.chatroom_id}']").css("font-weight", "bold")

We should see that refreshing these pages, I should be able to come to general, if I say "hello" on that one, you'lll see that now random has gone bold in real-time on the other page because it has unread messages.

This is running over 20 minutes so far, but as you get more familiar with the ActionCable stuff and all the little tricks that you have to remember, like using redis and so on. Once you do that you're going to be able to breeze through this a lot faster and just kind of have a standard way of going about this stuff, but the first few times you're going to do it you'll need to repeat yourself a bunch kind of stumble through the same the same mistakes a few times and this will become quicker and quicker as you go. We have a pretty good foundation, we're starting to get some actual decent functionality from a real-time chat app that are like: Hey there's unread messages in this other channel. This is pretty great and we're starting to make some good progress to it, so I'm going to leave this episode here, we'll dive into some more advanced stuff in the next episode, we're going to need to do real-time, well not real-time unread messages so we're going to need to remember what you've seen and what you haven't and that is going to be a fun episode. We'll also have to be able to remove the channels when you leave one. Disable the websocket connection for that channel and so on and all those fun things. We have a lot more to do but this has been a pretty fun project so far. We've spent a good amount of time building up very basics for it, but it's also a good foundation for a fairly complicated app because chat is actually a fairly complicated thing when you really get down to all of the details. That's it for this episode. Talk to you in the next one, hope you enjoyed it, I will talk to you later. Peace ✌️

Transcript written by Miguel

Loading...

Subscribe to the newsletter

Join 18,000+ developers who get early access to new screencasts, articles, guides, updates, and more.

By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

More of a social being? We're also on Twitter and YouTube.