ActionCable message saved but broadcast is ignored in after_create_commit - ruby-on-rails

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

Related

Typing indicator with action cable in rails- 7

I am unable to add typing indicator in my rails app with action cable I have created app in rails 7 and I user trubo stream tag and broadcast in it so I did't used channel for live chat , I tried to find tutorial and video but there is not any
I want to add typing indicator so I writtern js for the same on input it will called and it will go to controller
On input I am calling controller "rtm"
room controller
def rtm
#typing = "hhhhhhhhhhhhhhhhhhh"
# ActionCable.server.broadcast "typing_channel",{ message: "helloo"}
# #typings.broadcast_append_to "typing"
Turbo::StreamsChannel.broadcast_append_to "typing", target: 'typing', partial: 'rooms/typing', locals: { message: "#typing" }
end
here I have issue how can I broadcast the typing message to my room page
Room.rb
class Room < ApplicationRecord
scope :public_rooms, -> { where(is_private: false) }
has_many :messages
after_create_commit {broadcast_append_to "rooms"}
end
message.rb
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
after_create_commit { broadcast_append_to self.room }
end
rooms/index
<script>
$(document).ready(function(){
var tmo = null;
$("#msg").on("input", function(){
$.ajax({
type: 'GET',
url: '/rooms/rtm',
data: {data: ''}
});
document.getElementById("typing").innerHTML = "Typing...";
if (tmo) {
clearTimeout(tmo);
}
tmo = setTimeout(function () {
$.ajax({
type: 'GET',
url: '/rooms/rmm',
data: {data: ''}
});
document.getElementById("typing").innerHTML = "";
}, 1000);
});
});
</script>
<div class="container">
<h5> Hi <%= current_user&.firstname %> </h5>
<%= debug(params) if Rails.env.development? %>
<br> <h4> Rooms </h4>
<%= render partial: 'layouts/new_room_form' %>
<%= turbo_stream_from "rooms" %>
<div id="rooms">
<%= render #rooms %>
</div>
</div>
<% if #single_room.present? %>
<%= link_to #single_room.name,#single_room, class: "btn btn-primary" %>
<%= turbo_stream_from #single_room %>
<div id="messages">
<%= render #messages %>
</div>
<%= render partial: 'layouts/new_message_form' %>
<%= #typing %>
<%= turbo_stream_from #typing %>
<div id="typing">
</div>
<%= render partial: 'rooms/typing' %>
<span id="typing"></span><br>
<% end %>
To get the typing indicator you need to use action cable and create a channel for it. You can use turbo stream to render the typing indicator. Example:
app/channels/typing_channel.rb
class TypingChannel < ApplicationCable::Channel
def subscribed
stream_from "typing_channel"
end
end
app/javascript/channels/typing_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("TypingChannel", {
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
}
});
app/views/rooms/index.html.erb
<div id="typing">
<%= turbo_stream_from "typing_channel" %>
</div>
app/views/rooms/_typing.html.erb
<p><%= message %></p>
app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
def rtm
ActionCable.server.broadcast "typing_channel", { message: "helloo" }
end
end
app/javascript/controllers/rooms_controller.js
import { Controller } from "stimulus"
import consumer from "../channels/consumer"
export default class extends Controller {
static targets = [ "input" ]
connect() {
this.subscription = consumer.subscriptions.create("TypingChannel", {
received: (data) => {
this.renderTyping(data)
}
})
}
renderTyping(data) {
const typing = document.getElementById("typing")
typing.innerHTML = data.message
}
disconnect() {
this.subscription.unsubscribe()
}
}
Is not possible to use turbo stream with action cable. You need to use action cable to get the typing indicator. You can use turbo stream to render the typing indicator.
typing_channel.js
import consumer from "channels/consumer"
consumer.subscriptions.create("TypingChannel", {
connected() {
console.log("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) {
console.log(data)
const box = document.getElementById('typing');
if (box.textContent.includes(data.body)) {
} else {
$('#typing').append('<p>'+ data.body + data.message +'</p>');
}
}
});
I am using below js for indicator
<script>
$(document).ready(function(){
var tmo = null;
$("#chat").on("input", function(){
$.ajax({
type: 'GET',
url: '/rooms/rtm',
});
if (tmo) {
clearTimeout(tmo);
}
tmo = setTimeout(function () {
$.ajax({
type: 'GET',
url: '/rooms/rmm',
});
}, 1000);
});
});
</script>
in controller
ActionCable.server.broadcast "typing_channel", {message: 'Typing', body: "#{current_user.email}"}

Rails 5 Subscribing to Action Cable channel from ajax request

I've been trying to show all conversation messages without refreshing page just with an ajax request. With this code i'm able to send message with channel but i'm not able to receive the message.
I would like to pass dynamically ID from frontend. I know it's possible to make that with a data parameter added into the body tag (like this).
I would like make the same but when i click on a conversation that change body data "conversation-id" and subscribe to the new channel id. I hope it's possible to make that with rails 5/jquery(ajax).
Here's my code :
conversation.coffee
App.conversation = App.cable.subscriptions.create channel: "ConversationChannel", id: document.querySelector('#conversation').dataset.conversationId,
connected: ->
$('#new_message').submit (e) ->
$this = $(this)
id = $this.find('#message_conversation_id');
body = $this.find('#message_body')
App.conversation.write id.val(), body.val()
body.val('')
e.preventDefault()
return false
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
$('.box-msg').append(data.data)
write: (id, body) ->
#perform 'write', {id: id, body: body}
conversation_channel.rb
class ConversationChannel < ApplicationCable::Channel
def subscribed
id = "[HOW CAN I PASS DYNAMICALLY ID]"
if id #on stream seulement si id existe
stream_from "conversation_#{id}"
end
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def write(data)
msg = Message.create(
conversation_id: data["id"],
body: data['body'],
user_id: 41,
read: 0
)
if msg.save
html = ApplicationController.render(partial: 'messages/message', locals: { message: msg})
end
ActionCable.server.broadcast "conversation_#{data["id"]}", data: html
end
end
conversations/index.html.erb
<script>
$(document).ready(function() {
$('.card-conversation').click(function() {
$("#conversation-content").html('');
var id = $(this).data("conversation");
$("body").attr("data-conversation-id", id);
$("#message_conversation_id").val(id); //input id
$.ajax({
url: "/conversations/"+id+"/messages",
cache: false,
success: function(html){
$("#conversation-content").append(html);
}
});
$("#conversation-content").fadeIn();
});
});
</script>
<div class="row h-100 no-margin">
<div class="col-md-4 h-100 messages-list" style="padding-right: 2px !important;">
<% #conversations.first(3).each do |conversation| %>
<div class="card card-conversation" data-conversation="<%=conversation.id %>">
<p>see conversation</p>
</div>
<% end %>
</div>
<div class="col-md-8 conversation no-padding" id="conversation">
<div id="conversation-content"></div>
<%= simple_form_for Message.new, url: "#" do |f| %>
<%= f.input :body, label: false, placeholder: "Add msg" %>
<%= f.input :conversation_id, label: false %>
<%= f.submit 'Go' %>
<% end %>
</div>
</div>

Using multiple Stripe checkout buttons, all items use last items price and name on rails

I'm working on a simple store front, and wanted to use stripe checkout buttons for the items. I have a DB of items that I'm iterating through, and generating payment buttons for with JS, here's that code:
<%= form_tag charges_path, id: 'stripeForm' do -%>
<script src="https://checkout.stripe.com/checkout.js"></script>
<%= hidden_field_tag 'stripeToken' %>
<%= hidden_field_tag 'stripeEmail' %>
<%= hidden_field_tag 'stripeAmount' %>
<p>
<% puts p.name %>
<% #price= p.price %>
Buy <% p.name %> for <% number_to_currency(#price, :unit=> '$') %>
<button id="<%=p.id %>">Purchase: <%=p.name%></button>
</p>
<script>
var handler = StripeCheckout.configure({
key: '<%= Rails.configuration.stripe[:publishable_key] %>',
image: 'https://stripe.com/img/documentation/checkout/marketplace.png',
locale: 'auto',
token: function(token, args) {
$("#stripeToken").val(token.id);
$("#stripeEmail").val(token.email);
$("#stripeAmount").val('<%= #price %>');
$("#stripeForm").submit();
}
});
document.getElementById('<%= p.id %>').addEventListener('click', function(e) {
handler.open({
name: 'Malus Cider',
description: '<%=p.name%>',
shippingAddress:true,
billingAddress:true,
amount: <%= #price %>
});
e.preventDefault();
});
window.addEventListener('popstate', function() {
handler.close();
});
</script>
<% end %>
<% end %>
my controller:
def create
# Amount in cents
amount = params[:stripeAmount].to_i * 100
puts amount
# Create the customer in Stripe
customer = Stripe::Customer.create(
email: params[:stripeEmail],
source: params[:stripeToken]
)
# Create the charge using the customer data returned by Stripe API
charge = Stripe::Charge.create(
customer: customer.id,
amount: amount,
description: 'Rails Stripe customer',
currency: 'usd'
)
end
private
def product_params
params.require(:product).permit(:id, :name, :price)
end
this works! Except that at the last second the hidden field values change to the last item in the DB. So it almost works. Any ideas?

How to display a selection based on user input using ajax and jquery

Think of the below as a bike rental. Someone fills out a form and gets a bike assigned to them which they can rent and borrow for a certain amount of time.
The problem I am having is I am trying to show the person who wants to rent the bikes what bikes are available before they submit the form. Below is my attempt using ajax. I have no errors but also my select is not updating.
request controller methods below
def new
#bikes = Bike.available_based_on_request_date(params[:Borrow_date], params[:Return_date])
#new_request = Request.new
end
create method below (with a temporary workaround, that reloads the form with a warning about availability.)
def create
#request = Request.new(request_params)
available_bikes = #request.new_request(current_user.id)
if (available_bikes >= #request.number_of_bikes_wanted) && #request.save
redirect_to root_path
else
flash[:warning] = "You have requested more bikes than available. There are only #{available_bikes} bikes available"
redirect_to new_request_url
end
end
params in request controller
def request_params
params.require(:request).permit(:Borrow_time, :Borrow_date,
:Return_date, :Return_time,
:number_of_bikes_wanted, bike_ids: [])
end
new.html.erb view
<div class="form" align = "center">
<%= render 'form.js.erb' %>
</div>
_form.js.erb below
<script type="text/javascript">
$(document).ready(function() {
$('.my-date').on('change', function() {
var data = {}
$('.my-date').each(function() {
if($(this).val()) {
data[$(this).attr("id")] = $(this).val();
}
});
if(Object.keys(data).length > 1) {
$.ajax({
type: "POST",
url: <%= new_request_path %>,
data: data
});
}
});
});
var options = "";
<% #bikes.each do |bike| %>
options += "<option value='<%= bike.id %>'><%= bike.name %></option>"
<% end %>
$('#request_number_of_bikes_wanted').html(options);
</script>
<div class="block-it" align=center>
<br>
<%= form_for #new_request do |request| %>
<%= request.label :Borrow_date, 'Borrow on' %>
<%= request.date_field :Borrow_date, id: 'Borrow_date', class: 'my-date', min: Date.today, :required => true %>
<%= request.label :Borrow_time, 'Borrow at' %>
<%= request.time_field :Borrow_time, value: '10:00', min: '9:00 AM', max: '4:30 PM', default: '10:00 AM', :ignore_date => true, :required => true %>
<br><br>
<%= request.label :Return_date, 'Return On' %>
<%= request.date_field :Return_date, id: 'Return_date', class: 'my-date', min: Date.today, :required => true %>
<%= request.label :Return_time, 'Return at' %>
<%= request.time_field :Return_time, value: '10:00', min: '9:00 AM', max: '4:30 PM', default: '10:00 AM', :ignore_date => true, :required => true %>
<br><br>
<br><br>
<%= request.label :NumberOfBikesWanted, 'Number of bikes' %>
<%= request.select :number_of_bikes_wanted, %w(select_bike), :required => true %>
<br>
<%= request.submit 'Submit' %>
<%= request.submit 'Reset', :type => 'reset' %>
<% end %>
<br>
</div>
There are a two main problems with your code:
Controller
Use a different action to set the endpoint that you will call with ajax, so instead of this:
def new
#bikes = Bike.available_based_on_request_date(params[:Borrow_date], params[:Return_date])
#new_request = Request.new
end
Try this:
def bikes
#bikes = Bike.available_based_on_request_date(params[:Borrow_date], params[:Return_date])
def new
#new_request = Request.new
end
If you want to keep REST routes, then create a new controller and use the index action within that controller.
Form
This code:
var options = "";
<% #bikes.each do |bike| %>
options += "<option value='<%= bike.id %>'><%= bike.name %></option>"
<% end %>
$('#request_number_of_bikes_wanted').html(options);
doesn't belong here, it must be deleted from your file and instead put it on a new file called bikes.js.erb; also rename your form to _form.html.erb.
And update your ajax call to use your new route:
$.ajax({
type: "POST",
url: <%= bikes_path %>,
data: data
});
What you want to setup is a new endpoint but instead of returning html, it will return a js. But you must treat it as an independent action, just as any other action in rails. The only difference is how you call that action (ajax) and how you respond to it (js).

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.

Resources