mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-28 00:16:48 +09:00
Only room pages posted the subscription again, so a browser that changed its password and closed the profile page went without notifications until it next opened a room. A small controller in the layout does it now, still once per session, and the bell is back to how it was. Two tabs posting at once after a password change could both miss the row and insert two, so the endpoint is looked up again under the same write lock as the session check.
166 lines
4.8 KiB
JavaScript
166 lines
4.8 KiB
JavaScript
import { Controller } from "@hotwired/stimulus"
|
|
import { post } from "@rails/request.js"
|
|
import { pageIsTurboPreview } from "helpers/turbo_helpers"
|
|
import { onNextEventLoopTick } from "helpers/timing_helpers"
|
|
import { getCookie, setCookie } from "lib/cookie"
|
|
|
|
export default class extends Controller {
|
|
static values = { subscriptionsUrl: String }
|
|
static targets = [ "notAllowedNotice", "bell", "details" ]
|
|
static classes = [ "attention" ]
|
|
|
|
async connect() {
|
|
if (!pageIsTurboPreview()) {
|
|
if (window.notificationsPreviouslyReady) {
|
|
onNextEventLoopTick(() => this.dispatch("ready"))
|
|
} else {
|
|
const firstTimeReady = await this.isEnabled()
|
|
|
|
this.#pulseBellButton()
|
|
|
|
if (firstTimeReady) {
|
|
onNextEventLoopTick(() => this.dispatch("ready"))
|
|
window.notificationsPreviouslyReady = true
|
|
} else {
|
|
this.#showBellAlert()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async attemptToSubscribe() {
|
|
if (this.#allowed) {
|
|
const registration = await this.#serviceWorkerRegistration || await this.#registerServiceWorker()
|
|
|
|
switch(Notification.permission) {
|
|
case "denied": { this.#revealNotAllowedNotice(); break }
|
|
case "granted": { this.#subscribe(registration); break }
|
|
case "default": { this.#requestPermissionAndSubscribe(registration) }
|
|
}
|
|
} else {
|
|
this.#revealNotAllowedNotice()
|
|
}
|
|
|
|
this.#endFirstRun()
|
|
}
|
|
|
|
async isEnabled() {
|
|
if (this.#allowed) {
|
|
const registration = await this.#serviceWorkerRegistration
|
|
const existingSubscription = await registration?.pushManager?.getSubscription()
|
|
|
|
return Notification.permission == "granted" && registration && existingSubscription
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
get #allowed() {
|
|
return navigator.serviceWorker && window.Notification
|
|
}
|
|
|
|
get #serviceWorkerRegistration() {
|
|
return navigator.serviceWorker.getRegistration(window.location.origin)
|
|
}
|
|
|
|
#registerServiceWorker() {
|
|
return navigator.serviceWorker.register("/service-worker.js")
|
|
}
|
|
|
|
#revealNotAllowedNotice() {
|
|
this.notAllowedNoticeTarget.showModal()
|
|
this.#openSingleOption()
|
|
}
|
|
|
|
#openSingleOption() {
|
|
const visibleElements = this.detailsTargets.filter(item => !this.#isHidden(item))
|
|
|
|
if (visibleElements.length === 1) {
|
|
this.detailsTargets.forEach(item => item.toggleAttribute("open", item === visibleElements[0]))
|
|
}
|
|
}
|
|
|
|
#showBellAlert() {
|
|
this.bellTarget.querySelectorAll("img").forEach(img => img.toggleAttribute("hidden"))
|
|
}
|
|
|
|
#pulseBellButton() {
|
|
if (!this.#hasSeenFirstRun) {
|
|
this.bellTarget.classList.add(this.attentionClass)
|
|
}
|
|
}
|
|
|
|
#endFirstRun() {
|
|
this.bellTarget.classList.remove(this.attentionClass)
|
|
this.#markFirstRunSeen()
|
|
}
|
|
|
|
async #subscribe(registration) {
|
|
registration.pushManager
|
|
.subscribe({ userVisibleOnly: true, applicationServerKey: this.#vapidPublicKey })
|
|
.then(subscription => {
|
|
this.#syncPushSubscription(subscription)
|
|
this.dispatch("ready")
|
|
})
|
|
}
|
|
|
|
async #syncPushSubscription(subscription) {
|
|
const response = await post(this.subscriptionsUrlValue, { body: this.#extractJsonPayloadAsString(subscription), responseKind: "turbo-stream" })
|
|
if (!response.ok) subscription.unsubscribe()
|
|
}
|
|
|
|
async #requestPermissionAndSubscribe(registration) {
|
|
const permission = await Notification.requestPermission()
|
|
if (permission === "granted") this.#subscribe(registration)
|
|
}
|
|
|
|
get #vapidPublicKey() {
|
|
const encodedVapidPublicKey = document.querySelector('meta[name="vapid-public-key"]').content
|
|
return this.#urlBase64ToUint8Array(encodedVapidPublicKey)
|
|
}
|
|
|
|
get #hasSeenFirstRun() {
|
|
if (this.#isPWA) {
|
|
return getCookie("notifications-pwa-first-run-seen")
|
|
} else {
|
|
return getCookie("notifications-first-run-seen")
|
|
}
|
|
}
|
|
|
|
#markFirstRunSeen = (event) => {
|
|
if (this.#isPWA) {
|
|
setCookie("notifications-pwa-first-run-seen", true)
|
|
} else {
|
|
setCookie("notifications-first-run-seen", true)
|
|
}
|
|
}
|
|
|
|
#extractJsonPayloadAsString(subscription) {
|
|
const { endpoint, keys: { p256dh, auth } } = subscription.toJSON()
|
|
return JSON.stringify({ push_subscription: { endpoint, p256dh_key: p256dh, auth_key: auth } })
|
|
}
|
|
|
|
// VAPID public key comes encoded as base64 but service worker registration needs it as a Uint8Array
|
|
#urlBase64ToUint8Array(base64String) {
|
|
const padding = "=".repeat((4 - base64String.length % 4) % 4)
|
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/")
|
|
|
|
const rawData = window.atob(base64)
|
|
const outputArray = new Uint8Array(rawData.length)
|
|
|
|
for (let i = 0; i < rawData.length; ++i) {
|
|
outputArray[i] = rawData.charCodeAt(i)
|
|
}
|
|
|
|
return outputArray
|
|
}
|
|
|
|
#isHidden(item) {
|
|
return (item.offsetParent === null)
|
|
}
|
|
|
|
get #isPWA() {
|
|
return window.matchMedia("(display-mode: standalone)").matches
|
|
}
|
|
}
|