diff --git a/Gemfile b/Gemfile index 5c3b0ff65..fdeb0ea2b 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,7 @@ gem "thruster", require: false # Features gem "bcrypt", "~> 3.1.7" +gem "geared_pagination", "~> 1.2" gem "rqrcode" gem "redcarpet" gem "rouge" diff --git a/Gemfile.lock b/Gemfile.lock index 26b1c521b..cff4ceb78 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,6 +157,9 @@ GEM fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) + geared_pagination (1.2.0) + activesupport (>= 5.0) + addressable (>= 2.5.0) globalid (1.2.1) activesupport (>= 6.1) hotwire-spark (0.1.12) @@ -397,6 +400,7 @@ DEPENDENCIES brakeman capybara debug + geared_pagination (~> 1.2) hotwire-spark hotwire_combobox! importmap-rails diff --git a/app/assets/images/bell.svg b/app/assets/images/bell.svg new file mode 100644 index 000000000..52a5611fb --- /dev/null +++ b/app/assets/images/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css index 4b58106a9..a41f36635 100644 --- a/app/assets/stylesheets/notifications.css +++ b/app/assets/stylesheets/notifications.css @@ -1,84 +1,136 @@ .notification-tray { - height: 4rem; - inset: auto auto var(--block-space); + --size: 38ch; + + block-size: calc(4ch + var(--block-space-double)); + inline-size: var(--size); + inset: auto var(--inline-space) var(--block-space-half) auto; + padding: var(--block-space-half) var(--inline-space); position: fixed; + z-index: 1; + + .notification { + --outline-color: var(--color-link); + --offset: calc((var(--position) - 1) * (var(--block-space) * -1)); + --position: 1; + --scale: calc(1 - var(--position) / 30); + --z: calc(6 - var(--position)); + + inset: auto auto 0; + inline-size: var(--size); + pointer-events: none; + position: absolute; + scale: var(--scale); + transform: translateY(var(--offset)); + transform-origin: center; + transition: scale 0.2s ease-out, transform 0.2s ease-out; + z-index: var(--z); + + &:nth-child(1) { --position: 1; } + &:nth-child(2) { --position: 2; } + &:nth-child(3) { --position: 3; } + &:nth-child(4) { --position: 4; } + &:nth-child(5) { --position: 5; } + &:nth-child(6) { --position: 6; } + + &:nth-child(1n + 7) { display: none; } + + .notification-tray[open] & { + --scale: 1; + --offset: calc((100% + var(--block-space-half)) * (var(--position) * -1)); + + margin-block-end: calc(var(--block-space) * -1); + pointer-events: unset; + } + } } -.notification { - inset: auto auto 0; +.notification-tray__actions { + opacity: 0; + scale: calc(1 - 1 / 30); + transform: translateY(100%); + transition: opacity 0.2s ease-out, scale 0.2s ease-out, transform 0.2s ease-out; + + .notification-tray[open] & { + opacity: 1; + scale: 1; + transform: translateY(0); + } + + .notification-tray:has(.notification) & { + .btn img { + display: none; + } + } + + .notification-tray:not(:has(.notification)) & { + opacity: 1; + scale: 1; + transform: none; + transition: none; + + .notification-tray__all_action { + --btn-border-radius: 50%; + --btn-padding: 0; + + aspect-ratio: 1; + block-size: var(--btn-size); + display: grid; + inline-size: var(--btn-size); + place-items: center; + + > * { + grid-area: 1/1; + } + + span { + display: none; + } + } + + .notification-tray__read_action { + display: none; + } + } +} + +.notification__content { + color: var(--color-ink); + + .notificiations-list--read & { + border: 1px solid var(--color-subtle); + box-shadow: none; + } +} + +.notification-tray__expander { + --hover-size: 0; + --outline-size: 0; + + cursor: pointer; + inset: 0; position: absolute; - transform: translate(var(--offsetX), var(--offsetY)); - transform-origin: center center; - transition: transform 0.2s ease-in-out; - z-index: var(--z-index); + z-index: 6; - .notification__content { - color: var(--color-ink); - inline-size: var(--width); - } - - &:nth-child(1) { - --offsetX: 0; - --offsetY: 0; - --width: 40ch; - --z-index: 0; - } - - &:nth-child(2) { - --offsetX: 0.5ch; - --offsetY: calc(var(--block-space-half) * -1); - --width: 39ch; - --z-index: -1; - } - - &:nth-child(3) { - --offsetX: 1ch; - --offsetY: calc(var(--block-space) * -1); - --width: 38ch; - --z-index: -2; - } - - &:nth-child(4) { - --offsetX: 1.5ch; - --offsetY: calc(var(--block-space) * -1.5); - --width: 37ch; - --z-index: -3; - } - - &:nth-child(5) { - --offsetX: 2ch; - --offsetY: calc(var(--block-space) * -2); - --width: 36ch; - --z-index: -4; - } - - &:nth-child(1n + 6) { + .notification-tray[open] &, + .notification-tray:not(:has(.notification)) & { display: none; } +} - .notification-tray:hover & { - &:nth-child(2) { - --offsetX: 0; - --offsetY: calc((100% + var(--block-space-half)) * -1); - --width: 40ch; - } +.notification__unread_indicator { + --size: 0.8em; - &:nth-child(3) { - --offsetX: 0; - --offsetY: calc((100% + var(--block-space-half)) * -2); - --width: 40ch; - } + padding-block-start: var(--size); - &:nth-child(4) { - --offsetX: 0; - --offsetY: calc((100% + var(--block-space-half)) * -3); - --width: 40ch; - } + .dot { + aspect-ratio: 1; + background-color: var(--color-marker); + border-radius: 50%; + block-size: var(--size); + inline-size: var(--size); + } - &:nth-child(5) { - --offsetX: 0; - --offsetY: calc((100% + var(--block-space-half)) * -4); - --width: 40ch; - } + .notificiations-list--read & { + display: none; } } diff --git a/app/controllers/notifications/mark_all_reads_controller.rb b/app/controllers/notifications/mark_all_reads_controller.rb new file mode 100644 index 000000000..2aad42028 --- /dev/null +++ b/app/controllers/notifications/mark_all_reads_controller.rb @@ -0,0 +1,5 @@ +class Notifications::MarkAllReadsController < ApplicationController + def create + Current.user.notifications.unread.read_all + end +end diff --git a/app/controllers/notifications/trays_controller.rb b/app/controllers/notifications/trays_controller.rb new file mode 100644 index 000000000..4a01a1ee9 --- /dev/null +++ b/app/controllers/notifications/trays_controller.rb @@ -0,0 +1,5 @@ +class Notifications::TraysController < ApplicationController + def show + @notifications = Current.user.notifications.unread.ordered.limit(20) + end +end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 22e40167e..83e5db488 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,5 +1,9 @@ class NotificationsController < ApplicationController def index - @notifications = Current.user.notifications.unread.ordered.limit(20) + set_page_and_extract_portion_from Current.user.notifications.read.ordered + + if @page.first? + @unread = Current.user.notifications.unread.ordered + end end end diff --git a/app/controllers/readings_controller.rb b/app/controllers/readings_controller.rb index 9c5798ed3..fb7112b76 100644 --- a/app/controllers/readings_controller.rb +++ b/app/controllers/readings_controller.rb @@ -8,6 +8,6 @@ class ReadingsController < ApplicationController private def mark_bubble_notifications_read - Current.user.notifications.where(bubble: @bubble).update(read: true) + Current.user.notifications.where(bubble: @bubble).read_all end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index ddc142efb..bb0dade2a 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -25,6 +25,12 @@ module NotificationsHelper data: { turbo_frame: "_top" }, & end + def notifications_next_page_link(page) + unless @page.last? + tag.div id: "next_page", data: { controller: "fetch-on-visible", fetch_on_visible_url_value: notifications_path(page: @page.next_param) } + end + end + private def notification_event_action(notification) if notification_is_for_initial_assignement?(notification) diff --git a/app/javascript/controllers/beacon_controller.js b/app/javascript/controllers/beacon_controller.js index 07182bb36..794565f78 100644 --- a/app/javascript/controllers/beacon_controller.js +++ b/app/javascript/controllers/beacon_controller.js @@ -7,4 +7,8 @@ export default class extends Controller { connect() { post(this.urlValue, { responseKind: "turbo-stream" }) } + + disconnect() { + post(this.urlValue, { responseKind: "turbo-stream" }) + } } diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js index dcea9bdf9..fd4d122e0 100644 --- a/app/javascript/controllers/dialog_controller.js +++ b/app/javascript/controllers/dialog_controller.js @@ -26,6 +26,7 @@ export default class extends Controller { close() { this.dialogTarget.close() + this.dialogTarget.blur() } closeOnClickOutside({ target }) { diff --git a/app/javascript/controllers/fetch_on_visible_controller.js b/app/javascript/controllers/fetch_on_visible_controller.js new file mode 100644 index 000000000..42f1a36da --- /dev/null +++ b/app/javascript/controllers/fetch_on_visible_controller.js @@ -0,0 +1,25 @@ +import { Controller } from "@hotwired/stimulus" +import { get } from "@rails/request.js" + +export default class extends Controller { + static values = { url: String } + + connect() { + this.#observe() + } + + #observe() { + const observer = new IntersectionObserver((entries) => { + const visible = !!entries.find(entry => entry.isIntersecting) + if (visible) { + this.#fetch() + } + }) + + observer.observe(this.element) + } + + #fetch() { + get(this.urlValue, { responseKind: "turbo-stream" }) + } +} diff --git a/app/models/notification.rb b/app/models/notification.rb index 531be12bb..05d38c940 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -4,10 +4,21 @@ class Notification < ApplicationRecord belongs_to :bubble belongs_to :resource, polymorphic: true - scope :unread, -> { where(read: false) } - scope :ordered, -> { order(read: :desc, created_at: :desc) } + scope :unread, -> { where(read_at: nil) } + scope :read, -> { where.not(read_at: nil) } + scope :ordered, -> { order(read_at: :desc, created_at: :desc) } delegate :creator, to: :event broadcasts_to ->(notification) { [ notification.user, :notifications ] }, inserts_by: :prepend + + class << self + def read_all + update!(read_at: Time.current) + end + end + + def read? + read_at.present? + end end diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index cba5a252f..d309015b3 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,13 +1,17 @@ <% cache notification do %> <%= notification_tag notification do %>