0

I have created a WebSocket messaging feature using Rails 5 ActionCable. It works great when I use a DIV as the container. But when I changed it to use a UL and LI instead; it stopped working. I cant seem to debug this; but it looks like the received: event is not being trigger on the client. The send_message works...and is saved in the database, its just new messages do not appear on the page.

TL;DR; bit at the bottom of the question.


UPDATE

I have managed to diagnose the problem, it works again when i remove the lines (from the _message.html.erb partial) that check against the current_user message.user == current_user - but why?


Help would be greatly appreciated. Here is (I think) all the relevant code:

/app/assets/javascripts/channels/conversations.coffee

jQuery(document).on 'turbolinks:load', ->
  messages = $('#messages')
  if $('#messages').length > 0
    messages_to_bottom = -> messages.scrollTop(messages.prop("scrollHeight"))

    messages_to_bottom()

    App.global_chat = App.cable.subscriptions.create {
      channel: "ConversationsChannel"
      conversation_id: messages.data('conversation-id')
    },
      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) ->
        messages.append data['message']
        messages_to_bottom()

      send_message: (message, conversation_id) ->
        @perform 'send_message', message: message, conversation_id: conversation_id


    $('#new_message').submit (e) ->
      $this = $(this)
      textarea = $this.find('#message_body')
      if $.trim(textarea.val()).length > 1
        App.global_chat.send_message textarea.val(), messages.data('conversation-id')
        textarea.val('')
      e.preventDefault()
      return false

/app/channels/conversations_channel.rb

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class ConversationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "conversations_#{params['conversation_id']}_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def send_message(data)
    current_user.messages.create!(body: data['message'], conversation_id: data['conversation_id'], user: current_user)
  end
end

/app/jobs/message_broadcast_job.rb

class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast "conversations_#{message.conversation.id}_channel",
                                 message: render_message(message)
  end

  private

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

/app/views/conversations/show.html.erb

...

  <div id="messages" data-conversation-id="<%= @conversation.id %>">
    <ul class="list-unstyled media-block messages-list" data-conversation-id="<%= @conversation.id %>">
    <%= render @conversation.messages %>
    </ul>
  </div>

...

  <div class="row">
    <%= form_for @message, url: '#' do |f| %>
        <%= hidden_field_tag 'conversation_id', @conversation.id %>
        <div class="form-group">
          <%= f.label :body %>
          <%= f.text_area :body, class: 'form-control' %>
          <small class="text-muted">From 2 to 1000 characters</small>
        </div>

        <%= f.submit "Post", class: 'btn btn-primary btn-lg' %>
    <% end %>
  </div>

...

/app/views/messages/_message.html.erb

<% if message.body %>

    <li class="mar-btm message" data-id="<%= message.id %>">
      <div class="<%= message.user == current_user ? 'media-right' : 'media-left' %>">
        <img src="<%= message.user.avatar.url %>" class="img-circle img-sm" alt="Profile Picture">
      </div>
      <div class="media-body pad-hor
        <% if message.user == current_user %>speech-right
        <% end %>">
        <div class="speech">
          <h4><%= message.user.full_name %></h4>
          <%= message.body %>
          <p class="speech-time">
            <i class="demo-pli-clock icon-fw"></i>
            <span class="timestamp" data-value="<%= message.created_at %>"><%= time_ago_in_words message.created_at %>
              ago</span>
          </p>
        </div>
      </div>
    </li>

<% end %>

TL;DR;

As I said, previously i was using a container div: <div id="messages" data-conversation-id="<%= @conversation.id %>"> and each message was in a child DIV instead of a LI element, and it was working. I cant figure out what I broke (or why it matters).

When I post a new message it is send and saved to the database, but it doesn't get sent to the browser. The logs look like this though:

[ActionCable] [an...ey@gmail.com] ConversationsChannel#send_message({"message"=>"23r23r2", "conversation_id"=>5})
[ActionCable] [an...ey@gmail.com]    (0.1ms)  BEGIN
[ActionCable] [an...ey@gmail.com]   SQL (1.1ms)  INSERT INTO "messages" ("conversation_id", "user_id", "body", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["conversation_id", 5], ["user_id", 7], ["body", "23r23r2"], ["created_at", 2017-03-01 01:16:25 UTC], ["updated_at", 2017-03-01 01:16:25 UTC]]
[ActionCable] [an...ey@gmail.com]    (0.9ms)  COMMIT
[ActionCable] [an...ey@gmail.com] [ActiveJob] Enqueued MessageBroadcastJob (Job ID: a464e290-f23f-4cd4-a355-e364a43782e9) to Async(default) with arguments: #<GlobalID:0x007ff678cf74e0 @uri=#<URI::GID gid://itpo/Message/172>>
  Message Load (0.6ms)  SELECT  "messages".* FROM "messages" WHERE "messages"."id" = $1 LIMIT $2  [["id", 172], ["LIMIT", 1]]
[ActiveJob] [MessageBroadcastJob] [a464e290-f23f-4cd4-a355-e364a43782e9] Performing MessageBroadcastJob from Async(default) with arguments: #<GlobalID:0x007ff679dcc400 @uri=#<URI::GID gid://itpo/Message/172>>
[ActiveJob] [MessageBroadcastJob] [a464e290-f23f-4cd4-a355-e364a43782e9]   Conversation Load (0.3ms)  SELECT  "conversations".* FROM "conversations" WHERE "conversations"."id" = $1 LIMIT $2  [["id", 5], ["LIMIT", 1]]
[ActiveJob] [MessageBroadcastJob] [a464e290-f23f-4cd4-a355-e364a43782e9]   User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."deleted_at" IS NULL AND "users"."id" = $1 LIMIT $2  [["id", 7], ["LIMIT", 1]]
[ActiveJob] [MessageBroadcastJob] [a464e290-f23f-4cd4-a355-e364a43782e9]   Rendered messages/_message.html.erb (16.6ms)
[ActiveJob] [MessageBroadcastJob] [a464e290-f23f-4cd4-a355-e364a43782e9] Performed MessageBroadcastJob from Async(default) in 28.37ms

And since the last line of the log says Performed MessageBroadcastJob from Async(default) it leads me to believe that the problem doesn't exist on the server side, and that perhaps my coffee script is broken somehow.

Maybe the way you insert new elements into a UL is different to just appending HTML onto a DIV?

Please help.

Ash
  • 24,276
  • 34
  • 107
  • 152

1 Answers1

0

The problem had to do with the _message.html.erb partial view, which was doing a comparison on the 'current_user' from Devise. I guess the WebSocket being stateless doesn't have the same range of access to the user's session data that the view would have under regular conditions. To compensate I added data attributes tot he relevant DOM elements and created a special JavaScript function to apply the differentiating styles and the transform the data into the desired way. It seems to work flawlessly now.

Ash
  • 24,276
  • 34
  • 107
  • 152