253 lines
8.3 KiB
JavaScript
253 lines
8.3 KiB
JavaScript
/* eslint-env serviceworker */
|
|
|
|
import 'virtual:pleroma-fe/service_worker_env'
|
|
import { storage } from 'src/lib/storage.js'
|
|
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
|
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
|
import { shouldCache, cacheKey, emojiCacheKey } from './services/sw/sw.js'
|
|
import { createI18n } from 'vue-i18n'
|
|
// Collects all messages for service workers
|
|
// Needed because service workers cannot use dynamic imports
|
|
// See /build/sw_plugin.js for more information
|
|
import messages from 'virtual:pleroma-fe/service_worker_messages'
|
|
|
|
const i18n = createI18n({
|
|
// By default, use the browser locale, we will update it if neccessary
|
|
locale: 'en',
|
|
fallbackLocale: 'en',
|
|
messages
|
|
})
|
|
|
|
const state = {
|
|
lastFocused: null,
|
|
notificationIds: new Set(),
|
|
allowedNotificationTypes: null
|
|
}
|
|
|
|
function getWindowClients () {
|
|
return clients.matchAll({ includeUncontrolled: true })
|
|
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
|
|
}
|
|
|
|
const setSettings = async () => {
|
|
const vuexState = await storage.getItem('vuex-lz')
|
|
const locale = vuexState.config.interfaceLanguage || 'en'
|
|
i18n.locale = locale
|
|
const notificationsNativeArray = Object.entries(vuexState.config.notificationNative)
|
|
state.webPushAlwaysShowNotifications = vuexState.config.webPushAlwaysShowNotifications
|
|
|
|
state.allowedNotificationTypes = new Set(
|
|
notificationsNativeArray
|
|
.filter(([, v]) => v)
|
|
.map(([k]) => {
|
|
switch (k) {
|
|
case 'mentions':
|
|
return 'mention'
|
|
case 'statuses':
|
|
return 'status'
|
|
case 'likes':
|
|
return 'like'
|
|
case 'repeats':
|
|
return 'repeat'
|
|
case 'emojiReactions':
|
|
return 'pleroma:emoji_reaction'
|
|
case 'reports':
|
|
return 'pleroma:report'
|
|
case 'followRequest':
|
|
return 'follow_request'
|
|
case 'follows':
|
|
return 'follow'
|
|
case 'polls':
|
|
return 'poll'
|
|
default:
|
|
return k
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
const showPushNotification = async (event) => {
|
|
const activeClients = await getWindowClients()
|
|
await setSettings()
|
|
// Only show push notifications if all tabs/windows are closed
|
|
if (state.webPushAlwaysShowNotifications || activeClients.length === 0) {
|
|
const data = event.data.json()
|
|
|
|
const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}`
|
|
const notification = await fetch(url, { headers: { Authorization: 'Bearer ' + data.access_token } })
|
|
const notificationJson = await notification.json()
|
|
const parsedNotification = parseNotification(notificationJson)
|
|
|
|
const res = prepareNotificationObject(parsedNotification, i18n)
|
|
|
|
if (state.webPushAlwaysShowNotifications || state.allowedNotificationTypes.has(parsedNotification.type)) {
|
|
return self.registration.showNotification(res.title, res)
|
|
}
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
|
|
const cacheFiles = self.serviceWorkerOption.assets
|
|
const isEmoji = req => {
|
|
if (req.method !== 'GET') {
|
|
return false
|
|
}
|
|
const url = new URL(req.url)
|
|
|
|
return url.pathname.startsWith('/emoji/')
|
|
}
|
|
const isNotMedia = req => {
|
|
if (req.method !== 'GET') {
|
|
return false
|
|
}
|
|
const url = new URL(req.url)
|
|
return !url.pathname.startsWith('/media/')
|
|
}
|
|
const isAsset = req => {
|
|
const url = new URL(req.url)
|
|
return cacheFiles.includes(url.pathname)
|
|
}
|
|
|
|
const isSuccessful = (resp) => {
|
|
if (!resp.ok) {
|
|
return false
|
|
}
|
|
if ((new URL(resp.url)).pathname === '/index.html') {
|
|
// For index.html itself, there is no fallback possible.
|
|
return true
|
|
}
|
|
const type = resp.headers.get('Content-Type')
|
|
// Backend will revert to index.html if the file does not exist, so text/html for emojis and assets is a failure
|
|
return type && !type.includes('text/html')
|
|
}
|
|
|
|
self.addEventListener('install', async (event) => {
|
|
if (shouldCache) {
|
|
event.waitUntil((async () => {
|
|
// Do not preload i18n and emoji annotations to speed up loading
|
|
const shouldPreload = (route) => {
|
|
return !route.startsWith('/static/js/i18n/') && !route.startsWith('/static/js/emoji-annotations/')
|
|
}
|
|
const cache = await caches.open(cacheKey)
|
|
await Promise.allSettled(cacheFiles.filter(shouldPreload).map(async (route) => {
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Cache/add
|
|
// originally we used addAll() but it will raise a problem in one edge case:
|
|
// when the file for the route is not found, backend will return index.html with code 200
|
|
// but it's wrong, and it's cached, so we end up with a bad cache.
|
|
// this can happen when you refresh when you are in the process of upgrading
|
|
// the frontend.
|
|
const resp = await fetch(route)
|
|
if (isSuccessful(resp)) {
|
|
await cache.put(route, resp)
|
|
}
|
|
}))
|
|
})())
|
|
}
|
|
})
|
|
|
|
self.addEventListener('activate', async (event) => {
|
|
if (shouldCache) {
|
|
event.waitUntil((async () => {
|
|
const cache = await caches.open(cacheKey)
|
|
const keys = await cache.keys()
|
|
await Promise.all(
|
|
keys.filter(request => {
|
|
const url = new URL(request.url)
|
|
const shouldKeep = cacheFiles.includes(url.pathname)
|
|
return !shouldKeep
|
|
}).map(k => cache.delete(k))
|
|
)
|
|
})())
|
|
}
|
|
})
|
|
|
|
self.addEventListener('push', async (event) => {
|
|
if (event.data) {
|
|
// Supposedly, we HAVE to return a promise inside waitUntil otherwise it will
|
|
// show (extra) notification that website is updated in background
|
|
event.waitUntil(showPushNotification(event))
|
|
}
|
|
})
|
|
|
|
self.addEventListener('message', async (event) => {
|
|
await setSettings()
|
|
const { type, content } = event.data
|
|
|
|
if (type === 'desktopNotification') {
|
|
const { title, ...rest } = content
|
|
const { tag, type } = rest
|
|
if (state.notificationIds.has(tag)) return
|
|
state.notificationIds.add(tag)
|
|
setTimeout(() => state.notificationIds.delete(tag), 10000)
|
|
if (state.allowedNotificationTypes.has(type)) {
|
|
self.registration.showNotification(title, rest)
|
|
}
|
|
}
|
|
|
|
if (type === 'desktopNotificationClose') {
|
|
const { id, all } = content
|
|
const search = all ? null : { tag: id }
|
|
const notifications = await self.registration.getNotifications(search)
|
|
notifications.forEach(n => n.close())
|
|
}
|
|
|
|
if (type === 'updateFocus') {
|
|
state.lastFocused = event.source.id
|
|
|
|
const notifications = await self.registration.getNotifications()
|
|
notifications.forEach(n => n.close())
|
|
}
|
|
})
|
|
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close()
|
|
|
|
event.waitUntil(getWindowClients().then((list) => {
|
|
for (let i = 0; i < list.length; i++) {
|
|
const client = list[i]
|
|
client.postMessage({ type: 'notificationClicked', id: event.notification.tag })
|
|
}
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
const client = list[i]
|
|
if (state.lastFocused === null || client.id === state.lastFocused) {
|
|
if ('focus' in client) return client.focus()
|
|
}
|
|
}
|
|
|
|
if (clients.openWindow) return clients.openWindow('/')
|
|
}))
|
|
})
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
// Do not mess up with remote things
|
|
const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin
|
|
if (shouldCache && event.request.method === 'GET' && isSameOrigin && isNotMedia(event.request)) {
|
|
// this is a bit spammy
|
|
// console.debug('[Service worker] fetch:', event.request.url)
|
|
event.respondWith((async () => {
|
|
const r = await caches.match(event.request)
|
|
const isEmojiReq = isEmoji(event.request)
|
|
|
|
if (r && isSuccessful(r)) {
|
|
console.debug('[Service worker] already cached:', event.request.url)
|
|
return r
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(event.request)
|
|
if (response.ok &&
|
|
isSuccessful(response) &&
|
|
(isEmojiReq || isAsset(event.request))) {
|
|
console.debug(`[Service worker] caching ${isEmojiReq ? 'emoji' : 'asset'}:`, event.request.url)
|
|
const cache = await caches.open(isEmojiReq ? emojiCacheKey : cacheKey)
|
|
await cache.put(event.request.clone(), response.clone())
|
|
}
|
|
return response
|
|
} catch (e) {
|
|
console.error('[Service worker] error when caching emoji:', e)
|
|
throw e
|
|
}
|
|
})())
|
|
}
|
|
})
|