101 lines
4.1 KiB
JavaScript
101 lines
4.1 KiB
JavaScript
// The service worker of one persona's Web Push subscription. wwwroot/js/push.js registers this script once per persona
|
|||
|
|
// the reader turns notifications on for, at the scope push/{slot}/ (a random name this device keeps in localStorage's
|
||
|
|
// PushDevice, Services/PushService.cs), so each persona has a push subscription of its own: its own endpoint and keys,
|
||
|
|
// which PrivaPub stores with that persona's token alone. The same file serves development and published builds; the
|
||
|
|
// app's own workers (service-worker.js, service-worker.published.js) receive no pushes. It controls no page, so it has no
|
||
|
|
// fetch handler.
|
||
|
|
//
|
||
|
|
// A push shows the payload's title, body and icon, tagged with its notification id so a repeat replaces it. The payload
|
||
|
|
// is Mastodon's, so it also carries the subscription's access token: that is never read, kept, shown or passed on here.
|
||
|
|
// A click opens decePub on the notifications of the persona this registration is for: an open decePub tab is focused and
|
||
|
|
// told which (main.js, CascadingState.PushOpened), else a new one opens at ?notifications=1&push={slot}.
|
||
|
|
|
||
|
|
const appRoot = new URL("./", self.location).href
|
||
|
|
const slot = new URL(self.registration.scope).pathname.split("/").filter(part => part.length > 0).pop()
|
||
|
|
|
||
|
|
self.addEventListener("install", () => self.skipWaiting())
|
||
|
|
|
||
|
|
self.addEventListener("push", event => event.waitUntil(show(event.data)))
|
||
|
|
|
||
|
|
self.addEventListener("notificationclick", event => {
|
||
|
|
event.notification.close()
|
||
|
|
event.waitUntil(open())
|
||
|
|
})
|
||
|
|
|
||
|
|
// the push service changed the subscription: the same key subscribes again here, and the app tells PrivaPub the new
|
||
|
|
// endpoint the next time it starts (PushService.Reconcile), since this worker holds no token to tell it with
|
||
|
|
self.addEventListener("pushsubscriptionchange", event => event.waitUntil(renew(event)))
|
||
|
|
|
||
|
|
async function show(data) {
|
||
|
|
let title = "decePub"
|
||
|
|
const options = { icon: appRoot + "imgs/icon-192.png", timestamp: Date.now(), data: {} }
|
||
|
|
try {
|
||
|
|
const payload = data ? data.json() : null
|
||
|
|
if (payload && typeof payload == "object") {
|
||
|
|
if (typeof payload.title == "string" && payload.title.length > 0)
|
||
|
|
title = payload.title
|
||
|
|
if (typeof payload.body == "string")
|
||
|
|
options.body = payload.body
|
||
|
|
if (isWebAddress(payload.icon))
|
||
|
|
options.icon = payload.icon
|
||
|
|
if (payload.notification_id != null)
|
||
|
|
options.tag = String(payload.notification_id)
|
||
|
|
if (typeof payload.preferred_locale == "string")
|
||
|
|
options.lang = payload.preferred_locale
|
||
|
|
options.data = { notificationId: options.tag ?? null, type: typeof payload.notification_type == "string" ? payload.notification_type : null }
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error(e)
|
||
|
|
}
|
||
|
|
// a push always shows something (userVisibleOnly): one that can't be read shows decePub's name alone
|
||
|
|
await self.registration.showNotification(title, options)
|
||
|
|
try {
|
||
|
|
// the count is the app's to say (Index.razor); until it opens, a dot says something waits
|
||
|
|
await self.navigator.setAppBadge?.()
|
||
|
|
} catch (e) {
|
||
|
|
console.debug(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function isWebAddress(value) {
|
||
|
|
if (typeof value != "string")
|
||
|
|
return false
|
||
|
|
try {
|
||
|
|
const url = new URL(value)
|
||
|
|
return url.protocol === "https:" || url.protocol === "http:"
|
||
|
|
} catch {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function open() {
|
||
|
|
const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true })
|
||
|
|
const app = windows.filter(client => client.url.startsWith(appRoot))
|
||
|
|
const target = app.find(client => client.focused) ?? app.find(client => client.visibilityState === "visible") ?? app[0]
|
||
|
|
if (target) {
|
||
|
|
try {
|
||
|
|
await target.focus()
|
||
|
|
} catch (e) {
|
||
|
|
console.error(e)
|
||
|
|
}
|
||
|
|
// (navigate() is only for the worker that controls the tab: the app's own one does)
|
||
|
|
target.postMessage({ type: "decepub-push-open", slot })
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const url = new URL(appRoot)
|
||
|
|
url.searchParams.set("notifications", "1")
|
||
|
|
url.searchParams.set("push", slot)
|
||
|
|
await self.clients.openWindow(url.href)
|
||
|
|
}
|
||
|
|
|
||
|
|
async function renew(event) {
|
||
|
|
try {
|
||
|
|
const key = event.oldSubscription?.options?.applicationServerKey
|
||
|
|
if (!key || event.newSubscription)
|
||
|
|
return
|
||
|
|
await self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key })
|
||
|
|
} catch (e) {
|
||
|
|
console.error(e)
|
||
|
|
}
|
||
|
|
}
|