diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css
index 2a8021515..b1a030b59 100644
--- a/app/assets/stylesheets/comments.css
+++ b/app/assets/stylesheets/comments.css
@@ -44,7 +44,7 @@
margin-inline: var(--avatar-size) 0;
}
- .comment:has(+ .comment--upvotes) & {
+ .comment:has(+ .rollup) & {
padding-block-end: calc(var(--comment-padding-block) * 1.5);
}
}
@@ -73,8 +73,9 @@
opacity: 0.5;
}
-.comment--upvotes {
+.rollup {
font-weight: 600;
margin-block: calc(var(--comment-padding-block) * -1);
padding: calc(var(--comment-padding-block) * 0.5) var(--comment-padding-inline);
+ gap: 0.5ch;
}
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
new file mode 100644
index 000000000..f0e390afe
--- /dev/null
+++ b/app/helpers/events_helper.rb
@@ -0,0 +1,5 @@
+module EventsHelper
+ def boost_event_tag(event)
+ tag.span hidden: true, data: { rollup_target: "boostEvent", creator_id: event.creator_id, creator_name: event.creator_name }
+ end
+end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 0d7b49404..611524bf3 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 "extensions"
import "controllers"
diff --git a/app/javascript/controllers/rollup_controller.js b/app/javascript/controllers/rollup_controller.js
new file mode 100644
index 000000000..3d1d00898
--- /dev/null
+++ b/app/javascript/controllers/rollup_controller.js
@@ -0,0 +1,33 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = [ "boostEvent" ]
+
+ connect() {
+ this.#summarizeBoosts()
+ }
+
+ #summarizeBoosts() {
+ const el = document.createElement("span")
+ el.textContent = this.#boostSumaries.toSentence()
+ this.element.appendChild(el)
+ }
+
+ get #boostSumaries() {
+ return Object.entries(this.#boostsByCreator).map(([_creatorId, boostEvents]) => {
+ return `${boostEvents[0].dataset.creatorName} +${boostEvents.length}`
+ })
+ }
+
+ get #boostsByCreator() {
+ return this.boostEventTargets.reduce((acc, target) => {
+ const creatorId = target.dataset.creatorId
+
+ if (!acc[creatorId]) acc[creatorId] = []
+
+ acc[creatorId].push(target)
+
+ return acc
+ }, {})
+ }
+}
diff --git a/app/javascript/extensions/array_extensions.js b/app/javascript/extensions/array_extensions.js
new file mode 100644
index 000000000..9e8b8822a
--- /dev/null
+++ b/app/javascript/extensions/array_extensions.js
@@ -0,0 +1,17 @@
+Array.prototype.toSentence = function() {
+ const length = this.length
+
+ switch (length) {
+ case 0:
+ return ""
+ case 1:
+ return `${this[0]}.`
+ case 2:
+ return `${this[0]} and ${this[1]}.`
+ default:
+ return this.map((item, index) => {
+ const isLast = index === length - 1
+ return isLast ? `and ${item}.` : item
+ }).join(", ")
+ }
+}
diff --git a/app/javascript/extensions/index.js b/app/javascript/extensions/index.js
new file mode 100644
index 000000000..5424290ea
--- /dev/null
+++ b/app/javascript/extensions/index.js
@@ -0,0 +1 @@
+import "extensions/array_extensions"
diff --git a/app/views/bubbles/show.html.erb b/app/views/bubbles/show.html.erb
index a43755cd0..30b626e64 100644
--- a/app/views/bubbles/show.html.erb
+++ b/app/views/bubbles/show.html.erb
@@ -14,8 +14,13 @@
<% end %>
-
- <%= render "bubbles/bubble", bubble: @bubble %>
-
+<%= cache @bubble do %>
+
+ <%= render "bubbles/bubble", bubble: @bubble %>
+
-<%= render @bubble.thread %>
+
+<% end %>
diff --git a/app/views/bubbles/threads/_rollup.html.erb b/app/views/bubbles/threads/_rollup.html.erb
deleted file mode 100644
index adbfd5138..000000000
--- a/app/views/bubbles/threads/_rollup.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/app/views/bubbles/threads/_thread.html.erb b/app/views/bubbles/threads/_thread.html.erb
deleted file mode 100644
index 9d8103bb1..000000000
--- a/app/views/bubbles/threads/_thread.html.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-<% bubble = thread.bubble %>
-
-
diff --git a/app/views/events/_event.html.erb b/app/views/events/_event.html.erb
new file mode 100644
index 000000000..4979f8d80
--- /dev/null
+++ b/app/views/events/_event.html.erb
@@ -0,0 +1,5 @@
+<%= cache event do %>
+ <%# Template Dependency: events/actions/* %>
+ <%= render "events/actions/#{event.action}", event: event %>
+<% end %>
+
diff --git a/app/views/events/actions/_assigned.html.erb b/app/views/events/actions/_assigned.html.erb
new file mode 100644
index 000000000..52ba8fa07
--- /dev/null
+++ b/app/views/events/actions/_assigned.html.erb
@@ -0,0 +1 @@
+Assigned to <%= event.assignee_names.to_sentence %> <%= time_ago_in_words(event.created_at) %> ago.
diff --git a/app/views/events/actions/_boosted.html.erb b/app/views/events/actions/_boosted.html.erb
new file mode 100644
index 000000000..39268e2b4
--- /dev/null
+++ b/app/views/events/actions/_boosted.html.erb
@@ -0,0 +1 @@
+<%= boost_event_tag event %>
diff --git a/app/views/events/actions/_created.html.erb b/app/views/events/actions/_created.html.erb
new file mode 100644
index 000000000..6c32028df
--- /dev/null
+++ b/app/views/events/actions/_created.html.erb
@@ -0,0 +1 @@
+Added by <%= event.creator_name %> <%= time_ago_in_words(event.created_at) %> ago.
diff --git a/app/views/events/actions/_staged.html.erb b/app/views/events/actions/_staged.html.erb
new file mode 100644
index 000000000..a9821ee79
--- /dev/null
+++ b/app/views/events/actions/_staged.html.erb
@@ -0,0 +1 @@
+<%= event.creator_name %> moved this to '<%= event.stage_name %>'.
diff --git a/app/views/events/actions/_unstaged.html.erb b/app/views/events/actions/_unstaged.html.erb
new file mode 100644
index 000000000..0a3ec87ce
--- /dev/null
+++ b/app/views/events/actions/_unstaged.html.erb
@@ -0,0 +1 @@
+<%= event.creator_name %> removed this from '<%= event.stage_name %>'.
diff --git a/app/views/rollups/_rollup.html.erb b/app/views/rollups/_rollup.html.erb
new file mode 100644
index 000000000..63cdce5f2
--- /dev/null
+++ b/app/views/rollups/_rollup.html.erb
@@ -0,0 +1,3 @@
+
+ <%= render rollup.events, cache: true %>
+
diff --git a/app/views/thread_entries/_thread_entry.html.erb b/app/views/thread_entries/_thread_entry.html.erb
new file mode 100644
index 000000000..5a2b1f00c
--- /dev/null
+++ b/app/views/thread_entries/_thread_entry.html.erb
@@ -0,0 +1 @@
+<%= render thread_entry.threadable %>
diff --git a/config/importmap.rb b/config/importmap.rb
index 768403738..d5256b3a2 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -7,3 +7,4 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/helpers", under: "helpers"
+pin_all_from "app/javascript/extensions", under: "extensions"