3a7c3f7af2
* Add diagnostics settings & endpoint Stackers can now help us to identify and fix bugs by enabling diagnostics. This will send anonymized data to us. For now, this is only used to send events around push notifications. * Send diagnostics to slack * Detect OS * Diagnostics data is only pseudonymous, not anonymous It's only pseudonymous since with additional knowledge (which stacker uses which fancy name), we could trace the events back to individual stackers. Data is only anonymous if this is not possible - it must be irreversible. * Check if window.navigator is defined * Use Slack SDK * Catch errors of slack requests --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
150 lines
6.1 KiB
JavaScript
150 lines
6.1 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'
|
|
|
|
// comment out to enable workbox console logs
|
|
self.__WB_DISABLE_DEV_LOGS = true
|
|
|
|
const storage = new ServiceWorkerStorage('sw:storage', 1)
|
|
let messageChannelPort
|
|
|
|
// 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)
|
|
|
|
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) {
|
|
const message = `[sw:push] more than one notification with tag ${tag} found`
|
|
messageChannelPort?.postMessage({ level: 'error', message })
|
|
console.error(message)
|
|
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 === 'MESSAGE_PORT') {
|
|
messageChannelPort = event.ports[0]
|
|
}
|
|
messageChannelPort?.postMessage({ message: '[sw:message] received message', context: { action: event.data.action } })
|
|
if (event.data.action === 'STORE_SUBSCRIPTION') {
|
|
messageChannelPort?.postMessage({ message: '[sw:message] storing subscription in IndexedDB', context: { endpoint: event.data.subscription.endpoint } })
|
|
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
|
|
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] invoked' })
|
|
oldSubscription ??= await storage.getItem('subscription')
|
|
newSubscription ??= await self.registration.pushManager.getSubscription()
|
|
if (!newSubscription) {
|
|
// no subscription exists at the moment
|
|
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] no existing subscription found' })
|
|
return
|
|
}
|
|
if (oldSubscription?.endpoint === newSubscription.endpoint) {
|
|
// subscription did not change. no need to sync with server
|
|
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] old subscription matches existing subscription' })
|
|
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
|
|
})
|
|
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] synced push subscription with server', context: { endpoint: variables.endpoint, oldEndpoint: variables.oldEndpoint } })
|
|
await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription)))
|
|
}
|
|
|
|
self.addEventListener('pushsubscriptionchange', (event) => {
|
|
messageChannelPort?.postMessage({ message: '[sw:pushsubscriptionchange] received event' })
|
|
event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription))
|
|
}, false)
|