New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
action cable example with nested comments #1
Comments
Hi,
I’ve just created balakirevs branch for your actioncable codes and pushed to the remote GitHub.
But I’m sorry it doesn’t work.
Would you check your codes for whether typos exist on this branch(balakirevs)?
https://github.com/luciuschoi/vagrant-test/tree/balakirevs <https://github.com/luciuschoi/vagrant-test/tree/balakirevs>
Thank you for your contribution.
… On 17 Jun 2018, at 4:58 AM, Aleksandr Balakiriev ***@***.***> wrote:
Hi,
Thank you for the app example, very useful.
Trying implement the same but with action cable to make it more live. It works though but the issue is doubled comments displayed. I've added the following code to your existing app:
======
js/channel/comments.js
App.comments = App.cable.subscriptions.create('CommentsChannel', {
collection: function() {
return $("[data-channel='comments']");
},
connected: function() {
return setTimeout((function(_this) {
return function() {
return _this.followCurrentPost();
};
})(this), 1000);
},
received: function(data) {
if (!this.userIsCurrentUser(data.comment)) {
return this.collection().append(data.comment);
}
},
userIsCurrentUser: function(comment) {
return $(comment).attr('data-user-id') === $('meta[name=current-user]').attr('id');
},
disconnected: function() {},
followCurrentPost: function() {
var commentableId;
commentableId = this.collection();
if (commentableId = this.collection().data('commentable-id')) {
return this.perform('follow', {
commentable_id: commentableId
});
} else {
return this.perform('unfollow');
}
},
installPageChangeCallback: function() {
if (!this.installedPageChangeCallback) {
this.installedPageChangeCallback = true;
return $(document).on('turbolinks:load', function() {
return App.comments.followCurrentPost();
});
}
}
});
=============
channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.email
end
protected
def find_verified_user
verified_user = User.find_by(id: cookies.signed['user.id'])
if verified_user && cookies.signed['user.expires_at'] > Time.now
verified_user
else
reject_unauthorized_connection
end
end
end
end
===============
channels/comments_channel.rb
class CommentsChannel < ApplicationCable::Channel
def follow(data)
stop_all_streams
stream_from "#{data['commentable_id'].to_i}:comments"
end
def unfollow
stop_all_streams
end
end
==================
jobs/broadcasts_jobs.rb
class BroadcastCommentJob < ApplicationJob
queue_as :default
def perform(comment)
ActionCable.server.broadcast "#{comment.commentable_id}:comments",
comment: render_comment(comment)
end
private
def render_comment(comment)
ApplicationController.render(partial: 'comments/comment', locals: { comment: comment } )
end
end
=========
comment.rb
after_create_commit { BroadcastCommentJob.perform_now self }
===========
views/comments/_comment.html.erb
<%= comment.content %>
- <%= comment.user.name %> • <%= localize(comment.created_at, format: :long) %> • <%= link_to "Edit", edit_polymorphic_path([comment.commentable, comment]), class: 'edit-comment-link', remote: true %> • <%= link_to "Destroy", [comment.commentable, comment], method: :delete, class: 'delete-comment-link', data:{confirm:"Are your sure?"}, remote: true %> • <%= link_to "Reply", polymorphic_path([:reply, comment.commentable, comment]), class: 'reply-comment-link', remote: true %>
<% if comment.replies.any? %>
<%= render comment.replies %>
<% end %>
==============
views/comments/_widget.html.erb
Comments <%= commentable.comments.size %>
<%= render commentable.comments.where(parent: nil) %>
<%= render "comments/form", comment: commentable.comments.build %>
=========
application.html.erb
<title>RORLA-Blog</title> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <%= favicon_link_tag 'rorlab_logo.png' %> <% if user_signed_in? %> <%= tag :meta, name: 'current-user', data: { id: current_user.id } %> <% end %>
<%= render "shared/header" %>
<%= flash_messages(:close) %>
<%= yield %> <%= render "shared/sidebar" %>
<%= render 'shared/footer' %>
==============
routes.rb
mount ActionCable.server => '/cable'
======
cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
==========
Gemfile
gem 'redis'
================
config/initializers/warden_hooks.rb
Warden::Manager.after_set_user do |user, auth, opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
auth.cookies.signed["#{scope}.expires_at"] = 60.minutes.from_now
end
Warden::Manager.before_logout do |_user, auth, opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil
auth.cookies.signed["#{scope}.expires_at"] = nil
end
===================
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub <#1>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AHVi1fbYUKz8Yw_DeASUfBBG5gdd0NODks5t9WNOgaJpZM4UqmHi>.
|
Hi, thank you for reply.
class="comment" data-user-id="<%= comment.user.id %>"
data-channel='comments' data-commentable-id="<%= commentable.id %>"> |
Oh~ It works. Is that right? |
Correct. Currently i have not found how to fix it. |
OK, I'll also try to find how to fix it. |
Thank you too. |
Voila~ finally, it works. Bugs were disappeared.
But this is only for creating commens or replies but not for editing and destroying. Maybe, this is also possible on adding codes. Good luck! |
Oh, I've also updated |
Nice! Thank you! |
I'm sorry that I've updated your markdown codes in your comments without your permission for pretty viewing. |
:) it became much nicer. |
I think we should add also that user can edit destroy his own comments not others, as well as if parent comment has replies we should not update or destroy it any more... If you do not mind i will provide it a bit later or what do you think? |
Oh~ What a wonderful idea! |
As you mentioned, it was so easy to disable to update or destroy a comment or a reply if it had already replies. In <% if comment.replies.size.zero? %>
<%= link_to "Edit", edit_polymorphic_path([comment.commentable, comment]), class: 'edit-comment-link', remote: true %> •
<%= link_to "Destroy", [comment.commentable, comment], method: :delete, class: 'delete-comment-link', data:{confirm:"Are your sure?"}, remote: true %> •
<% end %> |
A redundant code was found: commentableId = this.collection();
if (commentableId = this.collection().data('commentable-id') { ···· } Finally, codes are as follows: followCurrentPost: function () {
var commentableId;
commentableId = this.collection().data('commentable-id');
if (commentableId) {
return this.perform('follow', {
commentable_id: commentableId
});
} else {
return this.perform('unfollow');
}
}, Is that right? |
Ah yes, genius as for disabling the links! $edit_link = $comment.find('a.edit-comment-link')[0] |
sure! sorry for the redundancy. |
Yes, we should do. |
Yeap!!! |
Thanks @luciuschoi!
gem 'cancancan', '~> 2.0' rails g cancan:ability in the generated class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
can [:edit, :update, :destroy], Comment, user_id: user.id
can [ :read, :show, :create, :reply], Comment
end
end after before_action in load_and_authorize_resource :comment, param_method: :comment_params
<span class='dot-bullet'><%= link_to "Edit", edit_polymorphic_path([comment.commentable, comment]), class: 'edit-comment-link', remote: true if can? :update, comment %></span>
<span class='dot-bullet'><%= link_to "Destroy", [comment.commentable, comment], method: :delete, class: 'delete-comment-link', data:{confirm:"Are your sure?"}, remote: true if can? :destroy, comment %></span> now user can edit destroy only his own comments not others. To do fix: currently we need to refresh page in order to see edit destroy links of user own comment. just came accros here with the problem of warden proxy bug due to devise (ActionView::Template::Error (Devise could not find the so we need to do some workaround:
class BroadcastCommentJob < ApplicationJob
before_perform :wardenize
queue_as :default
def perform(comment)
ActionCable.server.broadcast "#{comment.commentable_id}:comments",
comment: render_comment(comment), parent_id: comment&.parent&.id
end
private
def render_comment(comment)
@job_renderer.render(partial: 'comments/comment', locals: { comment: comment })
end
def wardenize
@job_renderer = ::ApplicationController.renderer.new
renderer_env = @job_renderer.instance_eval { @env }
warden = ::Warden::Proxy.new(renderer_env, ::Warden::Manager.new(Rails.application))
renderer_env['warden'] = warden
end
end
remove after_create_commit { BroadcastCommentJob.perform_now self } in if @comment.save
BroadcastCommentJob.perform_now @comment |
sure, notification to be continued... |
Notification (after creating a comment - notifying users who participating in current post discussion (comments)):
gem 'activity_notification'
gem 'js_cookie_rails' // we need it to get current user cookie id for action cable
//= require js.cookie create devise controller app/controllers/users/sessions_controller.rb class Users::SessionsController < Devise::SessionsController
def create
super
cookies[:current_user_id] = current_user.id
end
end generating activity_notification necessary files: $ bin/rails generate activity_notification:install
$ bin/rails generate activity_notification:migration
$ bin/rake db:migrate
$ bin/rails generate activity_notification:views -v notifications
$ bin/rails generate activity_notification:views users if you have problems with migration just add [5.1] at the end of the CreateActivityNotificationTables migration file: class CreateActivityNotificationTables < ActiveRecord::Migration after class CreateActivityNotificationTables < ActiveRecord::Migration[5.1] add in acts_as_target email: :email, batch_email_allowed: :confirmed_at add in has_many :commented_users, through: :comments, source: :user add in belongs_to :post, optional: true
...
acts_as_notifiable :users,
targets: ->(comment, key) {
([comment.commentable.user] + comment.commentable.commented_users.to_a - [comment.user]).uniq
},
notifiable_path: :post_notifiable_path,
notifier: :user,
group: :commentable,
tracked: { only: [:create] }
def post_notifiable_path
post_path(post)
end
devise_for :users, controllers:{ sessions: 'users/sessions' }
notify_to :users, with_devise: :users
<div id="notifications_in_header">
<%= render_notifications_of current_user, index_content: :unopened_with_attributes %>
</div> in newly generated by gem folder 'activity_notification' create a file: <%= link_to notification.notifier.name + ' commented on your post', post_path(notification.group) %> create directory comment and _create.html.erb file views/activity_notification/notifications/users/comment/_create.html.erb <%= link_to notification.target.name + ' commented on your post', post_path(notification.group) %>
def create
@comment = @commentable.comments.new(comment_params)
@comment.user = current_user
respond_to do |format|
if @comment.save
BroadcastCommentJob.perform_now @comment
ActivityNotification::Notification.notify :users, @comment, key: "comment.create", notifier: @comment.user, group: @commentable
notification_targets(@comment, "comment.create" ).each do |target_user|
BroadcastNotificationJob.perform_later target_user, @comment
end
format.js
else
format.html { render :back, notice: "Comment was not created." }
format.json { render json: @comment.errors }
format.js
end
end
end
private
def notification_targets comment, key
([comment.commentable.user] + comment.commentable.commented_users.to_a - [comment.user]).uniq
end create a job for notification
class BroadcastNotificationJob < ApplicationJob
queue_as :default
def perform(user, comment)
ActionCable.server.broadcast "notification_channel_#{user.id}",
notifications: render_notifications_for(user),
message: user.name + ' commented on your post',
link: 'posts/' + comment.commentable.id.to_s
end
private
def render_notifications_for user
ApplicationController.renderer.render partial: "activity_notification/socket_notifications",
locals: {user: user}
end
end finally lets add action cable to have live notifications:
App.notifications = App.cable.subscriptions.create(
{channel: 'NotificationChannel', user_id: Cookies.get('current_user_id')},
{
connected: function () {},
disconnected: function () {},
received: function (data) {
console.log('new notification');
$('#notifications_in_header').empty().html(data.notifications);
$('.dropdown_notification').dropdown();
}
});
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stop_all_streams
stream_from "notification_channel_#{params[:user_id]}"
end
def unsubscribed
stop_all_streams
end
end
<%= action_cable_meta_tag %> Now you should receive notifications of newly arrived comments. P.S. we can further experiment with it by doing other custom solutions or by using other gems like public_activity which is almost the same. Otherwise there are plenty of good screencasts of notifications: Thanks! |
<span class='dot-bullet'><%= link_to "Edit", edit_polymorphic_path([comment.commentable, comment]), class: 'edit-comment-link', remote: true if can? :update, comment %></span>
<span class='dot-bullet'><%= link_to "Destroy", [comment.commentable, comment], method: :delete, class: 'delete-comment-link', data:{confirm:"Are your sure?"}, remote: true if can? :destroy, comment %></span> This is NOT well-functioning. That is, when broadcasting after creating replies, their accessibility is already setup and so if condition is not functioning in _comment.html.erb partial. Do you understand what I mean? It is so difficult to explain this context in detail with my poor English. |
OK, may be that was the problem. |
Yeap!!! |
Thanks ! Will try to figure out... |
i think i found, in view/comments/_comment.html.erb try this: <%= render comment.replies, broadcasted: false %> i.e you should pass smth in comment.replies |
for the better performance i think we should replace in _widget.html.erb this <%= render partial: 'comments/comment', collection: commentable.comments.where(parent: nil), locals: { broadcasted: true } %> by this <%= render commentable.comments.includes(:user, :replies, :commentable).where(parent: nil).reverse, broadcasted: true %> i put reverse here as when i had lots of comments some of them appeared at the top... otherwise we can research this issue |
@klashe : "uninitialized constant ActivityNotification::Target::Association" error was already known. |
At the commit number |
Updated setup.sh and Vagrantfile to work well. |
Now, when you edit and destroy comments or replies already created, also they will be broadcasted to all subscribers. Commit number: 90bf1c48555a3aa1fa107509f415b051b8fd7117 |
You even went further :) Nice job! |
there is a small bug, when creating a comment a form completely disappears, needs page to be refreshed.... |
could you check please, in create.js.erb on line 20 remove this $comment_form.remove(); and put it somewhere after line 22 <% if @comment.parent.present? %>
...
$restore_link = $comment.find('a.delete-comment-link')[0];
$reply_link = $comment.find('a.reply-comment-link')[0];
$reply_link.href = $restore_link.href + "/reply";
$comment_form.remove(); |
Oh, I'm so sorry to be late. |
Now you feel better? |
@balakirevs : Where is the : views/activity_notification/_socket_notifications.html.erb <%= link_to notification.notifier.name + ' commented on your post', post_path(notification.group) %> Rendering error on performing BroadcastNotificationJob occurs.
|
could you try to put this : into views/activity_notification/_socket_notifications.html.erb And i will check a bit later why i put this : |
On this time, the error was disappeared but the number of notifications is not updated in real time. |
with this change i have notifications in real time... |
Hi,
Thank you for the app example, very useful.
Trying implement the same but with action cable to make it more live. It works though but the issue is doubled comments displayed & on reply incorrectly rendered i.e displayed as a comment but not a reply.
I've added the following code to your existing app:
js/channel/comments.js
:channels/application_cable/connection.rb
:channels/comments_channel.rb
:jobs/broadcasts_jobs.rb
:comment.rb
:views/comments/_comment.html.erb
:views/comments/_widget.html.erb
:application.html.erb
:routes.rb
:cable.yml
:Gemfile
:config/initializers/warden_hooks.rb
:The text was updated successfully, but these errors were encountered: