Skip to content
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

Live comment updates with ActionCable #2107

Merged
merged 53 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7c4e554
stage initial stuff
nimmolo Apr 18, 2024
0452256
try some more things
nimmolo Apr 18, 2024
8f80f6c
Update routes.rb
nimmolo Apr 18, 2024
19fabd7
Merge branch 'nimmo-field-slip-stimulus' into nimmo-actioncable
nimmolo Apr 18, 2024
e4be1a4
Merge branch 'nimmo-field-slip-stimulus' into nimmo-actioncable
nimmolo Apr 19, 2024
332e293
okay you actually do need connection.rb
nimmolo Apr 19, 2024
1d7d9df
setup ac server
nimmolo Apr 19, 2024
3a89aaf
Merge branch 'main' into nimmo-actioncable
nimmolo Apr 19, 2024
d6a51dd
Update Procfile.dev
nimmolo Apr 19, 2024
bcc537b
Merge branch 'nimmo-field-slip-stimulus' into nimmo-actioncable
nimmolo Apr 20, 2024
938d242
Merge branch 'nimmo-field-slip-stimulus' into nimmo-actioncable
nimmolo Apr 20, 2024
00854e4
No trackers. Try a different model
nimmolo Apr 20, 2024
72ed1f8
Hoowow, it works.
nimmolo Apr 20, 2024
dcf7cfd
Tidy up
nimmolo Apr 20, 2024
a32063f
not sure both turbo_stream_froms are necessary
nimmolo Apr 20, 2024
d3260f2
Ignore redis dumps
nimmolo Apr 20, 2024
2de4f17
Comment out explicit CRUD Turbo update actions in comments controller
nimmolo Apr 20, 2024
e5e830d
Merge branch 'main' into nimmo-actioncable
nimmolo May 1, 2024
f036bff
Restore bin/dev without dartsass:watch
nimmolo May 1, 2024
62f3777
Merge branch 'main' into nimmo-actioncable
nimmolo May 7, 2024
b9f5daf
Create connection_test.rb
nimmolo May 9, 2024
af92468
Update comment_test.rb
nimmolo May 9, 2024
f017286
Use puma for system tests
nimmolo May 10, 2024
bab47b3
obs show sys test: Split up naming and comment tests
nimmolo May 10, 2024
2f0ee02
Move importmap tags to the app/head partial
nimmolo May 10, 2024
5d4caa9
Closer, but "No template found for CommentsController#create"
nimmolo May 10, 2024
109e192
Update puma.rb
nimmolo May 10, 2024
e479dc6
Remove do...end from comments controller respond_to blocks
nimmolo May 10, 2024
935f695
Only destroy is working, not create or update
nimmolo May 11, 2024
8a3bc2b
Revert to broadcast from model. Show hide no comments with CSS
nimmolo May 11, 2024
b43d160
Turn on comment emails again (off for debugging)
nimmolo May 11, 2024
c9d2345
Alter config/puma.rb: try eliminating port in test
nimmolo May 12, 2024
bbbff52
Update observation_show_system_test.rb
nimmolo May 13, 2024
1c76350
Experimental config tweaks production.rb, cable.yml
nimmolo May 13, 2024
c092776
Move broadcast from model to controller
nimmolo May 14, 2024
bb4dcb9
Try to fix edit/delete controls
nimmolo May 14, 2024
2215500
Use css for controls
nimmolo May 15, 2024
556735f
Update _user_specific_css.html.erb
nimmolo May 15, 2024
4d33b67
Switch data-user-specific to user.id
nimmolo May 15, 2024
5568ead
Update capybara_integration_test_case.rb
nimmolo May 15, 2024
d8dfc11
Merge branch 'main' into nimmo-actioncable
nimmolo May 15, 2024
9bddc84
New system test for websocket
nimmolo May 15, 2024
96714ae
Update comments_integration_test.rb
nimmolo May 15, 2024
92af23b
Add path names for lookup routes
nimmolo May 15, 2024
d96d1d4
Update observation_comment_system_test.rb
nimmolo May 15, 2024
286acee
Delete comments_integration_test.rb
nimmolo May 15, 2024
d5c873b
Update comments_controller.rb
nimmolo May 15, 2024
a813d99
Move broadcast back to the model
nimmolo May 16, 2024
0ab9f2c
Update observation_comment_system_test.rb
nimmolo May 16, 2024
b4f5aa5
Update observation_comment_system_test.rb
nimmolo May 16, 2024
726a9eb
Update observation_comment_system_test.rb
nimmolo May 16, 2024
0cfcbb0
Merge branch 'main' into nimmo-actioncable
nimmolo May 16, 2024
2c4ae53
Update Gemfile
nimmolo May 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ checkpoints/
production-logs
.tm_properties
**/*.swp
dump.rdb

# Ignore bundler config.
.bundle
Expand Down
12 changes: 5 additions & 7 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ gem("prawn-svg")
gem("prawn")
gem("prawn-manual_builder")

# Use puma as the app server, also available for system tests
# To use Webrick locally, run `bundle config set --local without 'production'`
# https://stackoverflow.com/a/23125762/3357635
gem("puma")

########## Development, Testing, and Analysis ##################################
group :test, :development do
# https://github.com/ruby/debug
Expand Down Expand Up @@ -206,13 +211,6 @@ group :development do
# gem("rails_db", "~> 2.5.0", path: "../local_gems/rails_db")
end

group :development, :production do
# Use puma as the app server
# To use Webrick locally, run `bundle config set --local without 'production'`
# https://stackoverflow.com/a/23125762/3357635
gem("puma")
end

group :production do
# New Relic for application and other monitoring
# https://newrelic.com/
Expand Down
8 changes: 8 additions & 0 deletions app/assets/stylesheets/mo/_utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,14 @@
border-radius: 0;
}

.list-group-item.none-yet {
display: none;

&:only-child {
display: block;
}
}

.bg-none {
background: none !important;
}
Expand Down
6 changes: 6 additions & 0 deletions app/channels/application_cable/channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
35 changes: 35 additions & 0 deletions app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

# This is the connection class for the ActionCable websocket connection,
# responsible for setting the current_user for the connection. Any live updates
# over websockets need to be authenticated like regular requests, but they don't
# hit the regular controller actions, so we need to authenticate them
# separately. The current_user is set by checking the autologin cookie.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
self.current_user = find_verified_user
end

private

def find_verified_user
if (verified_user = validate_user_in_autologin_cookie)
verified_user
else
reject_unauthorized_connection
end
end

def validate_user_in_autologin_cookie
return unless (cookie = cookies["mo_user"]) &&
(split = cookie.split) &&
(user = User.where(id: split[0]).first) &&
(split[1] == user.auth_code)

user
end
end
end
28 changes: 8 additions & 20 deletions app/controllers/comments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
# update::
# destroy::
#
#
# rubocop:disable Metrics/ClassLength
class CommentsController < ApplicationController
before_action :login_required
# disable cop because index is defined in ApplicationController
Expand Down Expand Up @@ -284,11 +282,11 @@ def destroy
@comment.log_destroy
flash_notice(:runtime_form_comments_destroy_success.t(id: params[:id]))
end

respond_to do |format|
format.turbo_stream do
eager_load_target_comments
refresh_comments_for_object
end
# format.turbo_stream do
# helpers.render_turbo_stream_flash_messages
# end
format.html do
redirect_with_query(controller: @target.show_controller,
action: @target.show_action, id: @target.id)
Expand Down Expand Up @@ -331,15 +329,6 @@ def modal_title
end
end

def eager_load_target_comments
@comments = @target.comments&.includes(:user)&.
sort_by(&:created_at)&.reverse
end

def refresh_comments_for_object
render(partial: "comments/update_comments_for_object")
end

def permitted_comment_params
params[:comment].permit([:summary, :comment])
end
Expand All @@ -352,11 +341,11 @@ def reload_form
end

def refresh_comments_or_redirect_to_show
# Comment broadcasts are sent from the model
respond_to do |format|
format.turbo_stream do
eager_load_target_comments
refresh_comments_for_object
end
# format.turbo_stream do
# helpers.render_turbo_stream_flash_messages
# end
format.html do
redirect_with_query(controller: @target.show_controller,
action: @target.show_action, id: @target.id)
Expand Down Expand Up @@ -403,4 +392,3 @@ def comment_updated?
end
end
end
# rubocop:enable Metrics/ClassLength
4 changes: 4 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ def flash_notices_html
class: class_names("alert mt-3", alert_class))
end

def render_turbo_stream_flash_messages
turbo_stream.replace("flash", partial: "application/app/flash_notices")
end

# Returns a string that indicates the current user/logged_in/admin status.
# Used as a simple cache key for templates that may have three
# possible versions of cached HTML
Expand Down
7 changes: 4 additions & 3 deletions app/javascript/controllers/section-update_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ export default class extends Controller {
connect() {
this.element.dataset.stimulus = "connected";

// Note: this is simpler than adding an action on every frame.
// add event listener turbo:frame-render, call hide modal
// Note: this is simpler than adding an action on every frame. hides modal
this.element.addEventListener("turbo:frame-render", this.updated());
// this.element.addEventListener("turbo:before-stream-render", this.updated());
// this.element.addEventListener("turbo:submit-end", this.updated());
}

updated() {
// Remove modal which is present for certain updates (but not all)
// Must be in jQuery for Boostrap 3 and 4
$("#mo_ajax_progress").modal('hide');
document.getElementById('mo_ajax_progress_caption').innerHTML = "";
// console.log("Section updated");
console.log("Section updated");
// broadcast change
this.dispatch("updated");
}
Expand Down
4 changes: 4 additions & 0 deletions app/models/comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class Comment < AbstractModel
belongs_to :target, polymorphic: true
belongs_to :user

broadcasts_to(->(comment) { [comment.target, :comments] },
inserts_by: :prepend, partial: "comments/comment",
target: "comments")

after_create :notify_users
after_create :oil_and_water

Expand Down
5 changes: 4 additions & 1 deletion app/views/controllers/application/app/_head.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="turbo-refresh-scroll" content="preserve">
<%= action_cable_meta_tag %>
<%= auto_discovery_link_tag(
:rss, activity_logs_rss_path, { title: :app_rss.l }
) %>
Expand All @@ -26,7 +27,9 @@
<meta property="og:description" content="Mushroom Observer is a forum where amateur and professional mycologists can come together and celebrate their common passion for mushrooms by discussing and sharing photos of mushroom sightings from around the world."/>
<%=
stylesheet_link_tag(@css_theme, media: "screen") +
stylesheet_link_tag(@css_theme, media: "print")
stylesheet_link_tag(@css_theme, media: "print") +
render("application/app/user_specific_css")
%>
<%= javascript_importmap_tags %>
<%= @header unless @header.blank? %>
<!--/APPLICATION LAYOUT HEAD-->
20 changes: 20 additions & 0 deletions app/views/controllers/application/app/_user_specific_css.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<%#
show/hide controls per user.
This needs to be in a template to access the User.current method.

Use: `tag.div(data: { user_specific: User.current.id })`

source: https://discuss.hotwired.dev/t/how-to-pass-current-user-id-to-a-controller/287/3
%>

<% unless in_admin_mode? %>
<%# cache User.current || "no_user" do %>
<style>
<% if User.current %>
[data-user-specific]:not([data-user-specific="<%= User.current.id %>"]) { display: none; }
<% else %>
[data-user-specific] { display: none; }
<% end %>
</style>
<%# end %>
<% end %>
12 changes: 7 additions & 5 deletions app/views/controllers/comments/_comment.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ target_type = tag.span(target.class.name.to_sym.t, class: "small") \
rescue :runtime_object_deleted
%>

<%= tag.div(class: "list-group-item comment", id:"comment_#{comment.id}") do
<%# turbo_stream_from(target, :comment) %>

<%= tag.div(class: "list-group-item comment", id: dom_id(comment)) do
concat(tag.div(class: "row") do
[
tag.div(class: "col-xs-12 col-sm-9 col-lg-10") do
Expand All @@ -22,17 +24,17 @@ target_type = tag.span(target.class.name.to_sym.t, class: "small") \
concat(tag.span(class: "comment-author text-nowrap") do
["#{:BY.t}: ", user_link(user), " "].safe_join
end)
concat(tag.span(class: "text-nowrap") do
concat(tag.span(class: "text-nowrap",
data: { user_specific: comment.user.id }) do
[
"[",
modal_link_to("comment_#{comment.id}",
*edit_comment_tab(comment)),
modal_link_to(dom_id(comment), *edit_comment_tab(comment)),
"|",
destroy_button(name: :comment_show_destroy.t, target: comment,
icon: :delete, data: { turbo: true }),
"]",
].safe_join(" ")
end) if check_permission(comment)
end)
concat(tag.div(comment.created_at.web_time,
class: "float-sm-right text-nowrap small"))
end)
Expand Down
18 changes: 12 additions & 6 deletions app/views/controllers/comments/_comments_for_object.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
end
%>

<%= turbo_stream_from(object, :comments) %>
<%# NOTE: #comments is the inner list-group, not the whole panel %>
<%# It's a turbo-frame because section-update needs to listen to frame %>

<%=
tag.div(
class: "panel panel-default", id: "comments_for_object",
data: { controller: "section-update", updated_by: "modal_comment" }
class: "panel panel-default", id: "comments_for_object"
) do
concat([
tag.div(class: "panel-heading") do
Expand All @@ -18,10 +21,13 @@ tag.div(
class: "float-right")) if controls
end
end,
tag.div(class: "list-group list-group-flush comments") do
if comments.empty?
tag.div(:show_comments_no_comments_yet.t, class: "list-group-item")
else
tag.div(id: "comments",
class: "list-group list-group-flush comments",
data: { controller: "section-update",
updated_by: "modal_comment" }) do
concat(tag.div(:show_comments_no_comments_yet.t,
class: "list-group-item none-yet"))
unless comments.empty?
comments.each do |comment|
concat(render(partial: "comments/comment", object: comment))
end
Expand Down
1 change: 1 addition & 0 deletions app/views/controllers/comments/_form.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ end

<%= text_field_with_label(form: f, field: :summary, size: 80,
label: :form_comments_summary.t + ":",
autofocus: true, # only this has effect
data: { autofocus: true }) %>

<%= text_area_with_label(form: f, field: :comment, rows: 10,
Expand Down
14 changes: 0 additions & 14 deletions app/views/controllers/comments/_update_comments_for_object.erb

This file was deleted.

1 change: 0 additions & 1 deletion app/views/controllers/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ body_class = class_names(ctrlr_action, theme, location_format, logged_in)
<html class="<%= html_class %>">
<head>
<%= render(partial: "application/app/head") %>
<%= javascript_importmap_tags %>
</head>

<body class="<%= body_class %>" data-controller="lazyload">
Expand Down
2 changes: 1 addition & 1 deletion app/views/controllers/shared/_modal_form.erb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ tag.div(
end,
tag.div(class: "modal-body", id: "modal_#{identifier}_body") do
[
tag.turbo_frame("", id: "modal_#{identifier}_flash"), # turbo
tag.div("", id: "modal_#{identifier}_flash"), # turbo
render(partial: form, locals: form_locals) # turbo
].safe_join
end
Expand Down
9 changes: 9 additions & 0 deletions cable/config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

# This file is used to start a Rack-based standalone server for ActionCable
# Start our cable server with `bundle exec puma -p 28080 cable/config.ru`

require_relative "../config/environment"
Rails.application.eager_load!

run ActionCable.server
15 changes: 15 additions & 0 deletions config/cable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ActionCable is a Rails feature that allows for
# real-time communication between the server and the client.

development:
adapter: redis
url: redis://localhost:6379/4

test:
adapter: test

production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379" } %>
channel_prefix: mo_production
ssl_params: { ca_file: "/etc/ssl/certs/cs-certificates.crt" }
4 changes: 4 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@
config.bot_enabled = true

config.active_job.queue_adapter = :solid_queue

# Set up ActionCable to use a standalone server at port 28080
# config.action_cable.mount_path = nil
# config.action_cable.url = "ws://localhost:28080" # use :wss in production
end

file = File.expand_path("../consts-site.rb", __dir__)
Expand Down
6 changes: 6 additions & 0 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@

config.bot_enabled = true

# Use default cable setup
# config.action_cable.mount_path = "/cable" # or nil
# Or set up ActionCable to use a standalone server at port 28080
# config.action_cable.url = "wss://localhost:28080" # use :wss in production
config.action_cable.allowed_request_origins = [%r{http://*}, %r{https://*/}]

config.active_job.queue_adapter = :solid_queue
end

Expand Down
Loading
Loading