* Convert all top-level arrow functions to regular functions * Refactor webPush.sendNotification call * Refactor webPush logging * Rename var to title * Rewrite service worker This rewrite simplifies the service worker by removing * merging of push notifications via tag property * badge count These features weren't properly working on iOS. We concluded that we don't really need them. For example, this means replies will no longer get merged to "you have X new replies" but show up as individual notifications. Only zaps still use the tag property so devices that support it can still replace any previous "your post stacked X sats" notification for the same item. * Don't use async/await in service worker * Support app badge count * Fix extremely slow notificationclick * Fix serialization and save in pushsubscriptionchange event
217 lines
7.8 KiB
JavaScript
217 lines
7.8 KiB
JavaScript
/* global self */
|
|
import { precacheAndRoute } from 'workbox-precaching'
|
|
import { offlineFallback } from 'workbox-recipes'
|
|
import { setDefaultHandler } from 'workbox-routing'
|
|
import { NetworkOnly } from 'workbox-strategies'
|
|
import { enable } from 'workbox-navigation-preload'
|
|
|
|
import manifest from './precache-manifest.json'
|
|
import ServiceWorkerStorage from 'serviceworker-storage'
|
|
import { CLEAR_NOTIFICATIONS, DELETE_SUBSCRIPTION, STORE_SUBSCRIPTION } from '@/components/serviceworker'
|
|
|
|
// we store existing push subscriptions for the onpushsubscriptionchange event
|
|
const storage = new ServiceWorkerStorage('sw:storage', 1)
|
|
|
|
// comment out to enable workbox console logs
|
|
self.__WB_DISABLE_DEV_LOGS = true
|
|
|
|
// preloading improves startup performance
|
|
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
|
|
enable()
|
|
|
|
// ignore precache manifest generated by InjectManifest
|
|
// they statically check for the presence of this variable
|
|
console.log(self.__WB_MANIFEST)
|
|
// precache the manifest we generated ourselves
|
|
precacheAndRoute(manifest)
|
|
|
|
// immediately replace existing service workers with this one
|
|
// (no wait until this one becomes active)
|
|
self.addEventListener('install', () => self.skipWaiting())
|
|
|
|
// Using network-only as the default strategy ensures that we fallback
|
|
// to the browser as if the service worker wouldn't exist.
|
|
// The browser may use own caching (HTTP cache).
|
|
// Also, the offline fallback only works if request matched a route
|
|
setDefaultHandler(new NetworkOnly({
|
|
plugins: [{
|
|
fetchDidFail: async (args) => {
|
|
// tell us why a request failed in dev
|
|
// process.env.NODE_ENV !== 'production' && console.log('fetch did fail', ...args)
|
|
},
|
|
fetchDidSucceed: async ({ request, response, event, state }) => {
|
|
if (
|
|
response.ok &&
|
|
request.headers.get('x-nextjs-data') &&
|
|
response.headers.get('x-nextjs-matched-path') &&
|
|
response.headers.get('content-type') === 'application/json' &&
|
|
response.headers.get('content-length') === '2' &&
|
|
response.status === 200) {
|
|
console.log('service worker detected a successful yet empty nextjs SSR data response')
|
|
console.log('nextjs has a bug where it returns a 200 with empty data when it should return a 404')
|
|
console.log('see https://github.com/vercel/next.js/issues/56852')
|
|
console.log('HACK ... intercepting response and returning 404')
|
|
|
|
const headers = new Headers(response.headers)
|
|
headers.delete('x-nextjs-matched-path')
|
|
headers.delete('content-type')
|
|
headers.delete('content-length')
|
|
return new Response(null, {
|
|
status: 404,
|
|
statusText: 'Not Found',
|
|
headers,
|
|
ok: false
|
|
})
|
|
}
|
|
return response
|
|
}
|
|
}]
|
|
}))
|
|
|
|
// This won't work in dev because pages are never cached.
|
|
// See https://github.com/vercel/next.js/blob/337fb6a9aadb61c916f0121c899e463819cd3f33/server/render.js#L181-L185
|
|
offlineFallback({ pageFallback: '/offline' })
|
|
|
|
self.addEventListener('push', function (event) {
|
|
let payload
|
|
|
|
try {
|
|
payload = event.data?.json()
|
|
if (!payload) {
|
|
throw new Error('no payload in push event')
|
|
}
|
|
} catch (err) {
|
|
// we show a default nofication on any error because we *must* show a notification
|
|
// else the browser will show one for us or worse, remove our push subscription
|
|
return event.waitUntil(
|
|
self.registration.showNotification(
|
|
// TODO: funny message as easter egg?
|
|
// example: "dude i'm bugging, that's wild" from https://www.youtube.com/watch?v=QsQLIaKK2s0&t=176s but in wild west theme?
|
|
'something went wrong',
|
|
{ icon: '/icons/icon_x96.png' }
|
|
)
|
|
)
|
|
}
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(payload.title, payload.options)
|
|
.then(() => self.registration.getNotifications())
|
|
.then(notifications => self.navigator.setAppBadge?.(notifications.length))
|
|
)
|
|
})
|
|
|
|
self.addEventListener('notificationclick', function (event) {
|
|
event.notification.close()
|
|
|
|
const promises = []
|
|
const url = event.notification.data?.url
|
|
if (url) {
|
|
// First try to find and focus an existing client before opening a new window
|
|
promises.push(
|
|
self.clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
.then(clients => {
|
|
if (clients.length > 0) {
|
|
const client = clients[0]
|
|
return client.focus()
|
|
.then(() => {
|
|
return client.postMessage({
|
|
type: 'navigate',
|
|
url
|
|
})
|
|
})
|
|
} else {
|
|
return self.clients.openWindow(url)
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
promises.push(
|
|
self.registration.getNotifications()
|
|
.then(notifications => self.navigator.setAppBadge?.(notifications.length))
|
|
)
|
|
|
|
event.waitUntil(Promise.all(promises))
|
|
})
|
|
|
|
// not supported by iOS
|
|
// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/notificationclose_event
|
|
self.addEventListener('notificationclose', function (event) {
|
|
event.waitUntil(
|
|
self.registration.getNotifications()
|
|
.then(notifications => self.navigator.setAppBadge?.(notifications.length))
|
|
)
|
|
})
|
|
|
|
self.addEventListener('pushsubscriptionchange', function (event) {
|
|
// https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event
|
|
const { oldSubscription, newSubscription } = event
|
|
|
|
return event.waitUntil(
|
|
Promise.all([
|
|
oldSubscription ?? storage.getItem('subscription'),
|
|
newSubscription ?? self.registration.pushManager.getSubscription()
|
|
])
|
|
.then(([oldSubscription, newSubscription]) => {
|
|
if (!newSubscription || oldSubscription?.endpoint === newSubscription.endpoint) {
|
|
// no subscription exists at the moment or subscription did not change
|
|
return
|
|
}
|
|
|
|
// convert keys from ArrayBuffer to string
|
|
newSubscription = JSON.parse(JSON.stringify(newSubscription))
|
|
|
|
// save new subscription on server
|
|
return Promise.all([
|
|
newSubscription,
|
|
fetch('/api/graphql', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
query: `
|
|
mutation savePushSubscription(
|
|
$endpoint: String!,
|
|
$p256dh: String!,
|
|
$auth: String!,
|
|
$oldEndpoint: String!
|
|
) {
|
|
savePushSubscription(
|
|
endpoint: $endpoint,
|
|
p256dh: $p256dh,
|
|
auth: $auth,
|
|
oldEndpoint: $oldEndpoint
|
|
) {
|
|
id
|
|
}
|
|
}`,
|
|
variables: {
|
|
endpoint: newSubscription.endpoint,
|
|
p256dh: newSubscription.keys.p256dh,
|
|
auth: newSubscription.keys.auth,
|
|
oldEndpoint: oldSubscription?.endpoint
|
|
}
|
|
})
|
|
})
|
|
])
|
|
}).then(([newSubscription]) => storage.setItem('subscription', newSubscription))
|
|
)
|
|
})
|
|
|
|
self.addEventListener('message', function (event) {
|
|
switch (event.data.action) {
|
|
case STORE_SUBSCRIPTION: return event.waitUntil(storage.setItem('subscription', { ...event.data.subscription, swVersion: 2 }))
|
|
case DELETE_SUBSCRIPTION: return event.waitUntil(storage.removeItem('subscription'))
|
|
case CLEAR_NOTIFICATIONS:
|
|
return event.waitUntil(
|
|
Promise.all([
|
|
self.registration.getNotifications()
|
|
.then(notifications => notifications.forEach(notification => notification.close())),
|
|
self.navigator.clearAppBadge?.()
|
|
])
|
|
)
|
|
}
|
|
})
|