e3c60d1ef8
Most browsers don't support the pushsubscriptionchange event. We workaround this by saving the current push subscription in IndexedDB so we can check during every page load if the push subscription changed. If that is the case, we manually sync the push subscription with the server. However, this solution is not perfect as mentioned in https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f which was used for reference: > This solution is not perfect, the user could lose some push notifications if he doesn’t open the webapp for a long time. Co-authored-by: ekzyis <ek@stacker.news>
139 lines
5.0 KiB
JavaScript
139 lines
5.0 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'
|
|
|
|
self.__WB_DISABLE_DEV_LOGS = true
|
|
|
|
const storage = new ServiceWorkerStorage('sw:storage', 1)
|
|
|
|
// preloading improves startup performance
|
|
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
|
|
enable()
|
|
|
|
// uncomment to disable workbox console logs
|
|
// self.__WB_DISABLE_DEV_LOGS = true
|
|
|
|
// ignore precache manifest generated by InjectManifest
|
|
// they statically check for the presence of this variable
|
|
console.log(self.__WB_MANIFEST)
|
|
|
|
precacheAndRoute(manifest)
|
|
|
|
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({
|
|
// tell us why a request failed in dev
|
|
plugins: [{
|
|
fetchDidFail: async (args) => {
|
|
process.env.NODE_ENV !== 'production' && console.log('fetch did fail', ...args)
|
|
}
|
|
}]
|
|
}))
|
|
|
|
// 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', async function (event) {
|
|
const payload = event.data?.json()
|
|
if (!payload) return
|
|
const { tag } = payload.options
|
|
event.waitUntil((async () => {
|
|
if (!['REPLY', 'MENTION'].includes(tag)) {
|
|
return self.registration.showNotification(payload.title, payload.options)
|
|
}
|
|
|
|
const notifications = await self.registration.getNotifications({ tag })
|
|
// since we used a tag filter, there should only be zero or one notification
|
|
if (notifications.length > 1) {
|
|
console.error(`more than one notification with tag ${tag} found`)
|
|
return null
|
|
}
|
|
if (notifications.length === 0) {
|
|
return self.registration.showNotification(payload.title, payload.options)
|
|
}
|
|
const currentNotification = notifications[0]
|
|
const amount = currentNotification.data?.amount ? currentNotification.data.amount + 1 : 2
|
|
let title = ''
|
|
if (tag === 'REPLY') {
|
|
title = `You have ${amount} new replies`
|
|
} else if (tag === 'MENTION') {
|
|
title = `You were mentioned ${amount} times`
|
|
}
|
|
currentNotification.close()
|
|
const { icon } = currentNotification
|
|
return self.registration.showNotification(title, { icon, tag, data: { url: '/notifications', amount } })
|
|
})())
|
|
})
|
|
|
|
self.addEventListener('notificationclick', (event) => {
|
|
const url = event.notification.data?.url
|
|
if (url) {
|
|
event.waitUntil(self.clients.openWindow(url))
|
|
}
|
|
event.notification.close()
|
|
})
|
|
|
|
// https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data.action === 'STORE_SUBSCRIPTION') {
|
|
return event.waitUntil(storage.setItem('subscription', event.data.subscription))
|
|
}
|
|
if (event.data.action === 'SYNC_SUBSCRIPTION') {
|
|
return event.waitUntil(handlePushSubscriptionChange())
|
|
}
|
|
})
|
|
|
|
async function handlePushSubscriptionChange (oldSubscription, newSubscription) {
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event
|
|
// fallbacks since browser may not set oldSubscription and newSubscription
|
|
oldSubscription ??= await storage.getItem('subscription')
|
|
newSubscription ??= await self.registration.pushManager.getSubscription()
|
|
if (!newSubscription) {
|
|
// no subscription exists at the moment
|
|
return
|
|
}
|
|
if (oldSubscription?.endpoint === newSubscription.endpoint) {
|
|
// subscription did not change. no need to sync with server
|
|
return
|
|
}
|
|
// convert keys from ArrayBuffer to string
|
|
newSubscription = JSON.parse(JSON.stringify(newSubscription))
|
|
const variables = {
|
|
endpoint: newSubscription.endpoint,
|
|
p256dh: newSubscription.keys.p256dh,
|
|
auth: newSubscription.keys.auth,
|
|
oldEndpoint: oldSubscription?.endpoint
|
|
}
|
|
const query = `
|
|
mutation savePushSubscription($endpoint: String!, $p256dh: String!, $auth: String!, $oldEndpoint: String!) {
|
|
savePushSubscription(endpoint: $endpoint, p256dh: $p256dh, auth: $auth, oldEndpoint: $oldEndpoint) {
|
|
id
|
|
}
|
|
}`
|
|
const body = JSON.stringify({ query, variables })
|
|
await fetch('/api/graphql', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-type': 'application/json'
|
|
},
|
|
body
|
|
})
|
|
await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription)))
|
|
}
|
|
|
|
self.addEventListener('pushsubscriptionchange', (event) => {
|
|
event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription))
|
|
}, false)
|