From ec7fc750547b2a6b253f3d7d698f0c15c9dfe2ec Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 11 Oct 2024 20:14:09 -0500 Subject: [PATCH 01/13] Spike events system --- app/controllers/boosts_controller.rb | 2 +- app/controllers/comments_controller.rb | 7 +- app/helpers/bubbles_helper.rb | 2 +- app/helpers/comments_helper.rb | 80 ------------------- .../controllers/animation_controller.js | 2 +- .../controllers/filter_controller.js | 2 +- .../controllers/thread_controller.js | 14 ++++ app/javascript/helpers/current_helpers.js | 25 ++++++ app/javascript/helpers/index.js | 2 + app/models/boost.rb | 4 - app/models/bubble.rb | 7 +- app/models/bubble/assignable.rb | 7 +- app/models/bubble/boostable.rb | 10 +++ app/models/bubble/commentable.rb | 11 +++ app/models/bubble/eventable.rb | 14 ++++ app/models/bubble/thread.rb | 39 +++++++++ app/models/bubble/thread/rollup.rb | 59 ++++++++++++++ app/models/bubble/threaded.rb | 5 ++ app/models/current.rb | 8 +- app/models/event.rb | 10 +++ app/models/event/assignments.rb | 16 ++++ app/views/boosts/_boosts.html.erb | 8 +- app/views/boosts/create.turbo_stream.erb | 4 + app/views/bubbles/show.html.erb | 5 +- app/views/bubbles/threads/_entry.html.erb | 3 + app/views/bubbles/threads/_rollup.html.erb | 3 + app/views/bubbles/threads/_thread.html.erb | 13 +++ app/views/buckets/index.html.erb | 2 +- app/views/comments/_comment.html.erb | 2 +- app/views/layouts/application.html.erb | 4 + ...9181806_add_unique_index_to_assignments.rb | 5 ++ ...1009182303_make_comment_bodies_not_null.rb | 5 ++ db/migrate/20241009183354_create_events.rb | 14 ++++ db/migrate/20241009191253_drop_boosts.rb | 5 ++ ...241009191318_add_boost_count_to_bubbles.rb | 5 ++ ...remove_assignments_index_on_assignee_id.rb | 5 ++ db/schema.rb | 26 +++--- test/controllers/.keep | 0 test/controllers/boosts_controller_test.rb | 14 +++- test/controllers/comments_controller_test.rb | 14 +++- test/fixtures/assignments.yml | 7 ++ test/fixtures/boosts.yml | 15 ---- test/fixtures/bubbles.yml | 5 ++ test/fixtures/comments.yml | 1 + test/fixtures/events.yml | 66 +++++++++++++++ test/models/bubble_test.rb | 25 ++++++ 46 files changed, 436 insertions(+), 146 deletions(-) delete mode 100644 app/helpers/comments_helper.rb create mode 100644 app/javascript/controllers/thread_controller.js create mode 100644 app/javascript/helpers/current_helpers.js create mode 100644 app/javascript/helpers/index.js delete mode 100644 app/models/boost.rb create mode 100644 app/models/bubble/boostable.rb create mode 100644 app/models/bubble/commentable.rb create mode 100644 app/models/bubble/eventable.rb create mode 100644 app/models/bubble/thread.rb create mode 100644 app/models/bubble/thread/rollup.rb create mode 100644 app/models/bubble/threaded.rb create mode 100644 app/models/event.rb create mode 100644 app/models/event/assignments.rb create mode 100644 app/views/bubbles/threads/_entry.html.erb create mode 100644 app/views/bubbles/threads/_rollup.html.erb create mode 100644 app/views/bubbles/threads/_thread.html.erb create mode 100644 db/migrate/20241009181806_add_unique_index_to_assignments.rb create mode 100644 db/migrate/20241009182303_make_comment_bodies_not_null.rb create mode 100644 db/migrate/20241009183354_create_events.rb create mode 100644 db/migrate/20241009191253_drop_boosts.rb create mode 100644 db/migrate/20241009191318_add_boost_count_to_bubbles.rb create mode 100644 db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb delete mode 100644 test/controllers/.keep delete mode 100644 test/fixtures/boosts.yml create mode 100644 test/fixtures/events.yml diff --git a/app/controllers/boosts_controller.rb b/app/controllers/boosts_controller.rb index 166b7e634..5f5c95e93 100644 --- a/app/controllers/boosts_controller.rb +++ b/app/controllers/boosts_controller.rb @@ -2,6 +2,6 @@ class BoostsController < ApplicationController include BubbleScoped, BucketScoped def create - @bubble.boosts.create! + @bubble.boost! end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 3a6a650ac..d9ce0616c 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -2,12 +2,7 @@ class CommentsController < ApplicationController include BubbleScoped, BucketScoped def create - @bubble.comments.create!(comment_params) + @bubble.comment! params.dig(:comment, :body).presence redirect_to bucket_bubble_url(@bucket, @bubble) end - - private - def comment_params - params.require(:comment).permit(:body) - end end diff --git a/app/helpers/bubbles_helper.rb b/app/helpers/bubbles_helper.rb index 4d47f7d11..e1d2fdc84 100644 --- a/app/helpers/bubbles_helper.rb +++ b/app/helpers/bubbles_helper.rb @@ -8,7 +8,7 @@ module BubblesHelper end def bubble_size(bubble) - activity = bubble.boosts.size + bubble.comments.size + activity = bubble.boost_count + bubble.comments.count rank = case activity when 0..5 then "one" diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb deleted file mode 100644 index 18586dbe5..000000000 --- a/app/helpers/comments_helper.rb +++ /dev/null @@ -1,80 +0,0 @@ -module CommentsHelper - def render_comments_and_boosts(bubble) - combined_collection = combine_and_sort_items(bubble) - - safe_join([ - render_creator_summary(bubble, combined_collection), - render_remaining_items(combined_collection) - ]) - end - - private - def combine_and_sort_items(bubble) - (bubble.comments + bubble.boosts + bubble.assignments).sort_by(&:created_at) - end - - def render_creator_summary(bubble, combined_collection) - content_tag(:div, class: "comment--upvotes flex-inline flex-wrap align-start gap fill-white border-radius center position-relative") do - [ - creator_info(bubble), - initial_assignment_info(combined_collection), - render_initial_boosts(combined_collection) - ].compact.join(", ").html_safe - end - end - - def creator_info(bubble) - "Added by #{bubble.creator.name} #{time_ago_in_words(bubble.created_at)} ago" - end - - def initial_assignment_info(combined_collection) - initial_assignment = combined_collection.find { |item| item.is_a?(Assignment) } - "assigned to #{initial_assignment.assignee.name}" if initial_assignment - end - - def render_initial_boosts(combined_collection) - grouped_boosts = combined_collection.take_while { |item| item.is_a?(Boost) } - return if grouped_boosts.empty? - - user_boosts = grouped_boosts.group_by(&:creator).transform_values(&:count) - boost_summaries = user_boosts.map { |user, count| "#{user.name} +#{count}" } - boost_summaries.to_sentence - end - - def render_remaining_items(combined_collection) - initial_count = combined_collection.take_while { |item| !item.is_a?(Comment) }.count - items = combined_collection.drop(initial_count) - - safe_join(items.chunk_while { |i, j| grouped_item?(i) && grouped_item?(j) }.map do |chunk| - if chunk.first.is_a?(Comment) - render "comments/comment", comment: chunk.first - else - render_grouped_items(chunk) - end - end) - end - - def grouped_item?(item) - item.is_a?(Boost) || item.is_a?(Assignment) - end - - def render_grouped_items(items) - return if items.empty? - - content_tag(:div, class: "comment--upvotes flex-inline flex-wrap align-start gap fill-white border-radius center position-relative") do - [ - render_grouped_boosts(items.select { |item| item.is_a?(Boost) }), - render_grouped_assignments(items.select { |item| item.is_a?(Assignment) }) - ].flatten.compact.to_sentence.html_safe - end - end - - def render_grouped_boosts(boosts) - return if boosts.empty? - boosts.group_by(&:creator).map { |user, user_boosts| "#{user.name} +#{user_boosts.count}" } - end - - def render_grouped_assignments(assignments) - assignments.map { |assignment| "Assigned to #{assignment.assignee.name} #{time_ago_in_words(assignment.created_at)} ago" } - end -end diff --git a/app/javascript/controllers/animation_controller.js b/app/javascript/controllers/animation_controller.js index a6fe306a2..9fd0998d8 100644 --- a/app/javascript/controllers/animation_controller.js +++ b/app/javascript/controllers/animation_controller.js @@ -1,5 +1,5 @@ import { Controller } from "@hotwired/stimulus" -import { nextFrame } from "helpers/timing_helpers" +import { nextFrame } from "helpers" export default class extends Controller { static classes = [ "play" ] diff --git a/app/javascript/controllers/filter_controller.js b/app/javascript/controllers/filter_controller.js index 815b05952..953ae3a29 100644 --- a/app/javascript/controllers/filter_controller.js +++ b/app/javascript/controllers/filter_controller.js @@ -1,5 +1,5 @@ import { Controller } from "@hotwired/stimulus" -import { debounce } from "helpers/timing_helpers" +import { debounce } from "helpers" export default class extends Controller { static targets = [ "list" ] diff --git a/app/javascript/controllers/thread_controller.js b/app/javascript/controllers/thread_controller.js new file mode 100644 index 000000000..1004505b3 --- /dev/null +++ b/app/javascript/controllers/thread_controller.js @@ -0,0 +1,14 @@ +import { Controller } from "@hotwired/stimulus" +import { current } from "helpers" + +export default class extends Controller { + static classes = [ "myComment" ] + + connect() { + this.#myComments.forEach(comment => comment.classList.add(this.myCommentClass)) + } + + get #myComments() { + return this.element.querySelectorAll(`.comment[data-creator-id='${current.user.id}']`) + } +} diff --git a/app/javascript/helpers/current_helpers.js b/app/javascript/helpers/current_helpers.js new file mode 100644 index 000000000..6d2d9fdb2 --- /dev/null +++ b/app/javascript/helpers/current_helpers.js @@ -0,0 +1,25 @@ +// On-demand JavaScript objects from "current" HTML elements. Example: +// +// +// +// +// >> current.identity +// => { id: "123", timeZoneName: "Central Time (US & Canada)" } +// +// >> current.foo +// => {} + export const current = new Proxy({}, { + get(_target, propertyName) { + const result = {} + const prefix = `current-${propertyName}-` + for (const { name, content } of document.head.querySelectorAll(`meta[name^=${prefix}]`)) { + const key = camelize(name.slice(prefix.length)) + result[key] = content + } + return result + } + }) + + function camelize(string) { + return string.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase()) + } diff --git a/app/javascript/helpers/index.js b/app/javascript/helpers/index.js new file mode 100644 index 000000000..634fd65a2 --- /dev/null +++ b/app/javascript/helpers/index.js @@ -0,0 +1,2 @@ +export * from "helpers/current_helpers" +export * from "helpers/timing_helpers" diff --git a/app/models/boost.rb b/app/models/boost.rb deleted file mode 100644 index 41b0edb7a..000000000 --- a/app/models/boost.rb +++ /dev/null @@ -1,4 +0,0 @@ -class Boost < ApplicationRecord - belongs_to :bubble - belongs_to :creator, class_name: "User", default: -> { Current.user } -end diff --git a/app/models/bubble.rb b/app/models/bubble.rb index 43a6d05f8..debbf14cd 100644 --- a/app/models/bubble.rb +++ b/app/models/bubble.rb @@ -1,12 +1,9 @@ class Bubble < ApplicationRecord - include Assignable, Colored, Poppable, Searchable, Taggable + include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Taggable, Threaded belongs_to :bucket belongs_to :creator, class_name: "User", default: -> { Current.user } - has_many :comments, dependent: :destroy - has_many :boosts, dependent: :destroy - has_one_attached :image, dependent: :purge_later searchable_by :title, using: :bubbles_search_index @@ -14,7 +11,7 @@ class Bubble < ApplicationRecord before_save :set_default_title scope :reverse_chronologically, -> { order(created_at: :desc, id: :desc) } - scope :ordered_by_activity, -> { left_joins(:comments, :boosts).group(:id).order(Arel.sql("COUNT(comments.id) + COUNT(boosts.id) DESC")) } + scope :ordered_by_activity, -> { left_joins(:comments).group(:id).order(Arel.sql("COUNT(comments.id) + boost_count DESC")) } scope :mentioning, ->(query) do bubbles = search(query).select(:id).to_sql diff --git a/app/models/bubble/assignable.rb b/app/models/bubble/assignable.rb index 3abf18d39..828034dab 100644 --- a/app/models/bubble/assignable.rb +++ b/app/models/bubble/assignable.rb @@ -2,7 +2,7 @@ module Bubble::Assignable extend ActiveSupport::Concern included do - has_many :assignments, dependent: :destroy + has_many :assignments, dependent: :delete_all has_many :assignees, through: :assignments has_many :assigners, through: :assignments @@ -12,8 +12,9 @@ module Bubble::Assignable end def assign(users, assigner: Current.user) - (Array(users) - assignees).tap do |users| - users.each { |user| assignments.create!(assignee: user, assigner: assigner) } + transaction do + Assignment.insert_all Array(users).collect { |user| { assignee_id: user.id, assigner_id: assigner.id, bubble_id: id } } + track_event :assigned, assignee_ids: Array(users).map(&:id) end end end diff --git a/app/models/bubble/boostable.rb b/app/models/bubble/boostable.rb new file mode 100644 index 000000000..3c39d93fa --- /dev/null +++ b/app/models/bubble/boostable.rb @@ -0,0 +1,10 @@ +module Bubble::Boostable + extend ActiveSupport::Concern + + def boost! + transaction do + increment! :boost_count + track_event :boosted, boost_count: + end + end +end diff --git a/app/models/bubble/commentable.rb b/app/models/bubble/commentable.rb new file mode 100644 index 000000000..d0ff9c521 --- /dev/null +++ b/app/models/bubble/commentable.rb @@ -0,0 +1,11 @@ +module Bubble::Commentable + extend ActiveSupport::Concern + + included do + has_many :comments, dependent: :delete_all + end + + def comment!(body) + comments.create! body: + end +end diff --git a/app/models/bubble/eventable.rb b/app/models/bubble/eventable.rb new file mode 100644 index 000000000..e1137880a --- /dev/null +++ b/app/models/bubble/eventable.rb @@ -0,0 +1,14 @@ +module Bubble::Eventable + extend ActiveSupport::Concern + + included do + has_many :events, dependent: :delete_all + + after_create -> { track_event :created } + end + + private + def track_event(action, creator: Current.user, **particulars) + events.create! action:, creator:, particulars: + end +end diff --git a/app/models/bubble/thread.rb b/app/models/bubble/thread.rb new file mode 100644 index 000000000..1b8dda3c0 --- /dev/null +++ b/app/models/bubble/thread.rb @@ -0,0 +1,39 @@ +class Bubble::Thread + attr_reader :bubble + + def initialize(bubble) + @bubble = bubble + end + + def entries + sorted_entries.chunk_while { |a, b| consecutive_events?(a, b) }.map.with_index { |entries, index| roll_up(entries, index) } + end + + def to_partial_path + "bubbles/threads/thread" + end + + def cache_key + ActiveSupport::Cache.expand_cache_key bubble, :thread + end + + private + delegate :events, :comments, to: :bubble, private: true + + def sorted_entries + (events + comments).sort_by(&:created_at) + end + + def consecutive_events?(a, b) + [ a, b ] in [ Event, Event ] + end + + def roll_up(entries, index) + case entries.first + when Comment + entries.sole + when Event + Rollup.new self, entries, first_position: index.zero? + end + end +end diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb new file mode 100644 index 000000000..be4a49bb2 --- /dev/null +++ b/app/models/bubble/thread/rollup.rb @@ -0,0 +1,59 @@ +class Bubble::Thread::Rollup + def initialize(thread, entries, first_position: false) + @thread = thread + @entries = entries + @first_position = first_position + end + + def body + collapsed_entries.map { |entry, chunk_size| summarize(entry, chunk_size) }.to_sentence.upcase_first + end + + def to_partial_path + "bubbles/threads/rollup" + end + + def cache_key + ActiveSupport::Cache.expand_cache_key [ thread, entries.first, entries.last ], "rollup" + end + + private + attr_reader :thread, :entries, :first_position + + delegate :time_ago_in_words, to: "ApplicationController.helpers", private: true + + def first_position? + first_position + end + + def collapsed_entries + sorted_entries.chunk_while { |a, b| equivalent_boosts?(a, b) }.map { |chunk| [ chunk.last, chunk.size ] } + end + + def sorted_entries + entries.sort_by do |entry| + case entry.action + when "created" then [ 1, entry.created_at ] + when "assigned" then [ 2, entry.created_at ] + when "boosted" then [ 3, entry.creator, entry.created_at ] + end + end + end + + def equivalent_boosts?(a, b) + a.action == "boosted" && a.slice(:action, :creator_id) == b.slice(:action, :creator_id) + end + + def summarize(entry, chunk_size) + case entry.action + when "created" + "added by #{entry.creator.name} #{time_ago_in_words(entry.created_at)} ago" + when "assigned" + summary = "assigned to #{entry.assignee_names.to_sentence}" + summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position? + summary + when "boosted" + "#{entry.creator.name} +#{chunk_size}" + end + end +end diff --git a/app/models/bubble/threaded.rb b/app/models/bubble/threaded.rb new file mode 100644 index 000000000..22885f62a --- /dev/null +++ b/app/models/bubble/threaded.rb @@ -0,0 +1,5 @@ +module Bubble::Threaded + def thread + Bubble::Thread.new self + end +end diff --git a/app/models/current.rb b/app/models/current.rb index e5546b93b..d534f3e58 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,6 +1,10 @@ class Current < ActiveSupport::CurrentAttributes - attribute :session + attribute :session, :user - delegate :user, to: :session, allow_nil: true delegate :account, to: :user, allow_nil: true + + def session=(session) + super + self.user = session.user + end end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 000000000..8994acc05 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,10 @@ +class Event < ApplicationRecord + THREADABLE_ACTIONS = %w[ assigned boosted created ] + + include Assignments + + belongs_to :creator, class_name: "User" + belongs_to :bubble + + scope :threadable, -> { where action: THREADABLE_ACTIONS } +end diff --git a/app/models/event/assignments.rb b/app/models/event/assignments.rb new file mode 100644 index 000000000..ade131d31 --- /dev/null +++ b/app/models/event/assignments.rb @@ -0,0 +1,16 @@ +module Event::Assignments + extend ActiveSupport::Concern + + included do + store_accessor :particulars, :assignee_ids + end + + def assignee_names + assignees.map &:name + end + + private + def assignees + @assignees ||= creator.account.users.find assignee_ids + end +end diff --git a/app/views/boosts/_boosts.html.erb b/app/views/boosts/_boosts.html.erb index 1e2b1998b..973107476 100644 --- a/app/views/boosts/_boosts.html.erb +++ b/app/views/boosts/_boosts.html.erb @@ -1,6 +1,6 @@ <%= button_to bucket_bubble_boosts_path(bubble.bucket, bubble), - class: "btn btn--plain", - data: { action: "toggle-class#toggle" }, - form_class: "full-width" do %> - + <%= bubble.boosts.size if bubble.boosts.any? %> + class: "btn btn--plain", + data: { action: "toggle-class#toggle" }, + form_class: "full-width" do %> + + <%= bubble.boost_count if bubble.boost_count.positive? %> <% end %> diff --git a/app/views/boosts/create.turbo_stream.erb b/app/views/boosts/create.turbo_stream.erb index 4481aa4bf..718c9cf0d 100644 --- a/app/views/boosts/create.turbo_stream.erb +++ b/app/views/boosts/create.turbo_stream.erb @@ -1,3 +1,7 @@ <%= turbo_stream.update dom_id(@bubble, :boosts) do %> <%= render "boosts/boosts", bubble: @bubble %> <% end %> + +<%= turbo_stream.replace dom_id(@bubble, :thread) do %> + <%= render @bubble.thread %> +<% end %> diff --git a/app/views/bubbles/show.html.erb b/app/views/bubbles/show.html.erb index ee9cfbad8..9d320c4f7 100644 --- a/app/views/bubbles/show.html.erb +++ b/app/views/bubbles/show.html.erb @@ -25,7 +25,4 @@ <%= render "bubbles/bubble", bubble: @bubble %> -
- <%= render_comments_and_boosts(@bubble) %> - <%= render "comments/new", bubble: @bubble %> -
+<%= render @bubble.thread %> diff --git a/app/views/bubbles/threads/_entry.html.erb b/app/views/bubbles/threads/_entry.html.erb new file mode 100644 index 000000000..f299a3e08 --- /dev/null +++ b/app/views/bubbles/threads/_entry.html.erb @@ -0,0 +1,3 @@ +<%# Template Dependency: comments/comment %> +<%# Template Dependency: bubbles/threads/rollup %> +<%= render entry %> diff --git a/app/views/bubbles/threads/_rollup.html.erb b/app/views/bubbles/threads/_rollup.html.erb new file mode 100644 index 000000000..665807798 --- /dev/null +++ b/app/views/bubbles/threads/_rollup.html.erb @@ -0,0 +1,3 @@ +
+ <%= rollup.body %> +
diff --git a/app/views/bubbles/threads/_thread.html.erb b/app/views/bubbles/threads/_thread.html.erb new file mode 100644 index 000000000..7573722e5 --- /dev/null +++ b/app/views/bubbles/threads/_thread.html.erb @@ -0,0 +1,13 @@ +<% bubble = thread.bubble %> + +
+ <% cache thread do %> + <%= render partial: "bubbles/threads/entry", collection: thread.entries, cache: true %> + <% end %> + + <%= render "comments/new", bubble: bubble %> +
diff --git a/app/views/buckets/index.html.erb b/app/views/buckets/index.html.erb index 08573bd09..8436c5658 100644 --- a/app/views/buckets/index.html.erb +++ b/app/views/buckets/index.html.erb @@ -20,7 +20,7 @@ <%= link_to bucket_bubbles_path(bucket), class: "windshield__container flex justify-center align-center position-relative" do %>
- <% bucket.bubbles.not_popped.left_joins(:comments, :boosts).group(:id).order(Arel.sql("COUNT(comments.id) + COUNT(boosts.id) DESC")).limit(10).each do |bubble| %> + <% bucket.bubbles.not_popped.ordered_by_activity.limit(10).each do |bubble| %>
diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 6d5892457..1492a6a23 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,4 +1,4 @@ -
" id="<%= dom_id(comment) %>"> +
<%= avatar_tag comment.creator, loading: :lazy %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f75b3e64c..84908c22d 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,6 +9,10 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <% if Current.user %> + + <% end %> + <%= yield :head %> <%= stylesheet_link_tag :all, "data-turbo-track": "reload" %> diff --git a/db/migrate/20241009181806_add_unique_index_to_assignments.rb b/db/migrate/20241009181806_add_unique_index_to_assignments.rb new file mode 100644 index 000000000..6600bef57 --- /dev/null +++ b/db/migrate/20241009181806_add_unique_index_to_assignments.rb @@ -0,0 +1,5 @@ +class AddUniqueIndexToAssignments < ActiveRecord::Migration[8.0] + def change + add_index :assignments, %i[ assignee_id bubble_id ], unique: true + end +end diff --git a/db/migrate/20241009182303_make_comment_bodies_not_null.rb b/db/migrate/20241009182303_make_comment_bodies_not_null.rb new file mode 100644 index 000000000..eebedd368 --- /dev/null +++ b/db/migrate/20241009182303_make_comment_bodies_not_null.rb @@ -0,0 +1,5 @@ +class MakeCommentBodiesNotNull < ActiveRecord::Migration[8.0] + def change + change_column_null :comments, :body, false + end +end diff --git a/db/migrate/20241009183354_create_events.rb b/db/migrate/20241009183354_create_events.rb new file mode 100644 index 000000000..19c0f66da --- /dev/null +++ b/db/migrate/20241009183354_create_events.rb @@ -0,0 +1,14 @@ +class CreateEvents < ActiveRecord::Migration[8.0] + def change + create_table :events do |t| + t.references :bubble, null: false, index: false + t.references :creator, null: false + t.json :particulars, default: {} + t.string :action, null: false + + t.timestamps + end + + add_index :events, %i[ bubble_id action ], name: "index_events_on_bubble_id_and_action" + end +end diff --git a/db/migrate/20241009191253_drop_boosts.rb b/db/migrate/20241009191253_drop_boosts.rb new file mode 100644 index 000000000..9ecfeeea5 --- /dev/null +++ b/db/migrate/20241009191253_drop_boosts.rb @@ -0,0 +1,5 @@ +class DropBoosts < ActiveRecord::Migration[8.0] + def change + drop_table :boosts + end +end diff --git a/db/migrate/20241009191318_add_boost_count_to_bubbles.rb b/db/migrate/20241009191318_add_boost_count_to_bubbles.rb new file mode 100644 index 000000000..55a215ed5 --- /dev/null +++ b/db/migrate/20241009191318_add_boost_count_to_bubbles.rb @@ -0,0 +1,5 @@ +class AddBoostCountToBubbles < ActiveRecord::Migration[8.0] + def change + add_column :bubbles, :boost_count, :integer, null: false, default: 0 + end +end diff --git a/db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb b/db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb new file mode 100644 index 000000000..5f41bcc3a --- /dev/null +++ b/db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb @@ -0,0 +1,5 @@ +class RemoveAssignmentsIndexOnAssigneeId < ActiveRecord::Migration[8.0] + def change + remove_index :assignments, :assignee_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 4724af881..0218e49b4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do +ActiveRecord::Schema[8.0].define(version: 2024_10_09_211414) do create_table "accesses", force: :cascade do |t| t.integer "bucket_id", null: false t.integer "user_id", null: false @@ -64,18 +64,10 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "assigner_id", null: false - t.index ["assignee_id"], name: "index_assignments_on_assignee_id" + t.index ["assignee_id", "bubble_id"], name: "index_assignments_on_assignee_id_and_bubble_id", unique: true t.index ["bubble_id"], name: "index_assignments_on_bubble_id" end - create_table "boosts", force: :cascade do |t| - t.integer "creator_id", null: false - t.integer "bubble_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["bubble_id"], name: "index_boosts_on_bubble_id" - end - create_table "bubbles", force: :cascade do |t| t.string "title" t.string "color" @@ -84,6 +76,7 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do t.integer "creator_id", null: false t.date "due_on" t.integer "bucket_id", null: false + t.integer "boost_count", default: 0, null: false t.index ["bucket_id"], name: "index_bubbles_on_bucket_id" end @@ -98,13 +91,24 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do end create_table "comments", force: :cascade do |t| - t.text "body" + t.text "body", null: false t.integer "creator_id", null: false t.integer "bubble_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end + create_table "events", force: :cascade do |t| + t.integer "bubble_id", null: false + t.integer "creator_id", null: false + t.json "particulars", default: {} + t.string "action", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["bubble_id", "action"], name: "index_events_on_bubble_id_and_action" + t.index ["creator_id"], name: "index_events_on_creator_id" + end + create_table "pops", force: :cascade do |t| t.integer "bubble_id", null: false t.integer "user_id" diff --git a/test/controllers/.keep b/test/controllers/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/controllers/boosts_controller_test.rb b/test/controllers/boosts_controller_test.rb index 051cf6298..ce8135c78 100644 --- a/test/controllers/boosts_controller_test.rb +++ b/test/controllers/boosts_controller_test.rb @@ -1,7 +1,15 @@ require "test_helper" class BoostsControllerTest < ActionDispatch::IntegrationTest - # test "the truth" do - # assert true - # end + setup do + sign_in_as :kevin + end + + test "create" do + assert_difference "bubbles(:logo).reload.boost_count", +1 do + post bucket_bubble_boosts_url(buckets(:writebook), bubbles(:logo), format: :turbo_stream) + end + + assert_turbo_stream action: :update, target: dom_id(bubbles(:logo), :boosts) + end end diff --git a/test/controllers/comments_controller_test.rb b/test/controllers/comments_controller_test.rb index 3f25e015e..9578a2ffb 100644 --- a/test/controllers/comments_controller_test.rb +++ b/test/controllers/comments_controller_test.rb @@ -1,7 +1,15 @@ require "test_helper" class CommentsControllerTest < ActionDispatch::IntegrationTest - # test "the truth" do - # assert true - # end + setup do + sign_in_as :kevin + end + + test "create" do + assert_difference "bubbles(:logo).comments.count", +1 do + post bucket_bubble_comments_url(buckets(:writebook), bubbles(:logo), params: { comment: { body: "Agreed." } }) + end + + assert_redirected_to bucket_bubble_url(buckets(:writebook), bubbles(:logo)) + end end diff --git a/test/fixtures/assignments.yml b/test/fixtures/assignments.yml index d7dd9e0a6..6ac04e823 100644 --- a/test/fixtures/assignments.yml +++ b/test/fixtures/assignments.yml @@ -1,7 +1,14 @@ +logo_jz: + assigner: david + assignee: jz + bubble: logo + created_at: <%= 1.week.ago %> + logo_kevin: assigner: david assignee: kevin bubble: logo + created_at: <%= 1.day.ago %> layout_jz: assigner: david diff --git a/test/fixtures/boosts.yml b/test/fixtures/boosts.yml deleted file mode 100644 index 5d46d0cce..000000000 --- a/test/fixtures/boosts.yml +++ /dev/null @@ -1,15 +0,0 @@ -logo_jz_one: - bubble: logo - creator: jz - -logo_jz_two: - bubble: logo - creator: jz - -logo_kevin: - bubble: logo - creator: kevin - -layout_jz: - bubble: layout - creator: jz diff --git a/test/fixtures/bubbles.yml b/test/fixtures/bubbles.yml index 23ad32219..f97105d5a 100644 --- a/test/fixtures/bubbles.yml +++ b/test/fixtures/bubbles.yml @@ -4,21 +4,26 @@ logo: title: The logo isn't big enough color: "#ED8008" due_on: <%= 3.days.from_now %> + created_at: <%= 1.week.ago %> + boost_count: 4 layout: bucket: writebook creator: david title: Layout is broken color: "#698F9C" + created_at: <%= 1.week.ago %> text: bucket: writebook creator: kevin title: The text is too small color: "#3B4B59" + created_at: <%= 1.week.ago %> shipping: bucket: writebook creator: kevin title: We need to ship the app color: "#ED8008" + created_at: <%= 1.week.ago %> diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml index 0444a5a43..10ac4cca7 100644 --- a/test/fixtures/comments.yml +++ b/test/fixtures/comments.yml @@ -2,6 +2,7 @@ logo_agreement_jz: body: I agree. creator: jz bubble: logo + created_at: <%= 2.days.ago %> logo_agreement_kevin: body: Same, let’s do it. diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml new file mode 100644 index 000000000..04dfdf2b4 --- /dev/null +++ b/test/fixtures/events.yml @@ -0,0 +1,66 @@ +logo_created: + creator: david + bubble: logo + action: created + created_at: <%= 1.week.ago %> + +logo_assignment_jz: + creator: david + bubble: logo + action: assigned + particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:jz) ] }.to_json %> + created_at: <%= 1.week.ago %> + +logo_assignment_km: + creator: david + bubble: logo + action: assigned + particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:kevin) ] }.to_json %> + created_at: <%= 1.day.ago %> + +logo_boost_km1: + creator: kevin + bubble: logo + action: boosted + created_at: <%= 1.day.ago %> + +logo_boost_km2: + creator: kevin + bubble: logo + action: boosted + created_at: <%= 1.day.ago %> + +logo_boost_jz1: + creator: jz + bubble: logo + action: boosted + created_at: <%= 1.day.ago %> + +logo_boost_jz2: + creator: jz + bubble: logo + action: boosted + +layout_created: + creator: david + bubble: layout + action: created + created_at: <%= 1.week.ago %> + +layout_assignment_jz: + creator: david + bubble: layout + action: assigned + particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:jz) ] }.to_json %> + +text_created: + creator: kevin + bubble: text + action: created + created_at: <%= 1.week.ago %> + +shipping_created: + creator: kevin + bubble: shipping + action: created + created_at: <%= 1.week.ago %> diff --git a/test/models/bubble_test.rb b/test/models/bubble_test.rb index d22683df2..7e8447a38 100644 --- a/test/models/bubble_test.rb +++ b/test/models/bubble_test.rb @@ -1,6 +1,31 @@ require "test_helper" class BubbleTest < ActiveSupport::TestCase + setup do + Current.user = users(:kevin) + end + + test "boosting" do + assert_difference %w[ bubbles(:logo).boost_count bubbles(:logo).events.count ], +1 do + bubbles(:logo).boost! + end + end + + test "commenting" do + bubbles(:logo).comment! "Agreed." + + assert_equal "Agreed.", bubbles(:logo).comments.last.body + end + + test "assigning" do + bubbles(:logo).assign users(:david) + + assert_equal users(:kevin, :jz, :david), bubbles(:logo).assignees + assert_equal users(:david, :kevin), bubbles(:logo).assigners.uniq + assert_equal [ users(:david).id ], bubbles(:logo).events.last.assignee_ids + assert_equal [ "David" ], bubbles(:logo).events.last.assignee_names + end + test "searchable by title" do bubble = buckets(:writebook).bubbles.create! title: "Insufficient haggis", creator: users(:kevin) From 755cceb6bdc029f963d239d27f54e1c715cf4ec6 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 11 Oct 2024 20:20:45 -0500 Subject: [PATCH 02/13] Use size for counting bubble comments --- app/helpers/bubbles_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/bubbles_helper.rb b/app/helpers/bubbles_helper.rb index e1d2fdc84..8bdf50648 100644 --- a/app/helpers/bubbles_helper.rb +++ b/app/helpers/bubbles_helper.rb @@ -8,7 +8,7 @@ module BubblesHelper end def bubble_size(bubble) - activity = bubble.boost_count + bubble.comments.count + activity = bubble.boost_count + bubble.comments.size rank = case activity when 0..5 then "one" From 817de3465309f63a8c48b198af3b05cfceff07b4 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 11 Oct 2024 20:24:41 -0500 Subject: [PATCH 03/13] Add one more boost to logo bubble --- test/fixtures/bubbles.yml | 2 +- test/fixtures/events.yml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/fixtures/bubbles.yml b/test/fixtures/bubbles.yml index f97105d5a..7e44c8a85 100644 --- a/test/fixtures/bubbles.yml +++ b/test/fixtures/bubbles.yml @@ -5,7 +5,7 @@ logo: color: "#ED8008" due_on: <%= 3.days.from_now %> created_at: <%= 1.week.ago %> - boost_count: 4 + boost_count: 5 layout: bucket: writebook diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index 04dfdf2b4..61da40243 100644 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -18,6 +18,12 @@ logo_assignment_km: particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:kevin) ] }.to_json %> created_at: <%= 1.day.ago %> +logo_boost_dhh: + creator: david + bubble: logo + action: boosted + created_at: <%= 1.week.ago %> + logo_boost_km1: creator: kevin bubble: logo From dcb5fbbe856847ff37f22a189aec3d7b7f3c9308 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 11 Oct 2024 20:27:32 -0500 Subject: [PATCH 04/13] index -> position --- app/models/bubble/thread.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/bubble/thread.rb b/app/models/bubble/thread.rb index 1b8dda3c0..a87a4d4d3 100644 --- a/app/models/bubble/thread.rb +++ b/app/models/bubble/thread.rb @@ -28,12 +28,12 @@ class Bubble::Thread [ a, b ] in [ Event, Event ] end - def roll_up(entries, index) + def roll_up(entries, position) case entries.first when Comment entries.sole when Event - Rollup.new self, entries, first_position: index.zero? + Rollup.new self, entries, first_position: position.zero? end end end From 9d87d66be6faad2493f7921453458753725c35a5 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 11 Oct 2024 20:28:50 -0500 Subject: [PATCH 05/13] Ignore brakeman false positive --- config/brakeman.ignore | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/config/brakeman.ignore b/config/brakeman.ignore index add1499e7..28a750e32 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -22,8 +22,42 @@ 89 ], "note": "" + }, + { + "warning_type": "Dynamic Render Path", + "warning_code": 15, + "fingerprint": "344f91887bfa44bd0a64504b78280b988ab09d000518dafc8169215cdbc2b7b2", + "check_name": "Render", + "message": "Render path contains parameter value", + "file": "app/views/bubbles/show.html.erb", + "line": 28, + "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", + "code": "render(action => @bucket.bubbles.find(params[:id]).thread, {})", + "render_path": [ + { + "type": "controller", + "class": "BubblesController", + "method": "show", + "line": 35, + "file": "app/controllers/bubbles_controller.rb", + "rendered": { + "name": "bubbles/show", + "file": "app/views/bubbles/show.html.erb" + } + } + ], + "location": { + "type": "template", + "template": "bubbles/show" + }, + "user_input": "params[:id]", + "confidence": "Weak", + "cwe_id": [ + 22 + ], + "note": "" } ], - "updated": "2024-10-03 15:28:53 -0400", + "updated": "2024-10-11 20:28:34 -0500", "brakeman_version": "6.2.1" } From f13a8a3cd5ea5af3ccbb9ca1003fbde20303838b Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Sat, 12 Oct 2024 22:49:20 -0600 Subject: [PATCH 06/13] equivalent_boosts? -> repeated_boosts? --- app/models/bubble/thread/rollup.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb index be4a49bb2..27c760130 100644 --- a/app/models/bubble/thread/rollup.rb +++ b/app/models/bubble/thread/rollup.rb @@ -27,7 +27,7 @@ class Bubble::Thread::Rollup end def collapsed_entries - sorted_entries.chunk_while { |a, b| equivalent_boosts?(a, b) }.map { |chunk| [ chunk.last, chunk.size ] } + sorted_entries.chunk_while { |a, b| repeated_boosts?(a, b) }.map { |chunk| [ chunk.last, chunk.size ] } end def sorted_entries @@ -40,7 +40,7 @@ class Bubble::Thread::Rollup end end - def equivalent_boosts?(a, b) + def repeated_boosts?(a, b) a.action == "boosted" && a.slice(:action, :creator_id) == b.slice(:action, :creator_id) end From 9e64f3d62a596013f9c977a4706207b38b59fe43 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Sat, 12 Oct 2024 22:49:36 -0600 Subject: [PATCH 07/13] Remove unused boost_count --- app/models/bubble/boostable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/bubble/boostable.rb b/app/models/bubble/boostable.rb index 3c39d93fa..98ce28485 100644 --- a/app/models/bubble/boostable.rb +++ b/app/models/bubble/boostable.rb @@ -4,7 +4,7 @@ module Bubble::Boostable def boost! transaction do increment! :boost_count - track_event :boosted, boost_count: + track_event :boosted end end end From 6b477a274216e2e325e06e93229649d3a4c2ca45 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Sat, 12 Oct 2024 23:03:13 -0600 Subject: [PATCH 08/13] Install local-time --- Gemfile | 1 + Gemfile.lock | 2 ++ app/javascript/application.js | 1 + app/javascript/initializers/index.js | 1 + app/javascript/initializers/local_time.js | 2 ++ config/importmap.rb | 2 ++ vendor/javascript/local-time.js | 4 ++++ 7 files changed, 13 insertions(+) create mode 100644 app/javascript/initializers/index.js create mode 100644 app/javascript/initializers/local_time.js create mode 100644 vendor/javascript/local-time.js diff --git a/Gemfile b/Gemfile index 6eed6e153..a927d2e08 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem "importmap-rails" gem "propshaft" gem "stimulus-rails" gem "turbo-rails" +gem "local_time" # Deployment and drivers gem "bootsnap", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 53d2332f2..f6ec4d451 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,6 +145,7 @@ GEM reline (>= 0.4.2) json (2.7.2) language_server-protocol (3.17.0.3) + local_time (3.0.2) logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) @@ -326,6 +327,7 @@ DEPENDENCIES capybara debug importmap-rails + local_time propshaft puma (>= 5.0) rails! diff --git a/app/javascript/application.js b/app/javascript/application.js index 0d7b49404..e917b49c5 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,4 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" +import "initializers" import "controllers" diff --git a/app/javascript/initializers/index.js b/app/javascript/initializers/index.js new file mode 100644 index 000000000..9a501b4b4 --- /dev/null +++ b/app/javascript/initializers/index.js @@ -0,0 +1 @@ +import "initializers/local_time" diff --git a/app/javascript/initializers/local_time.js b/app/javascript/initializers/local_time.js new file mode 100644 index 000000000..8b294a5d5 --- /dev/null +++ b/app/javascript/initializers/local_time.js @@ -0,0 +1,2 @@ +import LocalTime from "local-time" +LocalTime.start() diff --git a/config/importmap.rb b/config/importmap.rb index 768403738..f7807adc1 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -4,6 +4,8 @@ pin "application" pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin "local-time" # @3.0.2 +pin_all_from "app/javascript/initializers", under: "initializers" pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/helpers", under: "helpers" diff --git a/vendor/javascript/local-time.js b/vendor/javascript/local-time.js new file mode 100644 index 000000000..64d6e7995 --- /dev/null +++ b/vendor/javascript/local-time.js @@ -0,0 +1,4 @@ +// local-time@3.0.2 downloaded from https://ga.jspm.io/npm:local-time@3.0.2/app/assets/javascripts/local-time.es2017-esm.js + +var t;t={config:{},run:function(){return this.getController().processElements()},process:function(...t){var e,r,a;for(r=0,a=t.length;r11?"pm":"am")).toUpperCase();case"P":return M("time."+(n>11?"pm":"am"));case"S":return p(o,l);case"w":return a;case"y":return p(u%100,l);case"Y":return u;case"Z":return S(t)}}))},p=function(t,e){return"-"===e?t:`0${t}`.slice(-2)},S=function(t){var e,r,a;return(r=h(t))?g[r]:(a=y(t,{allowGMT:!1}))||(a=v(t))?a:(e=y(t,{allowGMT:!0}))?e:""},h=function(t){return Object.keys(g).find((function(e){return b?new Date(t).toLocaleString("en-US",{timeZoneName:"long"}).includes(e):t.toString().includes(e)}))},y=function(t,{allowGMT:e}){var r;if(b&&(r=new Date(t).toLocaleString("en-US",{timeZoneName:"short"}).split(" ").pop(),e||!r.includes("GMT")))return r},v=function(t){var e,r,a,n,s;return(e=null!=(r=(s=t.toString()).match(/\(([\w\s]+)\)$/))?r[1]:void 0)?/\s/.test(e)?e.match(/\b(\w)/g).join(""):e:(e=null!=(a=s.match(/(\w{3,4})\s\d{4}$/))?a[1]:void 0)||(e=null!=(n=s.match(/(UTC[\+\-]\d+)/))?n[1]:void 0)?e:void 0},L.CalendarDate=class{static fromDate(t){return new this(t.getFullYear(),t.getMonth()+1,t.getDate())}static today(){return this.fromDate(new Date)}constructor(t,e,r){this.date=new Date(Date.UTC(t,e-1)),this.date.setUTCDate(r),this.year=this.date.getUTCFullYear(),this.month=this.date.getUTCMonth()+1,this.day=this.date.getUTCDate(),this.value=this.date.getTime()}equals(t){return(null!=t?t.value:void 0)===this.value}is(t){return this.equals(t)}isToday(){return this.is(this.constructor.today())}occursOnSameYearAs(t){return this.year===(null!=t?t.year:void 0)}occursThisYear(){return this.occursOnSameYearAs(this.constructor.today())}daysSince(t){if(t)return(this.date-t.date)/864e5}daysPassed(){return this.constructor.today().daysSince(this)}},({strftime:E,translate:I,getI18nValue:w,config:D}=L),L.RelativeTime=class{constructor(t){this.date=t,this.calendarDate=L.CalendarDate.fromDate(this.date)}toString(){var t,e;return(e=this.toTimeElapsedString())?I("time.elapsed",{time:e}):(t=this.toWeekdayString())?(e=this.toTimeString(),I("datetime.at",{date:t,time:e})):I("date.on",{date:this.toDateString()})}toTimeOrDateString(){return this.calendarDate.isToday()?this.toTimeString():this.toDateString()}toTimeElapsedString(){var t,e,r,a,n;return r=(new Date).getTime()-this.date.getTime(),a=Math.round(r/1e3),e=Math.round(a/60),t=Math.round(e/60),r<0?null:a<10?(n=I("time.second"),I("time.singular",{time:n})):a<45?`${a} ${I("time.seconds")}`:a<90?(n=I("time.minute"),I("time.singular",{time:n})):e<45?`${e} ${I("time.minutes")}`:e<90?(n=I("time.hour"),I("time.singularAn",{time:n})):t<24?`${t} ${I("time.hours")}`:""}toWeekdayString(){switch(this.calendarDate.daysPassed()){case 0:return I("date.today");case 1:return I("date.yesterday");case-1:return I("date.tomorrow");case 2:case 3:case 4:case 5:case 6:return E(this.date,"%A");default:return""}}toDateString(){var t;return t=this.calendarDate.occursThisYear()?w("date.formats.thisYear"):w("date.formats.default"),E(this.date,t)}toTimeString(){var t;return t=D.useFormat24?"default_24h":"default",E(this.date,w(`time.formats.${t}`))}},({elementMatchesSelector:C}=L),L.PageObserver=class{constructor(t,e){this.processMutations=this.processMutations.bind(this),this.processInsertion=this.processInsertion.bind(this),this.selector=t,this.callback=e}start(){if(!this.started)return this.observeWithMutationObserver()||this.observeWithMutationEvent(),this.started=!0}observeWithMutationObserver(){if("undefined"!=typeof MutationObserver&&null!==MutationObserver)return new MutationObserver(this.processMutations).observe(document.documentElement,{childList:!0,subtree:!0}),!0}observeWithMutationEvent(){return addEventListener("DOMNodeInserted",this.processInsertion,!1),!0}findSignificantElements(t){var e;return e=[],(null!=t?t.nodeType:void 0)===Node.ELEMENT_NODE&&(C(t,this.selector)&&e.push(t),e.push(...t.querySelectorAll(this.selector))),e}processMutations(t){var e,r,a,n,s,i,o,u;for(e=[],r=0,n=t.length;r Date: Sat, 12 Oct 2024 23:03:27 -0600 Subject: [PATCH 09/13] Use local time for rollups --- app/models/bubble/thread/rollup.rb | 6 +++--- app/views/bubbles/threads/_rollup.html.erb | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb index 27c760130..89d3d76b5 100644 --- a/app/models/bubble/thread/rollup.rb +++ b/app/models/bubble/thread/rollup.rb @@ -20,7 +20,7 @@ class Bubble::Thread::Rollup private attr_reader :thread, :entries, :first_position - delegate :time_ago_in_words, to: "ApplicationController.helpers", private: true + delegate :local_time_ago, to: "ApplicationController.helpers", private: true def first_position? first_position @@ -47,10 +47,10 @@ class Bubble::Thread::Rollup def summarize(entry, chunk_size) case entry.action when "created" - "added by #{entry.creator.name} #{time_ago_in_words(entry.created_at)} ago" + "added by #{entry.creator.name} #{local_time_ago(entry.created_at)}" when "assigned" summary = "assigned to #{entry.assignee_names.to_sentence}" - summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position? + summary += " #{local_time_ago(entry.created_at)}" unless first_position? summary when "boosted" "#{entry.creator.name} +#{chunk_size}" diff --git a/app/views/bubbles/threads/_rollup.html.erb b/app/views/bubbles/threads/_rollup.html.erb index 665807798..aec078644 100644 --- a/app/views/bubbles/threads/_rollup.html.erb +++ b/app/views/bubbles/threads/_rollup.html.erb @@ -1,3 +1,3 @@ -
- <%= rollup.body %> +
+ <%= rollup.body.html_safe %>
From bb5b0701ed8d1c510cb9e7856ecc52d5bcef99b4 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Sat, 12 Oct 2024 23:06:34 -0600 Subject: [PATCH 10/13] Simplify Current JS implementation --- .../controllers/thread_controller.js | 3 +-- app/javascript/helpers/current_helpers.js | 25 ------------------- app/javascript/helpers/index.js | 1 - app/javascript/initializers/current.js | 15 +++++++++++ app/javascript/initializers/index.js | 1 + 5 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 app/javascript/helpers/current_helpers.js create mode 100644 app/javascript/initializers/current.js diff --git a/app/javascript/controllers/thread_controller.js b/app/javascript/controllers/thread_controller.js index 1004505b3..0671c8361 100644 --- a/app/javascript/controllers/thread_controller.js +++ b/app/javascript/controllers/thread_controller.js @@ -1,5 +1,4 @@ import { Controller } from "@hotwired/stimulus" -import { current } from "helpers" export default class extends Controller { static classes = [ "myComment" ] @@ -9,6 +8,6 @@ export default class extends Controller { } get #myComments() { - return this.element.querySelectorAll(`.comment[data-creator-id='${current.user.id}']`) + return this.element.querySelectorAll(`.comment[data-creator-id='${Current.user.id}']`) } } diff --git a/app/javascript/helpers/current_helpers.js b/app/javascript/helpers/current_helpers.js deleted file mode 100644 index 6d2d9fdb2..000000000 --- a/app/javascript/helpers/current_helpers.js +++ /dev/null @@ -1,25 +0,0 @@ -// On-demand JavaScript objects from "current" HTML elements. Example: -// -// -// -// -// >> current.identity -// => { id: "123", timeZoneName: "Central Time (US & Canada)" } -// -// >> current.foo -// => {} - export const current = new Proxy({}, { - get(_target, propertyName) { - const result = {} - const prefix = `current-${propertyName}-` - for (const { name, content } of document.head.querySelectorAll(`meta[name^=${prefix}]`)) { - const key = camelize(name.slice(prefix.length)) - result[key] = content - } - return result - } - }) - - function camelize(string) { - return string.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase()) - } diff --git a/app/javascript/helpers/index.js b/app/javascript/helpers/index.js index 634fd65a2..c33ab8c40 100644 --- a/app/javascript/helpers/index.js +++ b/app/javascript/helpers/index.js @@ -1,2 +1 @@ -export * from "helpers/current_helpers" export * from "helpers/timing_helpers" diff --git a/app/javascript/initializers/current.js b/app/javascript/initializers/current.js new file mode 100644 index 000000000..96ace04b5 --- /dev/null +++ b/app/javascript/initializers/current.js @@ -0,0 +1,15 @@ +class Current { + get user() { + const currentUserId = this.#extractContentFromMetaTag("current-user-id") + + if (currentUserId) { + return { id: parseInt(currentUserId) } + } + } + + #extractContentFromMetaTag(name) { + return document.head.querySelector(`meta[name="${name}"]`)?.getAttribute("content") + } +} + +window.Current = new Current() diff --git a/app/javascript/initializers/index.js b/app/javascript/initializers/index.js index 9a501b4b4..c86bbda03 100644 --- a/app/javascript/initializers/index.js +++ b/app/javascript/initializers/index.js @@ -1 +1,2 @@ +import "initializers/current" import "initializers/local_time" From 1d6acb61195a3e4ff593de0d0de2c7e14c8eb0bd Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Sat, 12 Oct 2024 23:23:12 -0600 Subject: [PATCH 11/13] =?UTF-8?q?Stop=20caching=20threads=20=E2=80=94=20YA?= =?UTF-8?q?GNI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 1 - Gemfile.lock | 2 -- app/javascript/application.js | 1 - .../controllers/animation_controller.js | 2 +- app/javascript/controllers/filter_controller.js | 2 +- app/javascript/controllers/thread_controller.js | 13 ------------- app/javascript/helpers/index.js | 1 - app/javascript/initializers/current.js | 15 --------------- app/javascript/initializers/index.js | 2 -- app/javascript/initializers/local_time.js | 2 -- app/models/bubble/thread.rb | 4 ---- app/models/bubble/thread/rollup.rb | 10 +++------- app/views/bubbles/threads/_entry.html.erb | 3 --- app/views/bubbles/threads/_rollup.html.erb | 2 +- app/views/bubbles/threads/_thread.html.erb | 11 ++--------- app/views/comments/_comment.html.erb | 4 ++-- app/views/layouts/application.html.erb | 4 ---- config/importmap.rb | 2 -- vendor/javascript/local-time.js | 4 ---- 19 files changed, 10 insertions(+), 75 deletions(-) delete mode 100644 app/javascript/controllers/thread_controller.js delete mode 100644 app/javascript/helpers/index.js delete mode 100644 app/javascript/initializers/current.js delete mode 100644 app/javascript/initializers/index.js delete mode 100644 app/javascript/initializers/local_time.js delete mode 100644 app/views/bubbles/threads/_entry.html.erb delete mode 100644 vendor/javascript/local-time.js diff --git a/Gemfile b/Gemfile index a927d2e08..6eed6e153 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,6 @@ gem "importmap-rails" gem "propshaft" gem "stimulus-rails" gem "turbo-rails" -gem "local_time" # Deployment and drivers gem "bootsnap", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 1526749a3..2a1789a26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,7 +145,6 @@ GEM reline (>= 0.4.2) json (2.7.2) language_server-protocol (3.17.0.3) - local_time (3.0.2) logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) @@ -327,7 +326,6 @@ DEPENDENCIES capybara debug importmap-rails - local_time propshaft puma (>= 5.0) rails! diff --git a/app/javascript/application.js b/app/javascript/application.js index e917b49c5..0d7b49404 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,4 +1,3 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" -import "initializers" import "controllers" diff --git a/app/javascript/controllers/animation_controller.js b/app/javascript/controllers/animation_controller.js index 9fd0998d8..a6fe306a2 100644 --- a/app/javascript/controllers/animation_controller.js +++ b/app/javascript/controllers/animation_controller.js @@ -1,5 +1,5 @@ import { Controller } from "@hotwired/stimulus" -import { nextFrame } from "helpers" +import { nextFrame } from "helpers/timing_helpers" export default class extends Controller { static classes = [ "play" ] diff --git a/app/javascript/controllers/filter_controller.js b/app/javascript/controllers/filter_controller.js index 953ae3a29..815b05952 100644 --- a/app/javascript/controllers/filter_controller.js +++ b/app/javascript/controllers/filter_controller.js @@ -1,5 +1,5 @@ import { Controller } from "@hotwired/stimulus" -import { debounce } from "helpers" +import { debounce } from "helpers/timing_helpers" export default class extends Controller { static targets = [ "list" ] diff --git a/app/javascript/controllers/thread_controller.js b/app/javascript/controllers/thread_controller.js deleted file mode 100644 index 0671c8361..000000000 --- a/app/javascript/controllers/thread_controller.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static classes = [ "myComment" ] - - connect() { - this.#myComments.forEach(comment => comment.classList.add(this.myCommentClass)) - } - - get #myComments() { - return this.element.querySelectorAll(`.comment[data-creator-id='${Current.user.id}']`) - } -} diff --git a/app/javascript/helpers/index.js b/app/javascript/helpers/index.js deleted file mode 100644 index c33ab8c40..000000000 --- a/app/javascript/helpers/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from "helpers/timing_helpers" diff --git a/app/javascript/initializers/current.js b/app/javascript/initializers/current.js deleted file mode 100644 index 96ace04b5..000000000 --- a/app/javascript/initializers/current.js +++ /dev/null @@ -1,15 +0,0 @@ -class Current { - get user() { - const currentUserId = this.#extractContentFromMetaTag("current-user-id") - - if (currentUserId) { - return { id: parseInt(currentUserId) } - } - } - - #extractContentFromMetaTag(name) { - return document.head.querySelector(`meta[name="${name}"]`)?.getAttribute("content") - } -} - -window.Current = new Current() diff --git a/app/javascript/initializers/index.js b/app/javascript/initializers/index.js deleted file mode 100644 index c86bbda03..000000000 --- a/app/javascript/initializers/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import "initializers/current" -import "initializers/local_time" diff --git a/app/javascript/initializers/local_time.js b/app/javascript/initializers/local_time.js deleted file mode 100644 index 8b294a5d5..000000000 --- a/app/javascript/initializers/local_time.js +++ /dev/null @@ -1,2 +0,0 @@ -import LocalTime from "local-time" -LocalTime.start() diff --git a/app/models/bubble/thread.rb b/app/models/bubble/thread.rb index a87a4d4d3..8b0e28be5 100644 --- a/app/models/bubble/thread.rb +++ b/app/models/bubble/thread.rb @@ -13,10 +13,6 @@ class Bubble::Thread "bubbles/threads/thread" end - def cache_key - ActiveSupport::Cache.expand_cache_key bubble, :thread - end - private delegate :events, :comments, to: :bubble, private: true diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb index 89d3d76b5..e98f8a00e 100644 --- a/app/models/bubble/thread/rollup.rb +++ b/app/models/bubble/thread/rollup.rb @@ -13,14 +13,10 @@ class Bubble::Thread::Rollup "bubbles/threads/rollup" end - def cache_key - ActiveSupport::Cache.expand_cache_key [ thread, entries.first, entries.last ], "rollup" - end - private attr_reader :thread, :entries, :first_position - delegate :local_time_ago, to: "ApplicationController.helpers", private: true + delegate :time_ago_in_words, to: "ApplicationController.helpers", private: true def first_position? first_position @@ -47,10 +43,10 @@ class Bubble::Thread::Rollup def summarize(entry, chunk_size) case entry.action when "created" - "added by #{entry.creator.name} #{local_time_ago(entry.created_at)}" + "added by #{entry.creator.name} #{time_ago_in_words(entry.created_at)} ago" when "assigned" summary = "assigned to #{entry.assignee_names.to_sentence}" - summary += " #{local_time_ago(entry.created_at)}" unless first_position? + summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position? summary when "boosted" "#{entry.creator.name} +#{chunk_size}" diff --git a/app/views/bubbles/threads/_entry.html.erb b/app/views/bubbles/threads/_entry.html.erb deleted file mode 100644 index f299a3e08..000000000 --- a/app/views/bubbles/threads/_entry.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%# Template Dependency: comments/comment %> -<%# Template Dependency: bubbles/threads/rollup %> -<%= render entry %> diff --git a/app/views/bubbles/threads/_rollup.html.erb b/app/views/bubbles/threads/_rollup.html.erb index aec078644..adbfd5138 100644 --- a/app/views/bubbles/threads/_rollup.html.erb +++ b/app/views/bubbles/threads/_rollup.html.erb @@ -1,3 +1,3 @@
- <%= rollup.body.html_safe %> + <%= rollup.body %>
diff --git a/app/views/bubbles/threads/_thread.html.erb b/app/views/bubbles/threads/_thread.html.erb index 7573722e5..9d8103bb1 100644 --- a/app/views/bubbles/threads/_thread.html.erb +++ b/app/views/bubbles/threads/_thread.html.erb @@ -1,13 +1,6 @@ <% bubble = thread.bubble %> -
- <% cache thread do %> - <%= render partial: "bubbles/threads/entry", collection: thread.entries, cache: true %> - <% end %> - +
+ <%= render thread.entries %> <%= render "comments/new", bubble: bubble %>
diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 1492a6a23..0cd298ab3 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,4 +1,4 @@ -
+<%= tag.div id: dom_id(comment), class: [ "comment flex align-start full-width", { "comment--mine": Current.user == comment.creator } ] do %>
<%= avatar_tag comment.creator, loading: :lazy %>
@@ -16,4 +16,4 @@ <%= simple_format comment.body %>
-
+<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 84908c22d..f75b3e64c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,10 +9,6 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> - <% if Current.user %> - - <% end %> - <%= yield :head %> <%= stylesheet_link_tag :all, "data-turbo-track": "reload" %> diff --git a/config/importmap.rb b/config/importmap.rb index f7807adc1..768403738 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -4,8 +4,6 @@ pin "application" pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" -pin "local-time" # @3.0.2 -pin_all_from "app/javascript/initializers", under: "initializers" pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/helpers", under: "helpers" diff --git a/vendor/javascript/local-time.js b/vendor/javascript/local-time.js deleted file mode 100644 index 64d6e7995..000000000 --- a/vendor/javascript/local-time.js +++ /dev/null @@ -1,4 +0,0 @@ -// local-time@3.0.2 downloaded from https://ga.jspm.io/npm:local-time@3.0.2/app/assets/javascripts/local-time.es2017-esm.js - -var t;t={config:{},run:function(){return this.getController().processElements()},process:function(...t){var e,r,a;for(r=0,a=t.length;r11?"pm":"am")).toUpperCase();case"P":return M("time."+(n>11?"pm":"am"));case"S":return p(o,l);case"w":return a;case"y":return p(u%100,l);case"Y":return u;case"Z":return S(t)}}))},p=function(t,e){return"-"===e?t:`0${t}`.slice(-2)},S=function(t){var e,r,a;return(r=h(t))?g[r]:(a=y(t,{allowGMT:!1}))||(a=v(t))?a:(e=y(t,{allowGMT:!0}))?e:""},h=function(t){return Object.keys(g).find((function(e){return b?new Date(t).toLocaleString("en-US",{timeZoneName:"long"}).includes(e):t.toString().includes(e)}))},y=function(t,{allowGMT:e}){var r;if(b&&(r=new Date(t).toLocaleString("en-US",{timeZoneName:"short"}).split(" ").pop(),e||!r.includes("GMT")))return r},v=function(t){var e,r,a,n,s;return(e=null!=(r=(s=t.toString()).match(/\(([\w\s]+)\)$/))?r[1]:void 0)?/\s/.test(e)?e.match(/\b(\w)/g).join(""):e:(e=null!=(a=s.match(/(\w{3,4})\s\d{4}$/))?a[1]:void 0)||(e=null!=(n=s.match(/(UTC[\+\-]\d+)/))?n[1]:void 0)?e:void 0},L.CalendarDate=class{static fromDate(t){return new this(t.getFullYear(),t.getMonth()+1,t.getDate())}static today(){return this.fromDate(new Date)}constructor(t,e,r){this.date=new Date(Date.UTC(t,e-1)),this.date.setUTCDate(r),this.year=this.date.getUTCFullYear(),this.month=this.date.getUTCMonth()+1,this.day=this.date.getUTCDate(),this.value=this.date.getTime()}equals(t){return(null!=t?t.value:void 0)===this.value}is(t){return this.equals(t)}isToday(){return this.is(this.constructor.today())}occursOnSameYearAs(t){return this.year===(null!=t?t.year:void 0)}occursThisYear(){return this.occursOnSameYearAs(this.constructor.today())}daysSince(t){if(t)return(this.date-t.date)/864e5}daysPassed(){return this.constructor.today().daysSince(this)}},({strftime:E,translate:I,getI18nValue:w,config:D}=L),L.RelativeTime=class{constructor(t){this.date=t,this.calendarDate=L.CalendarDate.fromDate(this.date)}toString(){var t,e;return(e=this.toTimeElapsedString())?I("time.elapsed",{time:e}):(t=this.toWeekdayString())?(e=this.toTimeString(),I("datetime.at",{date:t,time:e})):I("date.on",{date:this.toDateString()})}toTimeOrDateString(){return this.calendarDate.isToday()?this.toTimeString():this.toDateString()}toTimeElapsedString(){var t,e,r,a,n;return r=(new Date).getTime()-this.date.getTime(),a=Math.round(r/1e3),e=Math.round(a/60),t=Math.round(e/60),r<0?null:a<10?(n=I("time.second"),I("time.singular",{time:n})):a<45?`${a} ${I("time.seconds")}`:a<90?(n=I("time.minute"),I("time.singular",{time:n})):e<45?`${e} ${I("time.minutes")}`:e<90?(n=I("time.hour"),I("time.singularAn",{time:n})):t<24?`${t} ${I("time.hours")}`:""}toWeekdayString(){switch(this.calendarDate.daysPassed()){case 0:return I("date.today");case 1:return I("date.yesterday");case-1:return I("date.tomorrow");case 2:case 3:case 4:case 5:case 6:return E(this.date,"%A");default:return""}}toDateString(){var t;return t=this.calendarDate.occursThisYear()?w("date.formats.thisYear"):w("date.formats.default"),E(this.date,t)}toTimeString(){var t;return t=D.useFormat24?"default_24h":"default",E(this.date,w(`time.formats.${t}`))}},({elementMatchesSelector:C}=L),L.PageObserver=class{constructor(t,e){this.processMutations=this.processMutations.bind(this),this.processInsertion=this.processInsertion.bind(this),this.selector=t,this.callback=e}start(){if(!this.started)return this.observeWithMutationObserver()||this.observeWithMutationEvent(),this.started=!0}observeWithMutationObserver(){if("undefined"!=typeof MutationObserver&&null!==MutationObserver)return new MutationObserver(this.processMutations).observe(document.documentElement,{childList:!0,subtree:!0}),!0}observeWithMutationEvent(){return addEventListener("DOMNodeInserted",this.processInsertion,!1),!0}findSignificantElements(t){var e;return e=[],(null!=t?t.nodeType:void 0)===Node.ELEMENT_NODE&&(C(t,this.selector)&&e.push(t),e.push(...t.querySelectorAll(this.selector))),e}processMutations(t){var e,r,a,n,s,i,o,u;for(e=[],r=0,n=t.length;r Date: Sat, 12 Oct 2024 23:28:16 -0600 Subject: [PATCH 12/13] TOC order in Rollup --- app/models/bubble/thread/rollup.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb index e98f8a00e..dd034aa8b 100644 --- a/app/models/bubble/thread/rollup.rb +++ b/app/models/bubble/thread/rollup.rb @@ -18,10 +18,6 @@ class Bubble::Thread::Rollup delegate :time_ago_in_words, to: "ApplicationController.helpers", private: true - def first_position? - first_position - end - def collapsed_entries sorted_entries.chunk_while { |a, b| repeated_boosts?(a, b) }.map { |chunk| [ chunk.last, chunk.size ] } end @@ -52,4 +48,8 @@ class Bubble::Thread::Rollup "#{entry.creator.name} +#{chunk_size}" end end + + def first_position? + first_position + end end From df83c72d6e4a036d505fa67413a074df502a7b41 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Tue, 15 Oct 2024 12:42:07 -0600 Subject: [PATCH 13/13] Use dependent: :destroy for comments --- app/models/bubble/commentable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/bubble/commentable.rb b/app/models/bubble/commentable.rb index d0ff9c521..911fd3956 100644 --- a/app/models/bubble/commentable.rb +++ b/app/models/bubble/commentable.rb @@ -2,7 +2,7 @@ module Bubble::Commentable extend ActiveSupport::Concern included do - has_many :comments, dependent: :delete_all + has_many :comments, dependent: :destroy end def comment!(body)