Skip to main content
23 Realtime Group Chat With ActionCable:

Group Chat with ActionCable: Part 4

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

Discussion


Fallback

As for message relay jobs matter for this particular subject of this ActionCable example, do you recommend putting down for its current_user's id or name in these kind of scenarios when it comes to logging down for the Message job queue and delivering it to Sidekiq?

Fallback

Not sure I'm following the question, can you clarify for me? ๐Ÿค“

Fallback

Starting at around 12:29, you were talking about the MessageRelayJob for broadcasting the message and chatroom#id into the job. So, what I am trying to say is let's say that the user wants to know if the message sent out into the channel but says it does not not return anything. Looking at the sever log views, would it be technically correct to display the user's username (if provided in the User model) instead of ID and as well as other methods provided for the user when troubleshooting chat history?

Example:


Class MessageRelayJob < ApplicationJob
def perform(message)
ActionCable.server.broadcast "chatrooms:#{message.chatroom.id}", {

message: MessagesController.render(message),
chatroom_id: message.chatroom_id
# Display the User ID or username (if defined and not nil)
user: current_user.username ||= current_user.id

end

end

Even though, it successfully connects to the websocket and is listening to the specific channel.

Fallback

I gotcha now. So I would keep everything organized on the Message's ID. You can client side send over a message and you could set it up to send back a message ID. That way you now it arrived server side but it's "processing". Then server side you can store that message and tag all your ActionCable broadcast stuff with the Message record's ID. You'll get a database log of messages which is important for loading up the first time, you can load recent history, and you can use that for troubleshooting. The Message record can have a statemachine on it so you know if it was broadcast or not. Slack basically does this when you're having internet issues. It can tell when something got published or not and keeps track of that nicely. You can flag those messages as "ready for broadcasting" or something and then after you broadcast it in the Relay Job, you could then mark it as complete so that if your job did crash or something, you wouldn't resend it.


Fallback

this series is really cool, thanks Chris

Fallback

Glad you like it! ๐ŸŽ‰


Fallback

Hi Chris! Thank you for your lessons!=)

<% if chatroom.chatroom_users.find_by_user_id(current_user) %>
<%= form_for [@chatroom, Message.new], remote: true do |f| %>
<%= f.text_area :body, rows: 1, class: 'form-control', autofocus: true %>
<% end %>
<% else %>
<%= link_to "Join", chatroom_chatroom_users_path(@chatroom), method: :post, remote: true %>
<% end %>

Thanks


Fallback

Hi, Chris! Great tutorial, thank you very much!
I'm trying to follow it to create a Chat Api. Is it possible? I would receive the messages through an Android app, pass it to my Api and then send the new messages to Android again as json. Could you give me any clues about what do I have to change so far? (I am already not implementing all the views).

Fallback

Hey Diane,

Two different approaches you could use:

1. Keep it simple and go with a Turbolinks-Android hybrid app where your chat is via the webview on the app. This is simpler and easier to setup.
2. You could do a native Android connection directly to the websocket. For this you'd basically use a standard websocket library for Android and then make the small tweaks necessary to setup the consumer just like the Javascript does. You'd basically be porting that JS into Android code. Not entirely sure on all the details or if someone else has done this already, but that's the rough outline for a native approach.

Fallback

Fallback

Chris, how can you have you redis working without configuring and running the server in dev env? For me it raises "`rescue in establish_connection': Error connecting to Redis on localhost:6379 (Errno::ECONNREFUSED) (Redis::CannotConnectError)`" which is logical since the redis server is down.

Fallback

I have my redis server always running on my laptop on the default port so it is always ready to go. Homebrew comes with instructions on how to start it when you login. If you follow those it will set it up the same way I have.


Fallback

For those getting error about "Request origin not allowed" and "Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)"

If you are running rails server with a specific IP address please add "config.action_cable.allowed_request_origins" to your 'development.rb' file and point it to the IP address you are using

Fallback

Great point Rick! That's probably a common problem for anyone trying this. Thanks for sharing that. ๐Ÿ‘

Fallback

I have to use Vagrant because I am on Windows, as such I had to bind 'rails server' to the IP address Vagrant is using. So I too suspect this is a common issue.

Also, I had to stop and start 'rails server' whenever I manually create controllers, otherwise I get controller errors.


Fallback

I am surprised no one is getting the error messages I am getting while doing this part.
When I add remote: true i get " ActionController::InvalidAuthenticityToken
in MessagesController#create" error. (to solve it i must add authenticity_token: true).
When I remove the redirect_to @chatroom i get another error:
MessagesController#create is missing a template for this request format and variant.

request.formats: ["text/html"]
request.variant: [] even if I created a messages/create.js.erb file in my views.
I am on a mac using ruby 2.3.0 with rails 5.0.1 via rvm. Could this be the evil source of my problems?

Fallback

You might continue on a bit, I ended up refactoring this to use ActionCable to send messages over instead of an AJAX form because you might as well be using it two ways.

I'm not sure what's up with your authentication token, but it does sound like your server side is looking for an HTML response, not a JS response, even though if it was an AJAX request, it should. Sounds like something's not quite right with that somehow. I'm not entirely sure off the top of my head though.

Fallback

Since no one has reported similar issues I am inclined to believe something is wrong with my setup somehow. For now I'll just press on. Thanks

Fallback

The authenticity_token: true thing makes me think the same. I haven't ever had to do that, but maybe something has changed more recently that I don't know about. Always possible!

Fallback
Getting the same error.ย 
Fallback
I encountered the same error message. I reviewed the logs and I can see an HTML response, not a JS response. If you follow all the video content from this video and the next video until 2:35 the error message will disappear. I do not know why it happened, but if you just keep going you can get past it.

Fallback

Hey Chris, I 've got an error when I click on the button "join" => "The action 'show' could not be found for ChatroomUsersController";
But I did exactly what you did in the last videos... I have the same link_to than yours; An idea what's going on? thanks a lot

Fallback

Hey Brice, you might check your code because the Join button I believe needs to submit a POST request and your error is looking for the show action which implies it sent a GET request instead. The Join button should create a POST request in order to create the record to set you as a user inside the channel, so it should take you to the ChatroomUsersController's create with that POST. Make sure that link has a "method: :post" option on it?

Fallback

Hey chris, thanks a lot for your reply. Yes of course, I have the "method: :post"... I copy/past all your code but still doesn't work, very strange.... :(
I send you my roots. Hope it can help, if not that's okay Chris ;)

Fallback

jquery_ujs is used to trigger that. If you've got method post, then you'll want to see if you have JS errors that are causing it not to run that code to intercept the click and submit a POST instead.

Aside from that, I'm not sure. You might clone the repository and see if that works for you and compare your code with it. Probably something small, but super hard to debug over comments. :)


Fallback

Hello Chris, just a quick question regarding the best practice with jobs: I learned in the past that passing only the id parameter to the job with sidekiq or else, then find the instance inside the job is the best practice. Since you pass the complete instance, I'm wondering if Rails do it for us automagically with ActiveJob? Thank you

Fallback

Ideally it's better to pass in only the ID. If the record was deleted before the job runs, you can safely quit if you can't find the record assuming it was deleted.

Now I believe that if you pass in an ActiveRecord job to ActiveJob, it will transform it to a GlobalID (https://github.com/rails/gl... / http://edgeguides.rubyonrai... which is a representation of the ID and class name so that you don't have to specifically pass the ID.

If you use Sidekiq, etc directly without ActiveJob it is going to try using that object which is why they recommend using the ID specifically.

ActiveJob + Global ID should look up the record once the job starts (you should see this in the logs) which will make running those jobs safe.


Fallback

Hey Chris, i've been following the videos for the group chat and so far they're awesome (as are all the videos of yours that i've watched).
I've run into a bit of a stale point though, in your video around 15:31 we're logging the message data to the console, but seems it's not working for me, nothing is printing to the chrome console.

my rails server output is showing:


[ActionCable] [User 2] ChatroomsChannel is streaming from chatrooms:2
Started POST "/chatrooms/1/messages" for ::1 at 2016-10-16 14:45:53 +0100
Processing by MessagesController#create as JS
Parameters: {"utf8"=>"โœ“", "message"=>{"body"=>"show me"}, "chatroom_id"=>"1"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 2], ["LIMIT", 1]]
Chatroom Load (0.1ms) SELECT "chatrooms".* FROM "chatrooms" WHERE "chatrooms"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.0ms) begin transaction
SQL (0.4ms) INSERT INTO "messages" ("chatroom_id", "user_id", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["chatroom_id", 1], ["user_id", 2], ["body", "show me"], ["created_at", 2016-10-16 13:45:53 UTC], ["updated_at", 2016-10-16 13:45:53 UTC]]
(7.4ms) commit transaction
[ActiveJob] Enqueued MessageRelayJob (Job ID: a83d7365-8e56-4ea6-a03f-db172f5b1c62) to Async(default) with arguments: #<globalid:0x007ff34bd10358 @uri="#&lt;URI::GID" gid:="" slack-clone-rails5="" message="" 16="">>
Message Load (0.3ms) SELECT "messages".* FROM "messages" WHERE "messages"."id" = ? LIMIT ? [["id", 16], ["LIMIT", 1]]
Rendering messages/create.js.erb
[ActiveJob] [MessageRelayJob] [a83d7365-8e56-4ea6-a03f-db172f5b1c62] Performing MessageRelayJob from Async(default) with arguments: #<globalid:0x007ff34c393810 @uri="#&lt;URI::GID" gid:="" slack-clone-rails5="" message="" 16="">>
Rendered messages/create.js.erb (0.6ms)
Completed 200 OK in 113ms (Views: 26.6ms | ActiveRecord: 8.0ms)

[ActiveJob] [MessageRelayJob] [a83d7365-8e56-4ea6-a03f-db172f5b1c62] Chatroom Load (0.1ms) SELECT "chatrooms".* FROM "chatrooms" WHERE "chatrooms"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
[ActiveJob] [MessageRelayJob] [a83d7365-8e56-4ea6-a03f-db172f5b1c62] Performed MessageRelayJob from Async(default) in 31.33ms


************
Which seems all good, just wondered if you had any input why this wouldn't be showing the console.log data

chatrooms.coffee:


App.chatrooms = App.cable.subscriptions.create "ChatroomsChannel",
connected: ->

disconnected: ->

received: (data) ->
console.log data

message_relay_job:


class MessageRelayJob < ApplicationJob
queue_as :default

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

chatrooms_channel:


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
end

not sure what else you would need to check? if you need my repo it's here
https://github.com/ggomersa...

Hope you can help :)

Fallback

Hard to say off the top of my head. You might just need to put in some more debugging lines to make sure that everything is connecting correctly. Make sure that 1) the JS connects to the websocket by watching the Chrome network logs 2) Make sure that your server side background job is executing by printing out in your logs 3) Make sure your user is connected to the channel so their websocket streams from it correctly.

Probably something small isn't connected right and it should be an easy fix once you figure out what it is. Also here's the link to the final app code for this that might be of help: https://github.com/gorails-...

Fallback

found it! :)

my message_relay_job.rb file was showing:


message: MessageController.render(message)

instead of:


message: MessagesController.render(message)

It's working a treat now :)

Fallback

Ah ha! That would do it! Good find. ๐Ÿ‘


Fallback

Hi Chris,

At 5:59, in the connection.rb file you add:

protected

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

If I'm not using devise, is "env['warden'].user" just the devise way of authenticate_user ? Is this just to make sure the current_user is the logged_in user? Want to make sure I'm implementing correctly.

Thanks!

Fallback

Exactly, you can just replace that with however you find the current user normally. ๐Ÿ‘

Fallback

Ok cool. If current user already checks that, do you have any suggestions for how to rewrite that? Would it just be:


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

Don't worry about this. Worked on it today. Figured it out!

Fallback

I was going to say, I don't think you can do that because ActionCable doesn't go through Rails controllers. You'd want to set self.current_user equal to the logic that finds the user from the session.

What was your solution? I'm sure other people would love to know it as well!

Fallback

I created a new ruby symbol "e.g. :actioncable_user_id" specifically for this action cable use.

In 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
current_user = User.find_by(id: cookies.signed[:actioncable_user_id])

current_user || reject_unauthorized_connection
end

end
end

and in my sessions_helper.rb (used for logging in / logging out)


module SessionsHelper

# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
#below is unique code added for ActionCable find_verified_user
cookies.signed[:actioncable_user_id] = user.id
end
end


Fallback

Just offering a solution here for some error messages I kept getting. The server error I got was:


Started GET "/cable" for ::1 at 2017-02-09 16:35:06 -0800

ActionController::RoutingError (No route matches [GET] "/cable"):

I added to my routes.rb :


Rails.application.routes.draw do
mount ActionCable.server, at: '/cable'
...
...
end

Not sure why in this repo example action cable works without it, but in my app once I added this everything worked. I'm using Rails '5.0.1' for reference.

Fallback

Hmm weird, they changed that to be the default URL for Rails in 5 I believe so it shouldn't be necessary. The project I'm using in 5.0.1 doesn't have it and it finds ActionCable just fine. Not sure what's up there.

Fallback

Definitely, I don't know why either. The only thing I could think of is at one time my app was Rails 4, and then also Rails 5 beta. Perhaps that requirement never got fully uninstalled or something. Beats me. Oh well, works now though!


Fallback

Hey Chris, Can you elaborate on the difference between what you use chatrooms_channel.rb and chatrooms.coffee for?

My interpretation so far: Chatroom_channel.rb is saying "These are the things we want to listen to" and chatrooms.coffee is where we say "Ok, those things we're listening to, if some new things happen, do these actions".


Fallback

Hello Chris,

My problem is that my channel is broadcasting but on the client side(the coffee script) didn't received anything, I did add an alert if it was connected and it is connected though, any idea why this happening?


Fallback

great tuto :)
i now want to manage rights so that the author of a comment can edit it. any ideas?


Fallback

Hey @excid3:disqus awesome series. My messages get saved to the db and in the response I can also see that turbolinks. But the message does not get added in the front end. I have to refresh the page in order to see it on the front end side. Any thoughts?


Fallback

Fallback
Since rails 5.2 the remote: true doesn't really work anymore for the form_for when you are trying to submit a new chat message in JS. I found that no matter how I tried to get it to answer with remote: true as JS it would always post it as html. The answer for rails 5.2+ is to use form_with as form_for is being depreciated. Also by default form_with sends all submits as JS, so we don't use remote: true.

In your show.html.erb, change the form_for line to this:
<%= form_with model: [@chatroom, Message.new], :html => { :id => 'chatroom_form' } do |f| %>

In your create.js.erb change the reset line to this:
$("#chatroom_form")[0].reset()

And if you are still doing the area thing (I didn't like that style personally) you need to change the keypress line to this:
  $("#message_body").on "keypress", (e) ->


Fallback

I've figure out the problem. a trick.
after use form_with, you must change the code in this video.
my code in chatrooms.coffee:
document.addEventListener 'turbolinks:load', ->
document.getElementById("new_message").addEventListener 'keypress', (e) ->
if e && e.keyCode == 13
e.preventDefault()
url = this.action
data = new FormData(this)
Rails.ajax({
type: "POST"
url: url
data: data
dataType: "json"
})


Fallback

Hi Chris,

Question: Two errors that I'm hoping you can assist with:

  1. https://gyazo.com/19aaab916790ec3c1a41476cc621398b It says the redis gem is not loaded. But it is. I found some posts on StackOverflow that says the redis gem 4.0.2 doesn't work with ActionCable, so you needed to use 3.3. But I installed 3.3 and still no success. Is there perhaps some type of additional setup? I did make sure to change the cable.yml as follows:
development:
  adapter: redis
  url: redis://localhost:6379/1

test:
  adapter: async

production:
  adapter: redis
  url: redis://localhost:6379/1

Anything obvious that I am missing, or forgetting to do?

  1. This may be relate to the above question. In console, when I type a message, it disappears properly. And posts when I refresh the page. But it's not showing up in the other console as an object, as the video suggests it should. Thoughts?

Thanks very much!
-Monroe


Login or create an account to join the conversation.