* fix: build inline script using Rollup This reduces code duplication and allows the theme engine to work the same without modifying the code in two places. It does extra extra deps, but I tried to keep them to a minimum. * change code comment * remove unnecessary constant
304 lines
8.7 KiB
JavaScript
304 lines
8.7 KiB
JavaScript
const timestamp = '__timestamp__'
|
|
const ASSETS = `assets_${timestamp}`
|
|
const WEBPACK_ASSETS = `webpack_assets_${timestamp}`
|
|
|
|
// `assets` is an array of everything in the `assets` directory
|
|
const assets = __assets__
|
|
.map(file => file.startsWith('/') ? file : `/${file}`)
|
|
.filter(filename => !filename.startsWith('/apple-icon'))
|
|
.filter(filename => !filename.endsWith('.map'))
|
|
.concat(['/index.html'])
|
|
|
|
// `shell` is an array of all the files generated by webpack
|
|
// also contains '/index.html' for some reason
|
|
const webpackAssets = __shell__
|
|
.filter(filename => !filename.endsWith('.map'))
|
|
.filter(filename => filename !== '/index.html')
|
|
|
|
// `routes` is an array of `{ pattern: RegExp }` objects that
|
|
// match the pages in your app
|
|
const routes = __routes__
|
|
|
|
self.addEventListener('install', event => {
|
|
event.waitUntil((async () => {
|
|
await Promise.all([
|
|
caches.open(WEBPACK_ASSETS).then(cache => cache.addAll(webpackAssets)),
|
|
caches.open(ASSETS).then(cache => cache.addAll(assets))
|
|
])
|
|
self.skipWaiting()
|
|
})())
|
|
})
|
|
|
|
self.addEventListener('activate', event => {
|
|
event.waitUntil((async () => {
|
|
let keys = await caches.keys()
|
|
|
|
// delete old asset/ondemand caches
|
|
for (let key of keys) {
|
|
if (key !== ASSETS &&
|
|
!key.startsWith('webpack_assets_')) {
|
|
await caches.delete(key)
|
|
}
|
|
}
|
|
|
|
// for webpack assets, keep the two latest builds because we may need
|
|
// them when the service worker has installed but the page has not
|
|
// yet reloaded (e.g. when it gives the toast saying "please reload"
|
|
// but then you don't refresh and instead load an async chunk)
|
|
let webpackKeysToDelete = keys
|
|
.filter(key => key.startsWith('webpack_assets_'))
|
|
.sort((a, b) => {
|
|
let aTimestamp = parseInt(a.substring(15), 10)
|
|
let bTimestamp = parseInt(b.substring(15), 10)
|
|
return bTimestamp < aTimestamp ? -1 : 1
|
|
})
|
|
.slice(2)
|
|
|
|
for (let key of webpackKeysToDelete) {
|
|
await caches.delete(key)
|
|
}
|
|
|
|
await self.clients.claim()
|
|
})())
|
|
})
|
|
|
|
self.addEventListener('fetch', event => {
|
|
const req = event.request
|
|
const url = new URL(req.url)
|
|
|
|
// don't try to handle e.g. data: URIs
|
|
if (!url.protocol.startsWith('http')) {
|
|
return
|
|
}
|
|
|
|
event.respondWith((async () => {
|
|
let sameOrigin = url.origin === self.origin
|
|
|
|
if (sameOrigin) {
|
|
// always serve webpack-generated resources and
|
|
// assets from the cache if possible
|
|
let response = await caches.match(req)
|
|
if (response) {
|
|
return response
|
|
}
|
|
// for routes, serve the /index.html file from the most recent
|
|
// assets cache
|
|
if (routes.find(route => route.pattern.test(url.pathname))) {
|
|
let response = await caches.match('/index.html')
|
|
if (response) {
|
|
return response
|
|
}
|
|
}
|
|
}
|
|
|
|
// for everything else, go network-only
|
|
return fetch(req)
|
|
})())
|
|
})
|
|
|
|
self.addEventListener('push', event => {
|
|
event.waitUntil((async () => {
|
|
const data = event.data.json()
|
|
const { origin } = new URL(data.icon)
|
|
|
|
try {
|
|
const notification = await get(`${origin}/api/v1/notifications/${data.notification_id}`, {
|
|
'Authorization': `Bearer ${data.access_token}`
|
|
}, { timeout: 2000 })
|
|
|
|
await showRichNotification(data, notification)
|
|
} catch (e) {
|
|
await showSimpleNotification(data)
|
|
}
|
|
})())
|
|
})
|
|
|
|
async function showSimpleNotification (data) {
|
|
await self.registration.showNotification(data.title, {
|
|
icon: data.icon,
|
|
body: data.body
|
|
})
|
|
}
|
|
|
|
async function showRichNotification (data, notification) {
|
|
const { origin } = new URL(data.icon)
|
|
|
|
switch (notification.type) {
|
|
case 'follow': {
|
|
await self.registration.showNotification(data.title, {
|
|
icon: data.icon,
|
|
body: data.body,
|
|
tag: notification.id,
|
|
data: {
|
|
url: `${self.location.origin}/accounts/${notification.account.id}`
|
|
}
|
|
})
|
|
break
|
|
}
|
|
case 'mention': {
|
|
const actions = [{
|
|
action: 'favourite',
|
|
title: 'Favourite'
|
|
}]
|
|
|
|
if ('reply' in NotificationEvent.prototype) {
|
|
actions.splice(0, 0, {
|
|
action: 'reply',
|
|
type: 'text',
|
|
title: 'Reply'
|
|
})
|
|
}
|
|
|
|
if (['public', 'unlisted'].includes(notification.status.visibility)) {
|
|
actions.push({
|
|
action: 'reblog',
|
|
title: 'Boost'
|
|
})
|
|
}
|
|
|
|
await self.registration.showNotification(data.title, {
|
|
icon: data.icon,
|
|
body: data.body,
|
|
tag: notification.id,
|
|
data: {
|
|
instance: origin,
|
|
status_id: notification.status.id,
|
|
access_token: data.access_token,
|
|
url: `${self.location.origin}/statuses/${notification.status.id}`
|
|
},
|
|
actions
|
|
})
|
|
break
|
|
}
|
|
case 'reblog': {
|
|
await self.registration.showNotification(data.title, {
|
|
icon: data.icon,
|
|
body: data.body,
|
|
tag: notification.id,
|
|
data: {
|
|
url: `${self.location.origin}/statuses/${notification.status.id}`
|
|
}
|
|
})
|
|
break
|
|
}
|
|
case 'favourite': {
|
|
await self.registration.showNotification(data.title, {
|
|
icon: data.icon,
|
|
body: data.body,
|
|
tag: notification.id,
|
|
data: {
|
|
url: `${self.location.origin}/statuses/${notification.status.id}`
|
|
}
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const cloneNotification = notification => {
|
|
const clone = { }
|
|
|
|
// Object.assign() does not work with notifications
|
|
for (let k in notification) {
|
|
clone[k] = notification[k]
|
|
}
|
|
|
|
return clone
|
|
}
|
|
|
|
const updateNotificationWithoutAction = (notification, action) => {
|
|
const newNotification = cloneNotification(notification)
|
|
|
|
newNotification.actions = newNotification.actions.filter(item => item.action !== action)
|
|
|
|
return self.registration.showNotification(newNotification.title, newNotification)
|
|
}
|
|
|
|
self.addEventListener('notificationclick', event => {
|
|
event.waitUntil((async () => {
|
|
switch (event.action) {
|
|
case 'reply': {
|
|
await post(`${event.notification.data.instance}/api/v1/statuses/`, {
|
|
status: event.reply,
|
|
in_reply_to_id: event.notification.data.status_id
|
|
}, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
|
|
await updateNotificationWithoutAction(event.notification, 'reply')
|
|
break
|
|
}
|
|
case 'reblog': {
|
|
await post(`${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/reblog`, null, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
|
|
await updateNotificationWithoutAction(event.notification, 'reblog')
|
|
break
|
|
}
|
|
case 'favourite': {
|
|
await post(`${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/favourite`, null, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
|
|
await updateNotificationWithoutAction(event.notification, 'favourite')
|
|
break
|
|
}
|
|
default: {
|
|
await self.clients.openWindow(event.notification.data.url)
|
|
await event.notification.close()
|
|
break
|
|
}
|
|
}
|
|
})())
|
|
})
|
|
|
|
// Copy-paste from ajax.js
|
|
async function get (url, headers, options) {
|
|
return _fetch(url, makeFetchOptions('GET', headers), options)
|
|
}
|
|
|
|
async function post (url, body, headers, options) {
|
|
return _putOrPostOrPatch('POST', url, body, headers, options)
|
|
}
|
|
|
|
async function _putOrPostOrPatch (method, url, body, headers, options) {
|
|
let fetchOptions = makeFetchOptions(method, headers)
|
|
if (body) {
|
|
if (body instanceof FormData) {
|
|
fetchOptions.body = body
|
|
} else {
|
|
fetchOptions.body = JSON.stringify(body)
|
|
fetchOptions.headers['Content-Type'] = 'application/json'
|
|
}
|
|
}
|
|
return _fetch(url, fetchOptions, options)
|
|
}
|
|
|
|
async function _fetch (url, fetchOptions, options) {
|
|
let response
|
|
if (options && options.timeout) {
|
|
response = await fetchWithTimeout(url, fetchOptions, options.timeout)
|
|
} else {
|
|
response = await fetch(url, fetchOptions)
|
|
}
|
|
return throwErrorIfInvalidResponse(response)
|
|
}
|
|
|
|
async function throwErrorIfInvalidResponse (response) {
|
|
let json = await response.json()
|
|
if (response.status >= 200 && response.status < 300) {
|
|
return json
|
|
}
|
|
if (json && json.error) {
|
|
throw new Error(response.status + ': ' + json.error)
|
|
}
|
|
throw new Error('Request failed: ' + response.status)
|
|
}
|
|
|
|
function fetchWithTimeout (url, fetchOptions, timeout) {
|
|
return new Promise((resolve, reject) => {
|
|
fetch(url, fetchOptions).then(resolve, reject)
|
|
setTimeout(() => reject(new Error(`Timed out after ${timeout / 1000} seconds`)), timeout)
|
|
})
|
|
}
|
|
|
|
function makeFetchOptions (method, headers) {
|
|
return {
|
|
method,
|
|
headers: Object.assign(headers || {}, {
|
|
'Accept': 'application/json'
|
|
})
|
|
}
|
|
}
|