feat: pwa with push notifications (#337)
This commit is contained in:
parent
a18e5e2332
commit
f0c91a3974
48 changed files with 2903 additions and 14 deletions
65
service-worker/sw.ts
Normal file
65
service-worker/sw.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/// <reference lib="WebWorker" />
|
||||
/// <reference types="vite/client" />
|
||||
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
|
||||
import { NavigationRoute, registerRoute } from 'workbox-routing'
|
||||
import { onNotificationClick, onPush } from './web-push-notifications'
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope
|
||||
/*
|
||||
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
|
||||
import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
|
||||
import { ExpirationPlugin } from 'workbox-expiration'
|
||||
*/
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING')
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
const entries = self.__WB_MANIFEST
|
||||
if (import.meta.env.DEV)
|
||||
entries.push({ url: '/', revision: Math.random().toString() })
|
||||
|
||||
precacheAndRoute(entries)
|
||||
|
||||
// clean old assets
|
||||
cleanupOutdatedCaches()
|
||||
|
||||
// allow only fallback in dev: we don't want to cache anything
|
||||
let allowlist: undefined | RegExp[]
|
||||
if (import.meta.env.DEV)
|
||||
allowlist = [/^\/$/]
|
||||
|
||||
// deny api and server page calls
|
||||
let denylist: undefined | RegExp[]
|
||||
if (import.meta.env.PROD)
|
||||
denylist = [/^\/api\//, /^\/login\//, /^\/oauth\//, /^\/signin\//]
|
||||
|
||||
// only cache pages and external assets on local build + start or in production
|
||||
if (import.meta.env.PROD) {
|
||||
// external assets: rn avatars from mas.to
|
||||
// requires <img crossorigin="anonymous".../> and http header: Allow-Control-Allow-Origin: *
|
||||
/*
|
||||
registerRoute(
|
||||
({ sameOrigin, request }) => !sameOrigin && request.destination === 'image',
|
||||
new NetworkFirst({
|
||||
cacheName: 'elk-external-media',
|
||||
plugins: [
|
||||
// add opaque responses?
|
||||
new CacheableResponsePlugin({ statuses: [/!* 0, *!/200] }),
|
||||
// 15 days max
|
||||
new ExpirationPlugin({ maxAgeSeconds: 60 * 60 * 24 * 15 }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
*/
|
||||
}
|
||||
|
||||
// to allow work offline
|
||||
registerRoute(new NavigationRoute(
|
||||
createHandlerBoundToURL('/'),
|
||||
{ allowlist, denylist },
|
||||
))
|
||||
|
||||
self.addEventListener('push', onPush)
|
||||
self.addEventListener('notificationclick', onNotificationClick)
|
8
service-worker/tsconfig.json
Normal file
8
service-worker/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "WebWorker"],
|
||||
"types": ["vite/client", "service-worker"]
|
||||
},
|
||||
"include": ["./"],
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
9
service-worker/types.ts
Normal file
9
service-worker/types.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export interface PushPayload {
|
||||
access_token: string
|
||||
notification_id: string
|
||||
notification_type: 'follow' | 'favourite' | 'reblog' | 'mention' | 'poll'
|
||||
preferred_locale: string
|
||||
title: string
|
||||
body: string
|
||||
icon: string
|
||||
}
|
85
service-worker/web-push-notifications.ts
Normal file
85
service-worker/web-push-notifications.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/// <reference lib="WebWorker" />
|
||||
/// <reference types="vite/client" />
|
||||
import type { PushPayload } from '~/service-worker/types'
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope
|
||||
|
||||
export const onPush = (event: PushEvent) => {
|
||||
const promise = isClientFocused().then((isFocused) => {
|
||||
if (isFocused)
|
||||
return Promise.resolve()
|
||||
|
||||
const options: PushPayload = event.data!.json()
|
||||
const {
|
||||
access_token,
|
||||
body,
|
||||
icon,
|
||||
notification_id,
|
||||
notification_type,
|
||||
preferred_locale,
|
||||
} = options
|
||||
|
||||
let url = 'home'
|
||||
if (notification_type) {
|
||||
switch (notification_type) {
|
||||
case 'follow':
|
||||
url = 'notifications'
|
||||
break
|
||||
case 'mention':
|
||||
url = 'notifications/mention'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const notificationOptions: NotificationOptions = {
|
||||
badge: '/pwa-192x192.png',
|
||||
body,
|
||||
data: {
|
||||
access_token,
|
||||
preferred_locale,
|
||||
url: `/${url}`,
|
||||
},
|
||||
dir: 'auto',
|
||||
icon,
|
||||
lang: preferred_locale,
|
||||
tag: notification_id,
|
||||
timestamp: new Date().getUTCDate(),
|
||||
}
|
||||
return self.registration.showNotification(options.title, notificationOptions)
|
||||
})
|
||||
|
||||
event.waitUntil(promise)
|
||||
}
|
||||
|
||||
export const onNotificationClick = (event: NotificationEvent) => {
|
||||
const reactToNotificationClick = new Promise((resolve) => {
|
||||
event.notification.close()
|
||||
resolve(openUrl(event.notification.data.url))
|
||||
})
|
||||
|
||||
event.waitUntil(reactToNotificationClick)
|
||||
}
|
||||
|
||||
function findBestClient(clients: WindowClient[]) {
|
||||
const focusedClient = clients.find(client => client.focused)
|
||||
const visibleClient = clients.find(client => client.visibilityState === 'visible')
|
||||
|
||||
return focusedClient || visibleClient || clients[0]
|
||||
}
|
||||
|
||||
async function openUrl(url: string) {
|
||||
const clients = await self.clients.matchAll({ type: 'window' })
|
||||
// Chrome 42-48 does not support navigate
|
||||
if (clients.length !== 0 && 'navigate' in clients[0]) {
|
||||
const client = findBestClient(clients as WindowClient[])
|
||||
await client.navigate(url).then(client => client?.focus())
|
||||
}
|
||||
|
||||
await self.clients.openWindow(url)
|
||||
}
|
||||
|
||||
function isClientFocused() {
|
||||
return self.clients
|
||||
.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then(windowClients => Promise.resolve(windowClients.some(windowClient => windowClient.focused)))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue