Skip to main content

14 Group Chat with ActionCable: Part 5

Episode 133 · August 17, 2016

Sending chat messages from the browser to the server with ActionCable

ActionCable


Transcripts

What's up guys? This episode we're going to take and convert the AJAX form for submitting a new message in a chat room, and we're going to convert that using ActionCable, and the reason why we're going to do that is because we've already got this open connection, and it has to be open, otherwise our chat is kind of useless, so we might as well take advantage of that and send our messages across through ActionCable instead of an AJAX request, so we're going to refactor what we've built and convert it to using ActionCable. The first thing that we want to do is actually look at the form itself, so I've pulled that up here in the chatroom/show.html.erb and we have our messages container, and our form just underneath that, and we were currently submitting this as remote= true which will send it over with jquery ujs as an AJAX request. Now this is fine, but we want to remove that because we want to intercept this form submission with our own code in order to go use the chatrooms ActionCable channel. We're going to write our own JavaScript and instead of jquery ujs doing it, we're going to do the intercept and that will be that. This is pretty straightforward, we're going to need to pull out the chatroom id and we're also going to need to pull out the message text from the body field here so we'll need to write some JavaScript to pull that off. We've already written a little function here that says anytime you type the enter key in that message box, we're going to not do a new line and we're going to submit that to the server, so this really says: Well if the character code or the key code is 13, which is the Enter key, we're going to not insert a new line so that was the default, so we're going to prevent the default and then we're going to submit that form. We can actually say

assets/javascripts/chatrooms.coffee

  $("#new_message").on "submit", (e) ->
    e.preventDefault()
    console.log "SUBMITTED"

Let's refresh this page and let's type: "hello random" and if hit enter, like I just did, nothing happens. That is a good sign, because then you should see a submitted form in the console and that means that our JavaScript here it not only listened to that Enter key in the first function, it submitted the form with this first dot submit, and immediately after that this function to this submit grabbed it and then canceled the submission. This is where we're going to need to grab both the chatroom id and the message text for that text field. First we can really just grab the chatroom id and this is pretty straightforward, it actually is on the messages container, and so we have the data-behavior messages class or attribute and then on that tag we can grab the chatroom id so we'll just take that one place that we will always know that will be there because the rest of our JavaScript will use that, so we'll just make sure that we grab it from there, so we'll have data-behavior

assets/javascripts/chatrooms.coffee

  $("#new_message").on "submit", (e) ->
    e.preventDefault()

    chatroom_id = $("[data-behavior='messages']").data("chatroom-id")
    body        = $("#message_body")

assets/javascripts/channels/chatrooms.coffee

  send_message: (chatroom_id, message) ->
    @perform "send_message", {chatroom_id: chatroom_id, body: message}

We'll send over this object, this JSON object, and so the server side will receive that. If you go into your

app/channels/chatrooms_channel.rb

def send_message 
    Rails.logger.info data
end 

So this @perform is calling send_message and we'll have some data that we receive which will be this object here and we will be able to receive that. We'll have this set up so that we can perform that send message function server side, and we can just pass that data over, and ActionCable is going to know what to do, it will convert it to the ruby function. It will call that function, it will execute that server side and do whatever you want. That means all we have to do now is to go back to our original

chatrooms.coffee

 App.chatrooms.send_message(chatroom_id, body.val())

We're going to grab the value of the field, so the actual text that you type and we'll send that over as the message argument which will get sent over again to the server side channel and will be received as one object, because we have this in a JSON object. This makes it all into one thing, and then we have data on the server which is that one thing, and it's just the hash and we can pull that apart and display. Let's save all of this. Refresh our page here, and then let's type "Test random" and hit Enter, and this should submit and we should be able to look at our logs here, and we'll see that user two received this message which was the info that we did. The Rails.logger.info, so you can see that the user 2 called the send_message function and passed over this JSON hash that got converted into a ruby hash, or JSON object, so this got converted to a ruby hash and you have full access to all of that stuff just from the ruby side of things just like you did from the client side. So it's cool it actually just transmits this data and says: Well from the JavaScript you can call any functions that you want server side and you're good to go as long as they're defined in chatrooms_channel.rb so as long as you access the right ActionCable subscription in your JavaScript, you can always call those methods server side from your JavaScript, so that's pretty neat. This is not very hard, and then the last thing that we want to do is say

chatrooms.coffee

body.val("") 

That will go clear out that string which when we hit enter here it didn't do anything. And if we do it now, and we hit Enter, it sends it over to the server it logs it on the server and then it clears this out. And so that is all working. The only thing we need to do next is to take that and instead of logging that message server side like we did here, we can actually just take this and transmit or broadcast that to all of the recipients for this channel for that chatroom. So we have the ID so we know the id and we're basically able to do the exact same publish as we did with the AJAX action. Now the question is: How do we actually go save that message and then send it out and broadcast it again? Well conveniently we already did that, so we can go into that messages controller that we created before and we can basically duplicate all that work that this does and put it in that function in the chatrooms_channel.rb, so basically we can set that chatroom just as we did before and we can say

chatrooms_channel.rb

def send_message(data)
    @chatroom = Chatroom.find(data)["chatroom_id"]
    message = @chatroom.messages.create(body: data["body"], user: current_user)
    MessageRelayJob.perform_later(message)
end 

This is going to effectively do the same thing that the entire controller action did but we'll be able to do that over the websocket connection instead. We don't really need this controller anymore because we're not going to be submitting this over AJAX. This won't ever really be that useful and you could probably go and delete this controller if you wanted. That is going to work, and we should be able to restart our rails servers, and sometimes when you're changing those channels you'll need to restart your rails server because once it's already loaded and the connections are live and all that you can't actually change it and get live updates like you can with the new request. So let's test this out and refresh our browser and get the latest JavaScript and let's test random, hit enter, and voila. We've sent that now over the websocket connection it has landed in the channel, it processes it, saves it in a database, and it sends it over to the same relay job which goes to your background workers which then talks to your ActionCable redis connection again which sends it out to everybody, and then everybody receives it and displays it in the browser.

Now we're doing quite a bit of stuff with ActionCable, we're receiving messages, we're sending messages, we have the ability to add new features in so like if I receive a message I can send a little notice to the server and say I've read up to this time and we can keep track of unread times and all that stuff which we're going to do in a future episode, but I do want to point out that now that we've done this, ActionCable is 100% critical to be running in our application. The way we had it before, where the form submitted with AJAX, if ActionCable was down, yeah, we don't receive new messages, but we can still send them and they will still work as long as the rails app is up. But now that that's sending goes over the websockets as well, the rails app could be functional, but if ActionCable isn't, our app doesn't do anything. It isn't able to save any messages and that's a problem, so you have to kind of keep in mind what you want to use your websockets for and determine weather or not it's crucial for it to be up as much as possible. Of course in a chatroom application you're going to focus more on this connection being up as much as possible, but if it's sort of a side project or something where you're doing notifications, you might consider making the requests over AJAX and sending data and just using it for receiving notifications. GitHub is a good example of this where the GitHub issues will update in real time using websockets but if you were to go down you don't really lose out on much of that user experience at the end of the day. You just kind of lose out that someone commented, but you can still comment and you can still use it even if they're struggling keeping their websocket servers up for whatever reason, like maybe they blow up in traffic one day and they're able to keep the website up but not the websockets. It's just something to keep in mind. It's now a new dependency that your 100% relying on, and that should affect the way that you go build your application, just to keep that in mind so that when you build something that really when it goes down it goes down hard. That's kind of just be something to keep in mind so that you can build a lot more sturdy applications just in case something bad happens.

That's it for this episode, we will be diving in most likely in the next one to have the ability to mark as unread so that we can see when the most recent messages were displayed. That should be fun but we will save that to the next episode. I hope you enjoyed it, I'll talk to you in the next one.

Transcript written by Miguel

Discussion


Gravatar

Hi Chris,
How many users is a chatroom-site like this able to handle?

thanks,

Gravatar

That will depend on how big your server is and it's configuration. I haven't tested any websocket stuff at scale, but I've seen some performance tests for 10,000 users before. It starts to slow down at a point, and they were using simulated things, so never real world data. Your mileage will probably vary quite heavily if you start going past a couple thousand users.


Gravatar

Hey Chris,
With this code (no controller action and message creation happens in channel), what is the best way to authenticate the user for message creation? The only solution that comes into my mind is front-end authentication with currentUserId on the body tag and checking with jQuery if there is any number, I don't really like it. Is there a better approach at the moment?

Gravatar

What we setup earlier was Devise so that you can login and since WebSockets share the same cookies a web request does, when you connect to the WS, it will log you in with Warden (devise's underlying authentication layer) and let you use the WebSocket. Then we have access to the current_user method inside ActionCable just like we do in a normal web request.

On the frontend, you can add a current-user meta tag keep track of it in your JS so that you can customize your frontend to handle things differently if you're not logged in, etc. Check out this episode for more on that: https://gorails.com/episode...


Gravatar

Hi Chris,
Awesome tutorials!!!

I followed instructions to refactor the existing code from last episode, but it doesn't work on my end. When I type in the message and press Enter, it doesn't get sent, nothing happens. I tried to log the message, but also nothing... I am not sure what happened there... Did you have similar problems?

Thank you!
Marko


Gravatar

Hi Chris. You're a life saver with these tutorials!

How would you extend this app to be or allow one-to-one chat. I've been thinking of a channel being created for each user-user combination - and only show it to those users. Conceptually it doesn't sound as an elegant solution to me. How would you go about it? or if you could point me in the right direction.

Thank you,
Alex

Gravatar

The main thing is probably that you could create a "direct_message" attribute for Channel where you can mark it as a direct message and auto-attach the users (yourself and the person you want to DM). On create, you can attach the users to it, and then pretty much everything else probably works about the same. The navbar probably renders direct message ones separate from the main channels and should only display for users who are in it.

Gravatar

When you say channel do you mean Chatroom or Chatroom_users? Or something else? I'm a newbie to programming so I'm still getting used to the language and how to understand technical explanations :)

Gravatar

Ha, no worries at all. :)

You'll need to tweak or at least work on both a bit. Basically you want to create a private Chatroom where the only two ChatroomUser records are you and the person you want to talk to.

Gravatar

Thank you! I got a working version of it by creating two ChatroomUsers on the Chatroom create function. but when I integrated on my multi-tenant app, everything works except the realtime part of it. I believe it has something to do with the MessageRelayJob part - is says "Performing MessageRelayJob from Async(default)" That should be redis though, right?


Gravatar

Hi Chris, thanks for these very good explanations. You mentioned the possibility of disconnecting a user from a channel when he wants to leave.
I don't know if it is explained in one of the two next pro episodes and if not I have two questions :)
I've used your tutorial to build a simple realtime messaging app with Mailboxer. It works pretty well thanks to you. What would be the simplest way to disconnect a user from a channel ? I would call the stop_all_stream function but I don't know exactly how to.
Furthermore, by calling this function, do we disconnect a user from a single chatroom or from all the chatrooms ?
Thanks a lot !

Gravatar

Interestingly, there isn't a stop_stream method in ActionCable, so you'd have to stop all streams at least right now. Someone has opened a PR with this but it's still pending: https://github.com/rails/ra...

For now, I guess you can either stop_all_streams and reconnect to the ones you want, or you can monkey patch in the code from that pull request. I would probably do that because it's likely to get accepted.

And to implement the disconnect, you'd just send the message from the JS to call a method in the server side channel to call the stop stream. This would look almost exactly like sending a message, you'd just pass in the name of the channel you want to remove.

Gravatar

Thank you for this answer and your dedication !


Gravatar

Hi Chris. Thanks for your great tutorial!!

I want to keep the chatroom name bold. But if I reload the page, the chatroom name back to normal font-weight...
How can I keep that css??

Thank you,
K


Gravatar
Hi, chris. Thank you for taking me so far.
But i got a problem. you remember this

  $("#new_message").on "submit", (e) ->
    e.preventDefault()

but in my case, the 'e.preventDefauly()" does not work always. sometimes, it goes html like

Started POST "/chatrooms/1/messages"

And sometimes, it goes from coffees to job&channel... like

Started GET "/cable/" [WebSocket] for 127.0.0.1

Do you see any clear answer this problem? Help me:)
And why e.preventDefault() and following coffee codes do not work sometimes?
As i see log, when socket successfully connencted it works fine. But if there is no "Socket successful..." sentence, then it goes HTML way....why?!! :(

Gravatar
Hi Chris, I am hoping you can help me narrow down my error. I am trying to set up the app on a multi-tenancy app. I had all the models working before we started to use Action Cable, Active Job, and the more complex coffee script. I was able to add messages to the chatroom without any errors. But, now I am getting an error message where the tenant_id is NULL. I have tried to figure out where and how I would add the tenant_id to the code. Where do I add the tenant_id? Do I need to add it to the coffee script or the channel or the job? Or would I need to add it to multiple locations? 

Gravatar
please help me! 
i build a message app in my web following this tutorial.
but As i enter the chatroom & send message, then 

"ActionController::InvalidAuthenticityToken in MessagesController#create" error occurs.
I think it means "submit" acts as html form.

but As i enter the chatroom & refresh, then log tells me that 

"Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)" 
it means actioncable is working now.

so, user has to refresh as they enter the chatroom. it is rediculous, right?
what is the reason? Why actioncable does not work when entering the chatroom at the first place?

Login or create an account to join the conversation.