how to find model in ActionCable `subscribed` method - ruby-on-rails

I've seen a lot of tutorials do something like this with the subscribed method:
class MessagesChannel < ApplicationCable::Channel
def subscribed
conversation = Conversation.find(params[:id])
stream_from "conversation_#{conversation.id}"
end
end
The idea being to allow for multiple conversations between multiple users. But I'm not clear on how that id param is sent to the method. If my routes are nested so that the conversation id is in the url, it seems like it should work.
resources :conversations, only: [:index, :create] do
resources :messages, only: [:index, :create]
end
However, the channel code above gives this error:
[ActionCable] [user] Registered connection (Z2lkOi8vZnJhY3Rpb25jbHViL1VzZXIvMQ)
[ActionCable] [user] Could not execute command from ({"command"=>"subscribe", "identifier"=>"{\"channel\":\"MessagesChannel\"}"}) [ActiveRecord::RecordNotFound - Couldn't find Conversation without an ID]: /Users/user/.rvm/gems/ruby-2.5.0/gems/activerecord-5.2.0/lib/active_record/relation/finder_methods.rb:431:in `find_with_ids' | /Users/user/.rvm/gems/ruby-2.5.0/gems/activerecord-5.2.0/lib/active_record/relation/finder_methods.rb:69:in `find' | /Users/user/.rvm/gems/ruby-2.5.0/gems/activerecord-5.2.0/lib/active_record/querying.rb:5:in `find' | /Users/user/.rvm/gems/ruby-2.5.0/gems/activerecord-5.2.0/lib/active_record/core.rb:167:in `find' | /Users/user/code/project/app/channels/messages_channel.rb:3:in `subscribed'
How would I pass the conversation id to the subscribed method so that my users can have multiple private conversations?
Update 1: This is my messages.coffee file
App.messages = App.cable.subscriptions.create
channel: "MessagesChannel"
conversation_id: 1
connected: ->
console.log 'Connected'
disconnected: ->
console.log 'Disconnected'
received: (data) ->
console.log 'Received'
$('#messages').append(data.message)
speak: (message, conversation_id) ->
#perform 'speak', message: message, conversation_id: conversation_id
$(document).on 'turbolinks:load', ->
submit_message()
scroll_bottom()
submit_message = () ->
$('#response').on 'keydown', (event) ->
if event.keyCode is 13
App.messages.speak(event.target.value)
event.target.value = ""
event.preventDefault()
scroll_bottom = () ->
$('#messages').scrollTop($('#messages')[0].scrollHeight)

This is how I am tackling this,
In my view form:
<%= form_for Message.new,remote: true,html: {id: 'new-message',multipart: true} do |f| %>
<%= f.hidden_field :chat_id, value: chat.id,id: 'chat-id' %>
....
Notice the id I have given to form and then chat_id field.
Then in my chat.js:
return App.chat = App.cable.subscriptions.create({
channel: "ChatChannel",
chat_id: $('#new-message').find('#chat-id').val()
}
Now I can use this chat_id param in my ChatChannel like this:
def subscribed
stream_from "chat_#{params['chat_id']}_channel"
end
EDIT:
In your case:
your MessagesChannel subscribed action should be like this:
def subscribed
stream_from "conversation_#{params[:conversation_id]}"
end

Related

ActionCable message saved but broadcast is ignored in after_create_commit

I'm using this ActionCable demo as a guideline for setting up user messaging, but I'm changing it so that there can be multiple private rooms between users. The message persists to the db through the speak method like so [ActionCable] [me] MessagesChannel#speak({"body"=>"eee", "conversation_id"=>1}) and then calls a background worker with an after_create_commit. The worker executes, but the broadcast is not broadcasting. What prevents that broadcast from executing?
2020-05-10T04:31:32.216Z pid=32378 tid=ouzv4m5h2 class=MessageBroadcastWorker jid=e8b23ada55bede39b811e770 INFO: start
2020-05-10T04:31:32.499Z pid=32378 tid=ouzv4m5h2 class=MessageBroadcastWorker jid=e8b23ada55bede39b811e770 elapsed=0.283 INFO: done
messages.coffee
if ($("meta[name='current-user']").length > 0)
App.messages = App.cable.subscriptions.create "MessagesChannel",
connected: ->
console.log 'Connected'
disconnected: ->
console.log 'Disconnected'
received: (data) ->
console.log 'Received'
speak: (body, conversation_id) ->
#perform 'speak', body: body, conversation_id: conversation_id
$(document).on 'turbolinks:load', ->
submit_message()
scroll_bottom()
submit_message = () ->
$('#response').on 'keydown', (event) ->
if event.keyCode is 13
conversation_id = $("#messages").data("conversation-id")
App.messages.speak(event.target.value, conversation_id)
event.target.value = ""
event.preventDefault()
scroll_bottom = () ->
if $('#messages').length > 0
$('#messages').scrollTop($('#messages')[0].scrollHeight)
messages_channel.rb
class MessagesChannel < ApplicationCable::Channel
def subscribed
#i have to change this so that the conversation id is a part of the stream but i'll do that later
stream_from "messages_channel"
end
def unsubscribed
stop_all_streams
end
def speak(data)
Message.create! body: data['body'], conversation_id: data['conversation_id'], user_id: current_user.id
end
end
message.rb
class Message < ApplicationRecord
after_create_commit { MessageBroadcastWorker.perform_async self.id }
end
message_broadcast_worker.rb
class MessageBroadcastWorker
include Sidekiq::Worker
def perform(message_id)
message = Message.find message_id
ActionCable.server.broadcast("messages_channel", message: render_message(message))
end
private
def render_message(message)
ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message, username: message.user.username })
end
end
views/messages/index.html.erb
<div id="messages" data-conversation-id="<%= #conversation.id %>">
<%= render #messages %>
</div>
<%= form_for [#conversation, #message], remote: true do |f| %>
<%= f.label :type_a_message %>
<%= f.text_area :body, placeholder: "Say something.", autofocus: true, id:"response" %>
<% end %>
views/messages/_message.html.erb
<% cache message do %>
<div class="message">
<div><strong><%= message.user.username %>:</strong> <%= message.body %></div>
<div class="date"><%= local_time(message.created_at) %></div>
</div>
<% end %>
i had two lines commented out in my config/cable.yml:
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
adding those back in brought my app to life

DOMstringMap for ApplicationChannel

I'm making a Chat App using ActionCable and websocket.
But I cannot get room_id in asset/javascripts/channels/rooms.coffee
I want to get it like messages.data('room-id')
could you tell me the reason?
asset/javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
console.log(messages.data('room-id'))
if $('#messages').length > 0
App.room = App.cable.subscriptions.create {
channel: "RoomChannel"
room_id: messages.data('room-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']
speak: (message, room_id) ->
#perform 'speak', message: message, room_id: room_id
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
if event.keyCode is 13 # return/enter = send
App.room.speak event.target.value
event.target.value = ''
event.preventDefault()
/view/rooms/show.html
<h1><%= #room.title %></h1>
<div id="messages", data-room-id="<%= #room.id %>">
<%= render #room.messages %>
</div>
<form>
<label>Say something:</label><br>
<input type="text" data-behavior="room_speaker">
<hidden> fa</hidden>
</form>
/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_#{params[:room_id]}_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
current_user.messages.create!(content: data['message'], room_id: data['room-id'])
end
end
I have wrong space in /view/rooms/show.html
<div id="messages", data-room-id="<%= #room.id %>">
<div id="messages", data-room-id="<%= #room.id %>">
this is correct.

Message username undefined for ActionCable chat app

I am following the Learn Enough Action Cable tutorial. I have gotten to the end of section 6, adding #mentions to create notifications to the mentioned user. One of the exercises is to append the sending user's name to the alert text. So far I am only getting "#undefined". I'm guessing data.mention.username is not the correct call to append. In the console to pull the username from a message I did User.find_by(id: Message.last.user_id).username, but I don't know how to translate that to working coffeescript.
room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
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) ->
alert("You have a new mention from #" + data.mention.username) if data.mention
if (data.message && !data.message.blank?)
$('#messages-table').append data.message
scroll_bottom()
$(document).on 'turbolinks:load', ->
submit_message()
scroll_bottom()
submit_message = () ->
$('#message_content').on 'keydown', (event) ->
if event.keyCode is 13 && !event.shiftKey
$('input').click()
event.target.value = ""
event.preventDefault()
scroll_bottom = () ->
$('#messages').scrollTop($('#messages')[0].scrollHeight)
messages_controller.rb
class MessagesController < ApplicationController
before_action :logged_in_user
before_action :get_messages
def index
end
def create
message = current_user.messages.build(message_params)
if message.save
ActionCable.server.broadcast 'room_channel',
message: render_message(message)
message.mentions.each do |mention|
ActionCable.server.broadcast "room_channel_user_#{mention.id}",
mention: true
end
end
end
private
def get_messages
#messages = Message.for_display
#message = current_user.messages.build
end
def message_params
params.require(:message).permit(:content)
end
def render_message(message)
render(partial: 'message', locals: { message: message })
end
end
message.rb
class Message < ApplicationRecord
belongs_to :user
validates :content, presence: true
scope :for_display, -> { order(:created_at).last(50) }
# Returns a list of users #mentioned in message content.
def mentions
content.scan(/#(#{User::NAME_REGEX})/).flatten.map do |username|
User.find_by(username: username)
end.compact
end
end
room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
stream_from "room_channel_user_#{message_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Have to define the username within the create method of messages controller. So the create method in messages_controller.rb looks something like:
def create
message = current_user.messages.build(message_params)
if message.save
ActionCable.server.broadcast 'room_channel',
message: render_message(message)
message.mentions.each do |mention|
ActionCable.server.broadcast "room_channel_user_#{mention.id}",
mention: true,
origin: "##{message.user.username}"
end
end
end
And the room.coffee alert calls data.origin like so:
received: (data) ->
alert("You have a new mention from " + data.origin) if data.mention
if (data.message && !data.message.blank?)
$('#messages-table').append data.message
scroll_bottom()
Thanks to LIUSINING of LearnEnough Society for pointing me in the right direction.

Action cable not working I have to refresh both the tab to see message

Here is code of room.coffee :
App.room = App.cable.subscriptions.create "RoomChannel",
connected: ->
disconnected: ->
received: (data) ->
$('#messages').append "<p>#{data}</p>"
speak: (message) ->
#perform 'speak' , message: message
cable.coffee :
#App ||= {}
App.cable = ActionCable.createConsumer()
rooms.coffee:
$ ->
$messages = $('messages')
$messages.scrollTop $messages.prop('scrollHieght')
$('#message_input').focus()
$(document).on 'keypress','message_input','e'->
if e.keycode == 13 and e.target.value
App.room.speak(e.target.value)
e.target.value = ''
e.preventDefault()
roomchannel:
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
message.create content: data['message']
end
end
Broadcostmessage:
def perform(message)
Actioncable.server.broadcast 'room_channel',render_message(message)
end
private
def render_message(message)
ApplicationController.renderer.render_message
end
when create the new message it will not automatically load all the messages of my browser untill the page in not reload.
I had a similar issue. Try including cable.coffee/js in the room view.
<%= javascript_include_tag 'cable', 'data-turbolinks-track': 'reload' %>
You'll want to add cable.js to /config/initializers/assets.rb as well.
Rails.application.config.assets.precompile += %w( cable.js )
This old question that came up in a Google search, but did anyone mention the typo in:
$messages.scrollTop $messages.prop('scrollHieght')
That should be:
$messages.scrollTop $messages.prop('scrollHeight')

Rails 5 action cable with nobrainer not working properly

I tried action cable example from https://github.com/rails/actioncable-examples. It worked well with ActiveRecord. I am getting ActiveJob::SerializationError (Unsupported argument type: Comment) when using with NoBrainer for rethinkDB.
So I just passed id instead of self like below for activejob. But action cable server is not able to listen to the any channels and CommentsChannel methods are not invoked.
comment.rb
after_save { CommentRelayJob.perform_later(self.id) }
*****comment_relay_job.rb*****
def perform(comment_id)
comment = Comment.find(comment_id)
ActionCable.server.broadcast "messages:#{comment.message_id}:comments",
comment: CommentsController.render(partial: 'comments/comment', locals: { comment: comment }
end
comments_channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base; end
end
class CommentsChannel < ApplicationCable::Channel
def follow(data)
stop_all_streams
stream_from "messages:#{data['message_id'].to_i}:comments"
end
def unfollow
stop_all_streams
end
end
comments.coffee
App.comments = App.cable.subscriptions.create "CommentsChannel", collection: -> $("[data-channel='comments']")
connected: ->
setTimeout =>
#followCurrentMessage()
#installPageChangeCallback()
, 1000
received: (data) ->
#collection().append(data.comment) unless #userIsCurrentUser(data.comment)
userIsCurrentUser: (comment) ->
$(comment).attr('data-user-id') is $('meta[name=current-user]').attr('id')
followCurrentMessage: ->
if messageId = #collection().data('message-id')
#perform 'follow', message_id: messageId
else
#perform 'unfollow'
installPageChangeCallback: ->
unless #installedPageChangeCallback
#installedPageChangeCallback = true
$(document).on 'page:change', -> App.comments.followCurrentMessage()

Resources