diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css
index 878eb25c5..b50e35a2c 100644
--- a/app/assets/stylesheets/notifications.css
+++ b/app/assets/stylesheets/notifications.css
@@ -40,6 +40,13 @@
}
}
+ /* Grouped notifications
+ /* ------------------------------------------------------------------------ */
+
+ .notification--grouped {
+ border: 3px solid red;
+ }
+
/* Read items
/* ------------------------------------------------------------------------ */
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index c557624be..69eab11a6 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -20,7 +20,10 @@ module NotificationsHelper
end
def notification_tag(notification, &)
- tag.div id: dom_id(notification), class: "tray__item tray__item--notification", data: { navigable_list_target: "item" } do
+ tag.div id: dom_id(notification), class: "tray__item tray__item--notification", data: {
+ navigable_list_target: "item",
+ notifications_tray_target: "notification",
+ card_id: notification.card.id, timestamp: notification.created_at.to_i } do
concat(
link_to(notification,
class: [ "card card--notification", { "card--closed": notification.card.closed? }, { "unread": !notification.read? } ],
diff --git a/app/javascript/controllers/notifications_tray_controller.js b/app/javascript/controllers/notifications_tray_controller.js
new file mode 100644
index 000000000..b356c4b20
--- /dev/null
+++ b/app/javascript/controllers/notifications_tray_controller.js
@@ -0,0 +1,57 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = [ "notification", "hiddenNotifications" ]
+ static classes = [ "grouped" ]
+
+ notificationTargetConnected(notification) {
+ this.#groupNotificationIfNeeded(notification)
+ }
+
+ #groupNotificationIfNeeded(notification) {
+ const groupedNotifications = this.#groupedNotificationsFor(notification)
+
+ if (groupedNotifications.length > 1) {
+ this.#renderGroup(groupedNotifications)
+ }
+ }
+
+ #groupedNotificationsFor(notification) {
+ const cardId = notification.dataset.cardId
+ return this.notificationTargets
+ .filter(notification => notification.dataset.cardId === cardId)
+ .sort((notification_1, notification_2) => parseInt(notification_1.dataset.timestamp) - parseInt(notification_2.dataset.timestamp))
+ }
+
+ #renderGroup(groupedNotifications) {
+ this.#showAsGrouped(groupedNotifications[0], groupedNotifications.length)
+ groupedNotifications.slice(1).forEach(notification => this.#hideInGroup(notification))
+ }
+
+ #showAsGrouped(notification, size) {
+ notification.classList.add(this.groupedClass)
+ this.#showGroupedNotification(notification)
+ this.#setGroupCount(notification, size)
+ }
+
+ #hideInGroup(notification) {
+ this.#hideGroupedNotification(notification)
+ this.#setGroupCount(notification, "")
+ }
+
+ // We use a hidden container instead of hiding the notifications directly so that the CSS that sort the
+ // tray element indexes doesn't get messed up.
+ #showGroupedNotification(notification) {
+ if (notification.parentElement === this.hiddenNotificationsTarget) {
+ this.hiddenNotificationsTarget.parentElement.insertBefore(notification, this.hiddenNotificationsTarget)
+ }
+ }
+
+ #hideGroupedNotification(notification) {
+ this.hiddenNotificationsTarget.appendChild(notification)
+ }
+
+ #setGroupCount(notification, count) {
+ notification.querySelector("[data-group-count]").textContent = count
+ }
+}
diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb
index b945960b8..49abafcb6 100644
--- a/app/views/notifications/_notification.html.erb
+++ b/app/views/notifications/_notification.html.erb
@@ -3,6 +3,7 @@