diff --git a/app/controllers/users/push_subscriptions_controller.rb b/app/controllers/users/push_subscriptions_controller.rb
index e4e46524..a3517a83 100644
--- a/app/controllers/users/push_subscriptions_controller.rb
+++ b/app/controllers/users/push_subscriptions_controller.rb
@@ -10,7 +10,7 @@ class Users::PushSubscriptionsController < ApplicationController
# Existing endpoints must pass current validations too
if subscription.valid?
- head save_while_signed_in(subscription) ? :ok : :unauthorized
+ head register_while_signed_in(subscription) ? :ok : :unauthorized
else
head :unprocessable_entity
end
@@ -27,12 +27,18 @@ class Users::PushSubscriptionsController < ApplicationController
end
# A password change ends every session and subscription. A request that authenticated just before it mustn't
- # put one back, so the session is checked under the same write lock as the save. Validation, which looks up
- # the endpoint's address, runs before taking the lock.
- def save_while_signed_in(subscription)
+ # put one back, so the session is checked under the same write lock as the save, and the endpoint is looked up
+ # again there so two tabs posting at once register it once. Validation, which resolves the endpoint's address,
+ # runs before taking the lock.
+ def register_while_signed_in(subscription)
Push::Subscription.transaction do
- Session.exists?(Current.session.id) &&
- (subscription.new_record? ? subscription.save!(validate: false) : subscription.touch)
+ if Session.exists?(Current.session.id)
+ if existing = @push_subscriptions.find_by(push_subscription_params)
+ existing.touch
+ else
+ subscription.save!(validate: false)
+ end
+ end
end
end
diff --git a/app/javascript/controllers/notifications_controller.js b/app/javascript/controllers/notifications_controller.js
index b3698a31..3ce53a5c 100644
--- a/app/javascript/controllers/notifications_controller.js
+++ b/app/javascript/controllers/notifications_controller.js
@@ -4,10 +4,8 @@ import { pageIsTurboPreview } from "helpers/turbo_helpers"
import { onNextEventLoopTick } from "helpers/timing_helpers"
import { getCookie, setCookie } from "lib/cookie"
-const PUSH_SUBSCRIPTION_SYNCED_KEY = "push-subscription-synced-session"
-
export default class extends Controller {
- static values = { subscriptionsUrl: String, session: String }
+ static values = { subscriptionsUrl: String }
static targets = [ "notAllowedNotice", "bell", "details" ]
static classes = [ "attention" ]
@@ -27,8 +25,6 @@ export default class extends Controller {
this.#showBellAlert()
}
}
-
- this.#resyncPushSubscriptionOncePerSession()
}
}
@@ -113,21 +109,6 @@ export default class extends Controller {
if (!response.ok) subscription.unsubscribe()
}
- // A password change removes the account's subscriptions, the one this browser holds included. Post it again
- // once per session, so a browser that's still signed in keeps getting notifications. A failure here leaves the
- // browser's own subscription alone: the next session tries again.
- async #resyncPushSubscriptionOncePerSession() {
- if (this.#allowed && this.sessionValue && localStorage.getItem(PUSH_SUBSCRIPTION_SYNCED_KEY) !== this.sessionValue) {
- const registration = await this.#serviceWorkerRegistration
- const subscription = await registration?.pushManager?.getSubscription()
-
- if (Notification.permission === "granted" && subscription) {
- await post(this.subscriptionsUrlValue, { body: this.#extractJsonPayloadAsString(subscription), responseKind: "turbo-stream" })
- localStorage.setItem(PUSH_SUBSCRIPTION_SYNCED_KEY, this.sessionValue)
- }
- }
- }
-
async #requestPermissionAndSubscribe(registration) {
const permission = await Notification.requestPermission()
if (permission === "granted") this.#subscribe(registration)
diff --git a/app/javascript/controllers/push_subscription_sync_controller.js b/app/javascript/controllers/push_subscription_sync_controller.js
new file mode 100644
index 00000000..d2636523
--- /dev/null
+++ b/app/javascript/controllers/push_subscription_sync_controller.js
@@ -0,0 +1,32 @@
+import { Controller } from "@hotwired/stimulus"
+import { post } from "@rails/request.js"
+
+const SYNCED_SESSION_KEY = "push-subscription-synced-session"
+
+// A password change removes the account's push subscriptions, the one this browser holds included. Post it again
+// once per session, so a browser that's still signed in keeps getting notifications. A failure here leaves the
+// browser's own subscription alone: the next session tries again.
+export default class extends Controller {
+ static values = { url: String, session: String }
+
+ async connect() {
+ if (this.#allowed && this.sessionValue && localStorage.getItem(SYNCED_SESSION_KEY) !== this.sessionValue) {
+ const registration = await navigator.serviceWorker.getRegistration(window.location.origin)
+ const subscription = await registration?.pushManager?.getSubscription()
+
+ if (Notification.permission === "granted" && subscription) {
+ await post(this.urlValue, { body: this.#payload(subscription), responseKind: "turbo-stream" })
+ localStorage.setItem(SYNCED_SESSION_KEY, this.sessionValue)
+ }
+ }
+ }
+
+ get #allowed() {
+ return navigator.serviceWorker && window.Notification
+ }
+
+ #payload(subscription) {
+ const { endpoint, keys: { p256dh, auth } } = subscription.toJSON()
+ return JSON.stringify({ push_subscription: { endpoint, p256dh_key: p256dh, auth_key: auth } })
+ }
+}
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 7e2f73c5..b901b380 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -62,6 +62,11 @@
<%= render "layouts/lightbox" %>
+ <% if Current.session %>
+ <%= tag.div hidden: true, data: { controller: "push-subscription-sync",
+ push_subscription_sync_url_value: user_push_subscriptions_path, push_subscription_sync_session_value: push_subscription_session_marker } %>
+ <% end %>
+
<%= image_tag "campfire-icon.png", alt: "Campfire logo", width: 256, height: 216 %>
diff --git a/app/views/rooms/involvements/_bell.html.erb b/app/views/rooms/involvements/_bell.html.erb
index 266bdfb0..d9fa32c5 100644
--- a/app/views/rooms/involvements/_bell.html.erb
+++ b/app/views/rooms/involvements/_bell.html.erb
@@ -1,7 +1,6 @@
+ data-controller="notifications" data-notifications-subscriptions-url-value="<%= user_push_subscriptions_path %>" data-notifications-attention-class="btn--pulsing">
<%= turbo_frame_tag dom_id(room, :involvement), data: {
controller: "turbo-frame", action: "notifications:ready@window->turbo-frame#load", turbo_frame_url_param: room_involvement_path(room) } do %>