Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails5 ActionCable Chat with Conversations

Tags:

According to DHHs Rails5 ActionCable chat example I'm going to create a further example with conversations and many messages in there:

rails g model conversation 

class Conversation < ApplicationRecord
  has_many :messages
end

rails g model message content:text conversation:references

view/conversations/show.html.erb

<h1>Conversation</h1>

<div id="messages">
  <%= render @messages %>
</div>

<form>
  <label>Say something:</label><br>
  <input type="text" data-behavior="conversation_speaker">
</form>

view/messages/_message.html.erb

<div class="message">
  <p><%= message.content %></p>
</div>

My questions is how to write a channel logic that every message related to its conversation gets written to the database:

First I recorded a conversation and a message on the console

Conversation.create
Message.create(conversation_id: '1', content: 'hello')

afterwards I created a Job

rails g job MessageBroadcast

class MessageBroadcastJob < ApplicationJob
  queue_as :default

  render_message(message)
  def perform(data)
    message = Message.create! content: data
    ActionCable.server.broadcast 'conversation_channel', message: render_message(message)
  end

  private
    def render_message(message)
      ApplicationController.renderer.render(partial: 'messages/message',
                                             locals: { message: message })
    end
end

and a channel

rails g channel conversation speak

assets/javascripts/channels/conversation.coffee

App.conversation = App.cable.subscriptions.create "ConversationChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel
    $('#messages').append data['message']

  speak: ->
    @perform 'speak'

$(document).on 'keypress', '[data-behavior~=conversation_speaker]', (event) ->
  if event.keyCode is 13 # return = send
    App.conversation.speak event.target.value
    event.target.value = ""
    event.preventDefault()

If I write:

channels/conversation_channel.rb

class ConversationChannel < ApplicationCable::Channel
  def subscribed
    stream_from "conversation_channel"
  end

  def speak
    Message.create! content: data['message']
  end
end

I get

Started GET "/cable/" [WebSocket] for ::1 at 2016-04-22 00:22:13 +0200
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: keep-a
live, Upgrade, HTTP_UPGRADE: websocket)
Started GET "/cable" for ::1 at 2016-04-22 00:22:13 +0200
Started GET "/cable/" [WebSocket] for ::1 at 2016-04-22 00:22:13 +0200
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: keep-a
live, Upgrade, HTTP_UPGRADE: websocket)
ConversationChannel is transmitting the subscription confirmation
ConversationChannel is streaming from conversation_channel
ConversationChannel is transmitting the subscription confirmation
ConversationChannel is streaming from conversation_channel

looks okay but if I enter some text in the textfield and hit return I get:

Could not execute command from {"command"=>"message", 
"identifier"=>"{\"channel\":\"ConversationChannel\"}", 
"data"=>"{\"action\":\"speak\"}"}) 
[NameError - undefined local variable or method `data' for #<ConversationChannel:0x00000008ad3100>]: 
C:/Sites/ActionCable/app/channels/conversation_channel.rb:13:
in `speak' | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/actioncable-5.0.0.beta3/lib/action_cable/channel/base.rb:253:
in `public_send' | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/actioncable-5.0.0.beta3/lib/action_cable/channel/base.rb:253:
in `dispatch_action' | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/actioncable-5.0.0.beta3/lib/action_cable/channel/base.rb:163:
in `perform_action' | C:/Ruby22-x64/lib/ruby/gems/2.2.0/gems/actioncable-5.0.0.beta3/lib/action_cable/connection/subscriptions.rb:49:
in `perform_action'

Any ideas?

like image 212
Stef Hej Avatar asked Apr 18 '16 16:04

Stef Hej


1 Answers

Going from the client side to the server side, you must (first of all) make your speak function accepts a message parameter AND send that message to the server as a JSON object.

speak: (message) ->
  @perform 'speak', message: message

Second, you have to define the parameters received by speak function at channels/conversation_channel.rb. Thus you must redefine it as:

def speak(data)
  Message.create! content: data['message']
end

Now your speak method is receiving a data parameter, which is a JSON with a message property which contains the message sent to the server. It's being recorded to the database, but there is no answer to those subscribed to the channel.

Thus we must notify them redefining the method above as:

def speak(data)
  Message.create! content: data['message']
  ActionCable.server.broadcast 'conversation_channel', message: render_message(message)
end

private

def render_message(message)
  ApplicationController.renderer.render(partial: 'messages/message',
                                         locals: { message: message })
end

Now it's supposed to work. What you'll do in background is up to you ;)

like image 137
Cristiano Mendonça Avatar answered Sep 30 '22 12:09

Cristiano Mendonça