From f0c91a39746ad443fff03afbba720ceb413b9e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20S=C3=A1nchez?= Date: Sun, 18 Dec 2022 00:29:16 +0100 Subject: [PATCH] feat: pwa with push notifications (#337) --- .env.mock | 2 +- .eslintignore | 2 + .gitignore | 1 + app.vue | 1 + components/PWAPrompt.client.vue | 39 + components/common/CommonCheckbox.vue | 35 + components/common/CommonRadio.vue | 37 + ...ificationEnablePushNotification.client.vue | 42 + .../NotificationPreferences.client.vue | 185 +++ .../createPushSubscription.ts | 119 ++ composables/push-notifications/types.ts | 22 + .../push-notifications/usePushManager.ts | 171 ++ composables/pwa/index.ts | 42 + composables/setups.ts | 25 + composables/users.ts | 49 +- config/pwa.ts | 51 + constants/index.ts | 2 + error.vue | 1 + https-dev-config/local-https-server.mjs | 12 + https-dev-config/localhost.crt | 25 + https-dev-config/localhost.key | 28 + locales/en-US.json | 38 + locales/es-ES.json | 38 + modules/pwa/config.ts | 69 + modules/pwa/index.ts | 83 + modules/pwa/types.ts | 12 + netlify.toml | 5 - nuxt.config.ts | 19 + package.json | 12 +- pages/notifications.vue | 25 +- plugins/masto.ts | 6 +- pnpm-lock.yaml | 1452 +++++++++++++++++ public-dev/apple-touch-icon.png | Bin 0 -> 6552 bytes public-dev/pwa-192x192.png | Bin 0 -> 6947 bytes public-dev/pwa-512x512.png | Bin 0 -> 19258 bytes public/apple-touch-icon.png | Bin 0 -> 8467 bytes public/favicon.svg | 11 + public/pwa-192x192.png | Bin 0 -> 7365 bytes public/pwa-512x512.png | Bin 0 -> 20140 bytes public/robots.txt | 2 + public/safari-pinned-tab.svg | 79 + server/api/[server]/oauth.ts | 2 +- service-worker/sw.ts | 65 + service-worker/tsconfig.json | 8 + service-worker/types.ts | 9 + service-worker/web-push-notifications.ts | 85 + shims.d.ts | 2 + types/index.ts | 4 +- 48 files changed, 2903 insertions(+), 14 deletions(-) create mode 100644 components/PWAPrompt.client.vue create mode 100644 components/common/CommonCheckbox.vue create mode 100644 components/common/CommonRadio.vue create mode 100644 components/notification/NotificationEnablePushNotification.client.vue create mode 100644 components/notification/NotificationPreferences.client.vue create mode 100644 composables/push-notifications/createPushSubscription.ts create mode 100644 composables/push-notifications/types.ts create mode 100644 composables/push-notifications/usePushManager.ts create mode 100644 composables/pwa/index.ts create mode 100644 config/pwa.ts create mode 100644 https-dev-config/local-https-server.mjs create mode 100644 https-dev-config/localhost.crt create mode 100644 https-dev-config/localhost.key create mode 100644 modules/pwa/config.ts create mode 100644 modules/pwa/index.ts create mode 100644 modules/pwa/types.ts create mode 100644 public-dev/apple-touch-icon.png create mode 100644 public-dev/pwa-192x192.png create mode 100644 public-dev/pwa-512x512.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon.svg create mode 100644 public/pwa-192x192.png create mode 100644 public/pwa-512x512.png create mode 100644 public/robots.txt create mode 100644 public/safari-pinned-tab.svg create mode 100644 service-worker/sw.ts create mode 100644 service-worker/tsconfig.json create mode 100644 service-worker/types.ts create mode 100644 service-worker/web-push-notifications.ts diff --git a/.env.mock b/.env.mock index df788721..e45acc79 100644 --- a/.env.mock +++ b/.env.mock @@ -1 +1 @@ -MOCK_USER='{"user":{"server":"universeodon.com","token":"BLMfvYGgiEPgLpiunVS0JYxxqzga3S58C60DDwu1jvw","account":{"id":"109424142224653388","username":"elkdev","acct":"elkdev@universeodon.com","displayName":"Elk Dev Team","locked":false,"bot":false,"discoverable":null,"group":false,"createdAt":"2022-11-28T00:00:00.000Z","note":"","url":"https://universeodon.com/@elkdev","avatar":"https://universeodon.com/avatars/original/missing.png","avatarStatic":"https://universeodon.com/avatars/original/missing.png","header":"https://universeodon.com/headers/original/missing.png","headerStatic":"https://universeodon.com/headers/original/missing.png","followersCount":0,"followingCount":0,"statusesCount":0,"lastStatusAt":null,"noindex":false,"source":{"privacy":"public","sensitive":false,"language":null,"note":"","fields":[],"followRequestsCount":0},"emojis":[],"fields":[],"role":{"id":"-99","name":"","permissions":"65536","color":"","highlighted":false}}},"server":{"109424142224653388":{"uri":"universeodon.com","title":"Universeodon","shortDescription":"Be one with the fediverse.","description":"","email":"novae@universeodon.com","version":"4.0.2","urls":{"streamingApi":"wss://universeodon.com"},"stats":{"userCount":57026,"statusCount":283364,"domainCount":11515},"thumbnail":"https://media.universeodon.com/site_uploads/files/000/000/003/@1x/9de6fc1bbd150b05.png","languages":["en"],"registrations":true,"approvalRequired":false,"invitesEnabled":true,"configuration":{"accounts":{"maxFeaturedTags":10},"statuses":{"maxCharacters":500,"maxMediaAttachments":4,"charactersReservedPerUrl":23},"mediaAttachments":{"supportedMimeTypes":["image/jpeg","image/png","image/gif","image/heic","image/heif","image/webp","image/avif","video/webm","video/mp4","video/quicktime","video/ogg","audio/wave","audio/wav","audio/x-wav","audio/x-pn-wave","audio/vnd.wave","audio/ogg","audio/vorbis","audio/mpeg","audio/mp3","audio/webm","audio/flac","audio/aac","audio/m4a","audio/x-m4a","audio/mp4","audio/3gpp","video/x-ms-asf"],"imageSizeLimit":10485760,"imageMatrixLimit":16777216,"videoSizeLimit":41943040,"videoFrameRateLimit":60,"videoMatrixLimit":2304000},"polls":{"maxOptions":4,"maxCharactersPerOption":50,"minExpiration":300,"maxExpiration":2629746}},"contactAccount":{"id":"109287809647205395","username":"supernovae","acct":"supernovae","displayName":"Supernovae","locked":false,"bot":false,"discoverable":true,"group":false,"createdAt":"2022-11-04T00:00:00.000Z","note":"","url":"https://universeodon.com/@supernovae","avatar":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","avatarStatic":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","header":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","headerStatic":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","followersCount":6387,"followingCount":305,"statusesCount":1753,"lastStatusAt":"2022-11-28","noindex":false,"emojis":[],"fields":[]},"rules":[]}}}' +MOCK_USER='{"user":{"server":"universeodon.com","token":"yZcpj0FmnsEkUvBiXSCb_KQnccl2IU0kx9TfDbcxPJY","vapidKey":"BJwtUVlyCabpMnLI6HOyu-qMfJswxEq_c8pgRymxjTN_vCzMWfGrRHrwNczj9LIokAHtxh6Ziw1Kq7_ERDoriz0=","account":{"id":"109424142224653388","username":"elkdev","acct":"elkdev@universeodon.com","displayName":"Elk Dev Team","locked":false,"bot":false,"discoverable":null,"group":false,"createdAt":"2022-11-28T00:00:00.000Z","note":"","url":"https://universeodon.com/@elkdev","avatar":"https://universeodon.com/avatars/original/missing.png","avatarStatic":"https://universeodon.com/avatars/original/missing.png","header":"https://universeodon.com/headers/original/missing.png","headerStatic":"https://universeodon.com/headers/original/missing.png","followersCount":3,"followingCount":4,"statusesCount":20,"lastStatusAt":"2022-12-13","noindex":false,"source":{"privacy":"public","sensitive":false,"language":null,"note":"","fields":[],"followRequestsCount":0},"emojis":[],"fields":[],"role":{"id":"-99","name":"","permissions":"65536","color":"","highlighted":false}}},"server":{"109424142224653388":{"uri":"universeodon.com","title":"Universeodon","shortDescription":"Be one with the fediverse.","description":"","email":"novae@universeodon.com","version":"4.0.2","urls":{"streamingApi":"wss://universeodon.com"},"stats":{"userCount":57026,"statusCount":283364,"domainCount":11515},"thumbnail":"https://media.universeodon.com/site_uploads/files/000/000/003/@1x/9de6fc1bbd150b05.png","languages":["en"],"registrations":true,"approvalRequired":false,"invitesEnabled":true,"configuration":{"accounts":{"maxFeaturedTags":10},"statuses":{"maxCharacters":500,"maxMediaAttachments":4,"charactersReservedPerUrl":23},"mediaAttachments":{"supportedMimeTypes":["image/jpeg","image/png","image/gif","image/heic","image/heif","image/webp","image/avif","video/webm","video/mp4","video/quicktime","video/ogg","audio/wave","audio/wav","audio/x-wav","audio/x-pn-wave","audio/vnd.wave","audio/ogg","audio/vorbis","audio/mpeg","audio/mp3","audio/webm","audio/flac","audio/aac","audio/m4a","audio/x-m4a","audio/mp4","audio/3gpp","video/x-ms-asf"],"imageSizeLimit":10485760,"imageMatrixLimit":16777216,"videoSizeLimit":41943040,"videoFrameRateLimit":60,"videoMatrixLimit":2304000},"polls":{"maxOptions":4,"maxCharactersPerOption":50,"minExpiration":300,"maxExpiration":2629746}},"contactAccount":{"id":"109287809647205395","username":"supernovae","acct":"supernovae","displayName":"Supernovae","locked":false,"bot":false,"discoverable":true,"group":false,"createdAt":"2022-11-04T00:00:00.000Z","note":"","url":"https://universeodon.com/@supernovae","avatar":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","avatarStatic":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","header":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","headerStatic":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","followersCount":6387,"followingCount":305,"statusesCount":1753,"lastStatusAt":"2022-11-28","noindex":false,"emojis":[],"fields":[]},"rules":[]}}}' diff --git a/.eslintignore b/.eslintignore index d2315970..2c0ecd21 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,5 @@ *.png *.ico *.toml +https-dev-config/localhost.crt +https-dev-config/localhost.key diff --git a/.gitignore b/.gitignore index 4481e661..a48a3cd7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist .DS_Store .idea/ .vite-inspect +.netlify/ public/shiki diff --git a/app.vue b/app.vue index 1dd5617d..b7379cb6 100644 --- a/app.vue +++ b/app.vue @@ -13,4 +13,5 @@ const key = computed(() => `${currentServer.value}:${currentUser.value?.account. + diff --git a/components/PWAPrompt.client.vue b/components/PWAPrompt.client.vue new file mode 100644 index 00000000..056cd2e2 --- /dev/null +++ b/components/PWAPrompt.client.vue @@ -0,0 +1,39 @@ + + + + diff --git a/components/common/CommonCheckbox.vue b/components/common/CommonCheckbox.vue new file mode 100644 index 00000000..a01c3334 --- /dev/null +++ b/components/common/CommonCheckbox.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/components/common/CommonRadio.vue b/components/common/CommonRadio.vue new file mode 100644 index 00000000..2c1e92ef --- /dev/null +++ b/components/common/CommonRadio.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/components/notification/NotificationEnablePushNotification.client.vue b/components/notification/NotificationEnablePushNotification.client.vue new file mode 100644 index 00000000..783970de --- /dev/null +++ b/components/notification/NotificationEnablePushNotification.client.vue @@ -0,0 +1,42 @@ + + + diff --git a/components/notification/NotificationPreferences.client.vue b/components/notification/NotificationPreferences.client.vue new file mode 100644 index 00000000..8e14aefe --- /dev/null +++ b/components/notification/NotificationPreferences.client.vue @@ -0,0 +1,185 @@ + + + diff --git a/composables/push-notifications/createPushSubscription.ts b/composables/push-notifications/createPushSubscription.ts new file mode 100644 index 00000000..61fe0823 --- /dev/null +++ b/composables/push-notifications/createPushSubscription.ts @@ -0,0 +1,119 @@ +import type { + CreatePushSubscriptionParams, + PushSubscription as MastoPushSubscription, +} from 'masto' +import type { + CreatePushNotification, + PushManagerSubscriptionInfo, + RequiredUserLogin, +} from '~/composables/push-notifications/types' +import { useMasto } from '~/composables/masto' +import { currentUser, removePushNotifications } from '~/composables/users' + +export const createPushSubscription = async ( + user: RequiredUserLogin, + notificationData: CreatePushNotification, +): Promise => { + const { server: serverEndpoint, vapidKey } = user + + return await getRegistration() + .then(getPushSubscription) + .then(({ registration, subscription }): Promise => { + if (subscription) { + const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey!)).toString() + const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString() + + // If the VAPID public key did not change and the endpoint corresponds + // to the endpoint saved in the backend, the subscription is valid + // If push subscription is not there, we need to create it: it is fetched on login + if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint && user.pushSubscription) { + return Promise.resolve(user.pushSubscription) + } + else if (user.pushSubscription) { + // if we have a subscription, but it is not valid, we need to remove it + return unsubscribeFromBackend(false) + .then(() => subscribe(registration, vapidKey)) + .then(subscription => sendSubscriptionToBackend(subscription, notificationData)) + } + } + + return subscribe(registration, vapidKey).then( + subscription => sendSubscriptionToBackend(subscription, notificationData), + ) + }) + .catch((error) => { + if (error.code === 20 && error.name === 'AbortError') + console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.') + else if (error.code === 5 && error.name === 'InvalidCharacterError') + console.error('The VAPID public key seems to be invalid:', vapidKey) + + return getRegistration() + .then(getPushSubscription) + .then(() => unsubscribeFromBackend(true)) + .then(() => Promise.resolve(undefined)) + .catch((e) => { + console.error(e) + return Promise.resolve(undefined) + }) + }) +} + +// Taken from https://www.npmjs.com/package/web-push +function urlBase64ToUint8Array(base64String: string) { + const padding = '='.repeat((4 - base64String.length % 4) % 4) + const base64 = `${base64String}${padding}` + .replace(/-/g, '+') + .replace(/_/g, '/') + + const rawData = window.atob(base64) + const outputArray = new Uint8Array(rawData.length) + + for (let i = 0; i < rawData.length; ++i) + outputArray[i] = rawData.charCodeAt(i) + + return outputArray +} + +function getRegistration() { + return navigator.serviceWorker.ready +} +async function getPushSubscription(registration: ServiceWorkerRegistration): Promise { + const subscription = await registration.pushManager.getSubscription() + return { registration, subscription } +} + +async function subscribe( + registration: ServiceWorkerRegistration, + applicationServerKey: string, +): Promise { + return await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(applicationServerKey), + }) +} + +async function unsubscribeFromBackend(fromSWPushManager: boolean) { + const cu = currentUser.value + if (cu) + await removePushNotifications(cu, fromSWPushManager) +} + +async function sendSubscriptionToBackend( + subscription: PushSubscription, + data: CreatePushNotification, +): Promise { + const { endpoint, keys } = subscription.toJSON() + const params: CreatePushSubscriptionParams = { + subscription: { + endpoint: endpoint!, + keys: { + p256dh: keys!.p256dh!, + auth: keys!.auth!, + }, + }, + data, + } + + return await useMasto().pushSubscriptions.create(params) +} + diff --git a/composables/push-notifications/types.ts b/composables/push-notifications/types.ts new file mode 100644 index 00000000..d3b617c2 --- /dev/null +++ b/composables/push-notifications/types.ts @@ -0,0 +1,22 @@ +import type { PushSubscription as MastoPushSubscription, PushSubscriptionAlerts, SubscriptionPolicy } from 'masto' + +import type { UserLogin } from '~/types' + +export type SubscriptionResult = 'subscribed' | 'notification-denied' | 'invalid-state' +export interface PushManagerSubscriptionInfo { + registration: ServiceWorkerRegistration + subscription: PushSubscription | null +} + +export interface RequiredUserLogin extends Required> { + pushSubscription?: MastoPushSubscription +} + +export interface CreatePushNotification { + alerts?: Partial | null + policy?: SubscriptionPolicy +} + +export type PushNotificationRequest = Record +export type PushNotificationPolicy = Record + diff --git a/composables/push-notifications/usePushManager.ts b/composables/push-notifications/usePushManager.ts new file mode 100644 index 00000000..057a31c3 --- /dev/null +++ b/composables/push-notifications/usePushManager.ts @@ -0,0 +1,171 @@ +import type { + CreatePushNotification, + PushNotificationPolicy, + PushNotificationRequest, + SubscriptionResult, +} from '~/composables/push-notifications/types' +import { createPushSubscription } from '~/composables/push-notifications/createPushSubscription' +import { STORAGE_KEY_NOTIFICATION, STORAGE_KEY_NOTIFICATION_POLICY } from '~/constants' +import { currentUser, removePushNotifications } from '~/composables/users' + +const supportsPushNotifications = typeof window !== 'undefined' + && 'serviceWorker' in navigator + && 'PushManager' in window + && 'getKey' in PushSubscription.prototype + +export const usePushManager = () => { + const isSubscribed = ref(false) + const notificationPermission = ref( + Notification.permission === 'denied' + ? 'denied' + : Notification.permission === 'granted' + ? 'granted' + : Notification.permission === 'default' + ? 'prompt' + : undefined, + ) + const isSupported = $computed(() => supportsPushNotifications) + const hiddenNotification = useLocalStorage(STORAGE_KEY_NOTIFICATION, {}) + const configuredPolicy = useLocalStorage(STORAGE_KEY_NOTIFICATION_POLICY, {}) + const pushNotificationData = ref({ + follow: currentUser.value?.pushSubscription?.alerts.follow ?? true, + favourite: currentUser.value?.pushSubscription?.alerts.favourite ?? true, + reblog: currentUser.value?.pushSubscription?.alerts.reblog ?? true, + mention: currentUser.value?.pushSubscription?.alerts.mention ?? true, + poll: currentUser.value?.pushSubscription?.alerts.poll ?? true, + policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all', + }) + const { history, commit, clear } = useManualRefHistory(pushNotificationData, { clone: true }) + const saveEnabled = computed(() => { + const current = pushNotificationData.value + const previous = history.value?.[0]?.snapshot + return current.favourite !== previous.favourite + || current.reblog !== previous.reblog + || current.mention !== previous.mention + || current.follow !== previous.follow + || current.poll !== previous.poll + || current.policy !== previous.policy + }) + + watch(() => currentUser.value?.pushSubscription, (subscription) => { + isSubscribed.value = !!subscription + pushNotificationData.value = { + follow: subscription?.alerts.follow ?? false, + favourite: subscription?.alerts.favourite ?? false, + reblog: subscription?.alerts.reblog ?? false, + mention: subscription?.alerts.mention ?? false, + poll: subscription?.alerts.poll ?? false, + policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all', + } + }, { immediate: true, flush: 'post' }) + + const subscribe = async (notificationData?: CreatePushNotification): Promise => { + if (!isSupported || !currentUser.value) + return 'invalid-state' + + const { pushSubscription, server, token, vapidKey, account: { acct } } = currentUser.value + + if (!token || !server || !vapidKey) + return 'invalid-state' + + let permission: PermissionState | undefined + + if (!notificationPermission.value || (notificationPermission.value === 'prompt' && !hiddenNotification.value[acct])) { + // safari 16 does not support navigator.permissions.query for notifications + try { + permission = (await navigator.permissions?.query({ name: 'notifications' }))?.state + } + catch { + permission = await Promise.resolve(Notification.requestPermission()).then((p: NotificationPermission) => { + return p === 'default' ? 'prompt' : p + }) + } + } + else { + permission = notificationPermission.value + } + + if (!permission || permission === 'denied') { + notificationPermission.value = permission + return 'notification-denied' + } + + currentUser.value.pushSubscription = await createPushSubscription({ + pushSubscription, server, token, vapidKey, + }, notificationData ?? { + alerts: { + follow: true, + favourite: true, + reblog: true, + mention: true, + poll: true, + }, + policy: 'all', + }) + await nextTick() + notificationPermission.value = permission + hiddenNotification.value[acct] = true + + return 'subscribed' + } + + const unsubscribe = async () => { + if (!isSupported || !isSubscribed || !currentUser.value) + return false + + await removePushNotifications(currentUser.value) + } + + const saveSettings = async () => { + commit() + configuredPolicy.value[currentUser.value!.account.acct ?? ''] = pushNotificationData.value.policy + await nextTick() + clear() + await nextTick() + } + + const undoChanges = () => { + const current = pushNotificationData.value + const previous = history.value[0].snapshot + current.favourite = previous.favourite + current.reblog = previous.reblog + current.mention = previous.mention + current.follow = previous.follow + current.poll = previous.poll + current.policy = previous.policy + configuredPolicy.value[currentUser.value!.account.acct ?? ''] = previous.policy + commit() + clear() + } + + const updateSubscription = async () => { + if (currentUser.value) { + currentUser.value.pushSubscription = await useMasto().pushSubscriptions.update({ + data: { + alerts: { + follow: pushNotificationData.value.follow, + favourite: pushNotificationData.value.favourite, + reblog: pushNotificationData.value.reblog, + mention: pushNotificationData.value.mention, + poll: pushNotificationData.value.poll, + }, + policy: pushNotificationData.value.policy, + }, + }) + await saveSettings() + } + } + + return { + pushNotificationData, + saveEnabled, + undoChanges, + hiddenNotification, + isSupported, + isSubscribed, + notificationPermission, + updateSubscription, + subscribe, + unsubscribe, + } +} diff --git a/composables/pwa/index.ts b/composables/pwa/index.ts new file mode 100644 index 00000000..09878d11 --- /dev/null +++ b/composables/pwa/index.ts @@ -0,0 +1,42 @@ +import { useRegisterSW } from 'virtual:pwa-register/vue' + +export const usePWA = () => { + const online = useOnline() + + const { + needRefresh, + updateServiceWorker, + } = useRegisterSW({ + immediate: true, + onRegisteredSW(swUrl, r) { + if (!r || r.installing) + return + + setInterval(async () => { + if (!online.value) + return + + const resp = await fetch(swUrl, { + cache: 'no-store', + headers: { + 'cache': 'no-store', + 'cache-control': 'no-cache', + }, + }) + + if (resp?.status === 200) + await r.update() + }, 60 * 60 * 1000 /* 1 hour */) + }, + }) + + const close = async () => { + needRefresh.value = false + } + + return { + needRefresh, + updateServiceWorker, + close, + } +} diff --git a/composables/setups.ts b/composables/setups.ts index 9f976243..4c2a9304 100644 --- a/composables/setups.ts +++ b/composables/setups.ts @@ -1,3 +1,5 @@ +import { pwaInfo } from 'virtual:pwa-info' +import type { Link } from '@unhead/schema' import { APP_NAME, STORAGE_KEY_LANG } from '~/constants' export function setupPageHeader() { @@ -6,11 +8,34 @@ export function setupPageHeader() { const i18n = useI18n() + const link: Link[] = [] + + if (pwaInfo && pwaInfo.webManifest) { + const { webManifest } = pwaInfo + if (webManifest) { + const { href, useCredentials } = webManifest + if (useCredentials) { + link.push({ + rel: 'manifest', + href, + crossorigin: 'use-credentials', + }) + } + else { + link.push({ + rel: 'manifest', + href, + }) + } + } + } + useHeadFixed({ htmlAttrs: { lang: () => i18n.locale.value, }, titleTemplate: title => `${title ? `${title} | ` : ''}${APP_NAME}${isDev ? ' (dev)' : isPreview ? ' (preview)' : ''}`, + link, }) // eslint-disable-next-line no-unused-expressions diff --git a/composables/users.ts b/composables/users.ts index f4dc1f30..b59a93be 100644 --- a/composables/users.ts +++ b/composables/users.ts @@ -2,7 +2,16 @@ import { login as loginMasto } from 'masto' import type { Account, AccountCredentials, Instance, WsEvents } from 'masto' import type { Ref } from 'vue' import type { UserLogin } from '~/types' -import { DEFAULT_POST_CHARS_LIMIT, DEFAULT_SERVER, STORAGE_KEY_CURRENT_USER, STORAGE_KEY_SERVERS, STORAGE_KEY_USERS } from '~/constants' +import { + DEFAULT_POST_CHARS_LIMIT, + DEFAULT_SERVER, + STORAGE_KEY_CURRENT_USER, + STORAGE_KEY_NOTIFICATION, + STORAGE_KEY_NOTIFICATION_POLICY, + STORAGE_KEY_SERVERS, + STORAGE_KEY_USERS, +} from '~/constants' +import type { PushNotificationPolicy, PushNotificationRequest } from '~/composables/push-notifications/types' const mock = process.mock const users = useLocalStorage(STORAGE_KEY_USERS, mock ? [mock.user] : [], { deep: true }) @@ -53,12 +62,15 @@ export async function loginTo(user?: Omit & { account?: Ac else { try { - const [me, server] = await Promise.all([ + const [me, server, pushSubscription] = await Promise.all([ masto.accounts.verifyCredentials(), masto.instances.fetch(), + // we get 404 response instead empty data + masto.pushSubscriptions.fetch().catch(() => Promise.resolve(undefined)), ]) user.account = me + user.pushSubscription = pushSubscription currentUserId.value = me.id servers.value[me.id] = server @@ -83,6 +95,37 @@ export async function loginTo(user?: Omit & { account?: Ac return masto } +export async function removePushNotifications(user: UserLogin, fromSWPushManager = true) { + // unsubscribe push notifications + try { + await useMasto().pushSubscriptions.remove() + } + catch { + // ignore + } + // clear push subscription + user.pushSubscription = undefined + const { acct } = user.account + // clear request notification permission + delete useLocalStorage(STORAGE_KEY_NOTIFICATION, {}).value[acct] + // clear push notification policy + delete useLocalStorage(STORAGE_KEY_NOTIFICATION_POLICY, {}).value[acct] + + // we remove the sw push manager if required and there are no more accounts with subscriptions + if (fromSWPushManager && (users.value.length === 0 || users.value.every(u => !u.pushSubscription))) { + // clear sw push subscription + try { + const registration = await navigator.serviceWorker.ready + const subscription = await registration.pushManager.getSubscription() + if (subscription) + await subscription.unsubscribe() + } + catch { + // juts ignore + } + } +} + export async function signout() { // TODO: confirm if (!currentUser.value) @@ -97,6 +140,8 @@ export async function signout() { clearUserLocalStorage() delete servers.value[_currentUserId] + await removePushNotifications(currentUser.value) + currentUserId.value = '' // Remove the current user from the users users.value.splice(index, 1) diff --git a/config/pwa.ts b/config/pwa.ts new file mode 100644 index 00000000..5f288d1e --- /dev/null +++ b/config/pwa.ts @@ -0,0 +1,51 @@ +import { isCI } from 'std-env' +import type { VitePWANuxtOptions } from '../modules/pwa/types' + +const isPreview = process.env.PULL_REQUEST === 'true' + +const pwa: VitePWANuxtOptions = { + mode: isCI ? 'production' : 'development', + // disabled PWA only on production + disable: !isPreview && process.env.VITE_DEV_PWA !== 'true', + scope: '/', + srcDir: './service-worker', + filename: 'sw.ts', + strategies: 'injectManifest', + injectRegister: false, + includeManifestIcons: false, + manifest: { + scope: '/', + id: '/', + name: `Elk${isCI ? isPreview ? ' (preview)' : '' : ' (dev)'}`, + short_name: `Elk${isCI ? isPreview ? ' (preview)' : '' : ' (dev)'}`, + description: `A nimble Mastodon Web Client${isCI ? isPreview ? ' (preview)' : '' : ' (development)'}`, + theme_color: '#ffffff', + icons: [ + { + src: 'pwa-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: 'pwa-512x512.png', + sizes: '512x512', + type: 'image/png', + }, + { + src: 'logo.svg', + sizes: '250x250', + type: 'image/png', + purpose: 'any maskable', + }, + ], + }, + injectManifest: { + globPatterns: ['**/*.{js,json,css,html,txt,svg,png,ico,webp,woff,woff2,ttf,eot,otf,wasm}'], + }, + devOptions: { + enabled: process.env.VITE_DEV_PWA === 'true', + type: 'module', + }, +} + +export { pwa } diff --git a/constants/index.ts b/constants/index.ts index 676e6cb1..9b9e225c 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -15,5 +15,7 @@ export const STORAGE_KEY_FEATURE_FLAGS = 'elk-feature-flags' export const STORAGE_KEY_HIDE_EXPLORE_POSTS_TIPS = 'elk-hide-explore-posts-tips' export const STORAGE_KEY_HIDE_EXPLORE_NEWS_TIPS = 'elk-hide-explore-news-tips' export const STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS = 'elk-hide-explore-tags-tips' +export const STORAGE_KEY_NOTIFICATION = 'elk-notification' +export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy' export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/ diff --git a/error.vue b/error.vue index 7ea016fc..c4b9ee86 100644 --- a/error.vue +++ b/error.vue @@ -54,4 +54,5 @@ const reload = async () => { + diff --git a/https-dev-config/local-https-server.mjs b/https-dev-config/local-https-server.mjs new file mode 100644 index 00000000..6ea2469a --- /dev/null +++ b/https-dev-config/local-https-server.mjs @@ -0,0 +1,12 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +process.env.NITRO_SSL_CERT = readFileSync(fileURLToPath(new URL('./localhost.crt', import.meta.url)), 'utf8') +process.env.NITRO_SSL_KEY = readFileSync(fileURLToPath(new URL('./localhost.key', import.meta.url)), 'utf8') + +async function run() { + await import('../.output/server/index.mjs') +} + +run() + diff --git a/https-dev-config/localhost.crt b/https-dev-config/localhost.crt new file mode 100644 index 00000000..483798f4 --- /dev/null +++ b/https-dev-config/localhost.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELjCCApagAwIBAgIRAKdt7EA97mviRgEBUUnM2LAwDQYJKoZIhvcNAQELBQAw +dTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSUwIwYDVQQLDBxCTEFD +S1JPQ0tcSm9hcXXDrW5AQmxhY2tSb2NrMSwwKgYDVQQDDCNta2NlcnQgQkxBQ0tS +T0NLXEpvYXF1w61uQEJsYWNrUm9jazAeFw0yMjA4MzAyMTQxNTRaFw0yNDExMzAy +MjQxNTRaMFAxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0 +ZTElMCMGA1UECwwcQkxBQ0tST0NLXEpvYXF1w61uQEJsYWNrUm9jazCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAPepkg2Nec3FUxqNfrq/8pHXL88G2Bsn +Oyy2bJ1D3k9/7Mn5RkZ67dCs9XVa4u5gtkGnMy+S5FqGyhahEaaW6k45Vbs8uIgE +1i8tx90r6rtIqXedkJyrhdr5xZWNzzj2ItmFkU1KGnCbFj8ZgXLW2miqXbWgpLLe +eRTOIadcQJlQJC5LTAIzOSsZyWvrQw2UaOjAqrSdFbXm0/G/V6MFBlsat6MgsFDg +8JuvITDYX6dX0jhtO5mQvJRESkP/5TaOdxzxjjTnXrTEIYn+QJUJ+rwa2d9fv7pM +CdQ0kHBezYKzp2NMKp7rpIQxFbFzR9NADk8wLaAQMUBz5QS435Q9998CAwEAAaNe +MFwwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQY +MBaAFAnP38cmUbpd5YTbKDLGa6I2FkzyMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAN +BgkqhkiG9w0BAQsFAAOCAYEAUIQp0DtACRVAAat4Sl1kzbOI35aIQjkSsX4KgIgC +8HX3qFa4NpbOBmshvgAZFrNQzJS/dLz3oOg7Ww/UH6BZjQT5QCHK7ASA12Ick1Iz +V3aTicaXn7ZyHMOpJwJXgwh6Ekv/sNjr7Sc0NahisRkAR1KglymQtYm3bGbFEKzW +0pyGnDvDBmLuQCfxq2ZwnX7eqM9R3BXBVRFe3uRoqIdwHorlupZ8N+rfyumJjIUP +gOWDUf3VuqnqhjK0BMDdTEkF8po9YQ5Qtj5Iw0JSSBfWE4WJsqyC+4EueCCHuNYs +rTtItjGT6WNGnEGI8VNijtmwHL1cPDmE6l+YrnCK0JG0u21Y+osWMeRhJhGAPY4d +cIu15gcm/PRG0hdYZrGuz18hKNy9NRtIK+na8k2R6o+uNDekIB7Pk6DHoA8Z6UDU +Q5Au+2FAt6mMyFNcifj965nPAnnSv1hg45fFwIww8edClXqfB6MCNxFO6Yzfn2UD +ABNN450+8Nd1pgXJEifiDAIe +-----END CERTIFICATE----- diff --git a/https-dev-config/localhost.key b/https-dev-config/localhost.key new file mode 100644 index 00000000..3d9d8fcd --- /dev/null +++ b/https-dev-config/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD3qZINjXnNxVMa +jX66v/KR1y/PBtgbJzsstmydQ95Pf+zJ+UZGeu3QrPV1WuLuYLZBpzMvkuRahsoW +oRGmlupOOVW7PLiIBNYvLcfdK+q7SKl3nZCcq4Xa+cWVjc849iLZhZFNShpwmxY/ +GYFy1tpoql21oKSy3nkUziGnXECZUCQuS0wCMzkrGclr60MNlGjowKq0nRW15tPx +v1ejBQZbGrejILBQ4PCbryEw2F+nV9I4bTuZkLyUREpD/+U2jncc8Y405160xCGJ +/kCVCfq8GtnfX7+6TAnUNJBwXs2Cs6djTCqe66SEMRWxc0fTQA5PMC2gEDFAc+UE +uN+UPfffAgMBAAECggEBAO84XLo4iInI6x+/wsSSObTDXQuk+cMontDumHVDpA24 +bDkfTdEwVlv1ZNbpZj+JLSK3ZQqz4VzLy5IWHJ2EMmhCm1vTKA9CVLyXhPFOxVoH +sqG2kYOzbgT4s/BkXOARZ9IiYRp91JImS1PByDbr72WgAgo5VDzuBZiiDwHAaylp +yDwwvPPtps6cP7k30RW7l3W2CmL2p8cta9g8NfNBmq6NHDjwsqvxzPNZ2O2S+V70 +55DBH2yNtHa7lwDC9jcwULhtGk2k9kqwfyd9c+QxeIxX/sA7xDJSmK72yutTT+Hw +F5Cttw6aifcRQURoLQ6Qwm1iP93rKN/FRxqGlCFpDMECgYEA/4gL0/m59gaCjShK +SyfaTkGzqJWVNEiHq8Qc9/2DQ4cl0u5aKI6H7wBRiTXfpeYmWmDjGGAEl0UlxtFu +c7OREA47wbgNc4cBvgrrslUzyduV+0CIRWypxgiT+KTl7T2lVy5VreYlr4gaM0rt +68N3jbktyM1R57aIs5XJODAtZWECgYEA+B3UkndgAyk8tUrZaOn7jE4ZnwrLUnRw +nGbAiZG3lmZEULO8jBFJFM2oeuj6467+ckZRviIVWQ8T2KqMye8eB7+fHOBOKCXs +PsCUV/asqN9ibh9UumOKkAsnO1G4p1+EJHzYNNucO3dEF2QTmEQvyHnw4tvxgos5 +bf1YJjZLJT8CgYEAwDkDTM6LCXwUMUOhv6+XFU9vat47gz0cciXw9MyMNfwwg+Ax +iljOAQhoTaNtPktHhq1jqC5yxaiKpmldgUQPV9idMzjVRZbFxMRKUbiuYKcCyCLf +X/pCLGq/hUfmfvTksBR2934t00G7E+LF35kHEmG/A1MQzhIN+6ot2ErFm4ECgYAH +o9OB1w8ryb9GzdE3+8x1G4qKbSipl1BIYJmZItWGWgvMeFxb68RWUabYcggXrrHD +DwtBUYdawK4Zw9al+SjxkCL0HqwJbHGD1SY8NypF4OsE/Q3810fS+6TvnKqU7MoC +3Z1Cs2hyJFACcGByFddq0uZp9d/P5z2Td3OZaZ6SvQKBgEQ754VsRiO9zYR70kXf +5ZG75rZICgu14fHxwStCWghvD/4AT53y6kQ1gpdTEKspCmYv3f5xbOPVIW4agHEw +DHZK7EsLEW1EmXdA9QIBKgcBqaGEzuKVtWFzCgNupssC8N9ys3X8r2nNM4qeXHsz +t1+EzpMzGsmMWuJ5l0TcI+W+ +-----END PRIVATE KEY----- diff --git a/locales/en-US.json b/locales/en-US.json index 6d3b0380..f708e3ce 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -144,6 +144,38 @@ "missing_type": "MISSING notification.type:", "reblogged_post": "reblogged your post", "request_to_follow": "requested to follow you", + "settings": { + "alerts": { + "favourite": "Favourites", + "follow": "New followers", + "mention": "Mentions", + "poll": "Polls", + "reblog": "Reblog your post", + "title": "What notifications to receive?" + }, + "close_btn": "Close desktop notification settings", + "policy": { + "all": "From anyone", + "followed": "Of people I follow", + "follower": "Of people who follow me", + "none": "From no one", + "title": "Who can I receive notifications from?" + }, + "save_settings": "Save settings changes", + "show_btn": "Show desktop notification settings", + "title": "Desktop notification settings", + "undo_settings": "Undo settings changes", + "unsubscribe": "Disable desktop notifications", + "unsubscribed_with_warning": "Enable notifications to receive notifications from this account by clicking \"@:notification.settings.warning.enable_desktop{'\"'} button.", + "unsupported": "Your browser does not support desktop notifications.", + "warning": { + "enable_close": "Close", + "enable_description": "To receive notifications when Elk is not open, enable desktop notifications. You can control precisely what types of interactions generate desktop notifications via the \"Show Settings\" button above once enabled.", + "enable_description_short": "To change desktop notification settings when Elk is not open, you must first enable desktop notifications.", + "enable_desktop": "Enable desktop notifications", + "enable_title": "Never miss anything" + } + }, "update_status": "updated their status" }, "placeholder": { @@ -153,6 +185,12 @@ "replying": "Replying", "the_thread": "the thread" }, + "pwa": { + "close": "Close", + "message": "@:pwa.title{','} click on @:pwa.reload button to update.", + "reload": "Reload", + "title": "New Elk version available" + }, "search": { "search_desc": "Search for people & hashtags" }, diff --git a/locales/es-ES.json b/locales/es-ES.json index e92b7045..88bcc815 100644 --- a/locales/es-ES.json +++ b/locales/es-ES.json @@ -141,6 +141,38 @@ "missing_type": "MISSING notification.type:", "reblogged_post": "retooteó tu publicación", "request_to_follow": "ha solicitado seguirte", + "settings": { + "alerts": { + "favourite": "Favoritos", + "follow": "Nuevos seguidores", + "mention": "Menciones", + "poll": "Encuestas", + "reblog": "Retooteo de tus publicaciones", + "title": "¿Qué notificaciones recibir?" + }, + "close_btn": "Cerrar ajuste de las notificaciones de escritorio", + "policy": { + "all": "De cualquier persona", + "followed": "De personas que sigo", + "follower": "De personas que me siguen", + "none": "De nadie", + "title": "¿De quién puedo recibir notificaciones?" + }, + "save_settings": "Guardar cambios en los ajustes", + "show_btn": "Mostrar ajustes de las notificaciones de escritorio", + "title": "Ajustes de notificaciones de escritorio", + "undo_settings": "Deshacer cambios en los ajustes", + "unsubscribe": "Cancelar notificaciones de escritorio", + "unsubscribed_with_warning": "Habilite las notificaciones para recibir notificaciones de esta cuenta haciendo clic en el botón \"@:notification.settings.warning.enable_desktop{'\"'}.", + "unsupported": "Tu navegador no soporta notificaciones de escritorio.", + "warning": { + "enable_close": "Cerrar", + "enable_description": "Para recibir notificaciones cuando Elk no esté abierto, habilite las notificaciones de escritorio. Puedes controlar con precisión qué tipos de interacciones generan notificaciones de escritorio a través del botón \"Mostrar ajustes\" de arriba una vez que estén habilitadas.", + "enable_description_short": "Para cambiar los ajustes de las notificaciones de escritorio cuando Elk no esté abierto, debe habilitar antes las notificaciones de escritorio.", + "enable_desktop": "Habilitar notificaciones de escritorio", + "enable_title": "Nunca te pierdas nada" + } + }, "update_status": "ha actualizado su estado" }, "placeholder": { @@ -150,6 +182,12 @@ "replying": "Respondiendo", "the_thread": "el hilo" }, + "pwa": { + "close": "Cerrar", + "message": "@:pwa.title{','} haz click en el botón @:pwa.reload para actualizar.", + "reload": "Recargar", + "title": "Nueva versión de Elk disponible" + }, "state": { "edited": "(Editado)", "editing": "Editando", diff --git a/modules/pwa/config.ts b/modules/pwa/config.ts new file mode 100644 index 00000000..262f00ab --- /dev/null +++ b/modules/pwa/config.ts @@ -0,0 +1,69 @@ +import type { Nuxt } from '@nuxt/schema' +import type { VitePWAOptions } from 'vite-plugin-pwa' +import { resolve } from 'pathe' + +export function configurePWAOptions(options: Partial, nuxt: Nuxt) { + if (!options.outDir) { + const publicDir = nuxt.options.nitro?.output?.publicDir + options.outDir = publicDir ? resolve(publicDir) : resolve(nuxt.options.buildDir, '../.output/public') + } + + let config: Partial< + import('workbox-build').BasePartial + & import('workbox-build').GlobPartial + & import('workbox-build').RequiredGlobDirectoryPartial + > + + if (options.strategies === 'injectManifest') { + options.injectManifest = options.injectManifest ?? {} + config = options.injectManifest + } + else { + options.workbox = options.workbox ?? {} + if (options.registerType === 'autoUpdate' && (options.injectRegister === 'script' || options.injectRegister === 'inline')) { + options.workbox.clientsClaim = true + options.workbox.skipWaiting = true + } + if (nuxt.options.dev) { + // on dev force always to use the root + + options.workbox.navigateFallback = nuxt.options.app.baseURL ?? '/' + if (options.devOptions?.enabled && !options.devOptions.navigateFallbackAllowlist) + options.devOptions.navigateFallbackAllowlist = [new RegExp(nuxt.options.app.baseURL) ?? /\//] + } + config = options.workbox + // todo: change navigateFallback based on the command: use 404 only when using generate + /* else if (nuxt.options.build) { + if (!options.workbox.navigateFallback) + options.workbox.navigateFallback = '/200.html' + } */ + } + if (!nuxt.options.dev) + config.manifestTransforms = [createManifestTransform(nuxt.options.app.baseURL ?? '/')] +} + +function createManifestTransform(base: string): import('workbox-build').ManifestTransform { + return async (entries) => { + // prefix non html assets with base + /* + entries.filter(e => e && !e.url.endsWith('.html')).forEach((e) => { + if (!e.url.startsWith(base)) + e.url = `${base}${e.url}` + }) +*/ + entries.filter(e => e && e.url.endsWith('.html')).forEach((e) => { + const url = e.url.startsWith('/') ? e.url.slice(1) : e.url + if (url === 'index.html') { + e.url = base + } + else { + const parts = url.split('/') + parts[parts.length - 1] = parts[parts.length - 1].replace(/\.html$/, '') + // e.url = `${base}${parts.length > 1 ? parts.slice(0, parts.length - 1).join('/') : parts[0]}` + e.url = parts.length > 1 ? parts.slice(0, parts.length - 1).join('/') : parts[0] + } + }) + + return { manifest: entries, warnings: [] } + } +} diff --git a/modules/pwa/index.ts b/modules/pwa/index.ts new file mode 100644 index 00000000..40d34357 --- /dev/null +++ b/modules/pwa/index.ts @@ -0,0 +1,83 @@ +import { defineNuxtModule } from '@nuxt/kit' +import type { VitePluginPWAAPI } from 'vite-plugin-pwa' +import { VitePWA } from 'vite-plugin-pwa' +import type { Plugin } from 'vite' +import type { VitePWANuxtOptions } from './types' +import { configurePWAOptions } from './config' + +export * from './types' +export default defineNuxtModule({ + meta: { + name: 'pwa', + configKey: 'pwa', + }, + defaults: nuxt => ({ + base: nuxt.options.app.baseURL, + scope: nuxt.options.app.baseURL, + }), + async setup(options, nuxt) { + let vitePwaClientPlugin: Plugin | undefined + const resolveVitePluginPWAAPI = (): VitePluginPWAAPI | undefined => { + return vitePwaClientPlugin?.api + } + + // TODO: combine with configurePWAOptions? + nuxt.hook('nitro:init', (nitro) => { + options.outDir = nitro.options.output.publicDir + options.injectManifest = options.injectManifest || {} + options.injectManifest.globDirectory = nitro.options.output.publicDir + }) + nuxt.hook('vite:extend', ({ config }) => { + const plugin = config.plugins?.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa') + if (plugin) + throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!') + }) + nuxt.hook('vite:extendConfig', (viteInlineConfig, { isClient }) => { + viteInlineConfig.plugins = viteInlineConfig.plugins || [] + const plugin = viteInlineConfig.plugins.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa') + if (plugin) + throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!') + + configurePWAOptions(options, nuxt) + const plugins = VitePWA(options) + viteInlineConfig.plugins.push(plugins) + if (isClient) + vitePwaClientPlugin = plugins.find(p => p.name === 'vite-plugin-pwa') as Plugin + }) + + if (nuxt.options.dev) { + const webManifest = `${nuxt.options.app.baseURL}${options.devOptions?.webManifestUrl ?? options.manifestFilename ?? 'manifest.webmanifest'}` + const devSw = `${nuxt.options.app.baseURL}dev-sw.js?dev-sw` + const workbox = `${nuxt.options.app.baseURL}workbox-` + // @ts-expect-error just ignore + const emptyHandle = (_req, _res, next) => { + next() + } + nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => { + if (isServer) + return + + viteServer.middlewares.stack.push({ route: webManifest, handle: emptyHandle }) + viteServer.middlewares.stack.push({ route: devSw, handle: emptyHandle }) + }) + if (!options.strategies || options.strategies === 'generateSW') { + nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => { + if (isServer) + return + + viteServer.middlewares.stack.push({ route: workbox, handle: emptyHandle }) + }) + nuxt.hook('close', async () => { + // todo: cleanup dev-dist folder + }) + } + } + else { + nuxt.hook('nitro:init', (nitro) => { + nitro.hooks.hook('rollup:before', async () => { + await resolveVitePluginPWAAPI()?.generateSW() + }) + }) + } + }, +}) diff --git a/modules/pwa/types.ts b/modules/pwa/types.ts new file mode 100644 index 00000000..90446c52 --- /dev/null +++ b/modules/pwa/types.ts @@ -0,0 +1,12 @@ +import type { VitePWAOptions } from 'vite-plugin-pwa' + +export interface VitePWANuxtOptions extends Partial {} + +declare module '@nuxt/schema' { + interface NuxtConfig { + pwa?: { [K in keyof VitePWANuxtOptions]?: Partial } + } + interface NuxtOptions { + pwa: VitePWANuxtOptions + } +} diff --git a/netlify.toml b/netlify.toml index d6f1f50a..9060086f 100755 --- a/netlify.toml +++ b/netlify.toml @@ -8,8 +8,3 @@ to = "https://discord.gg/vAZSDU9J" status = 301 force = true - -[[redirects]] - from = "/*" - to = "/index.html" - status = 200 diff --git a/nuxt.config.ts b/nuxt.config.ts index ac7704d6..a28dae25 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,10 +2,16 @@ import { fileURLToPath } from 'node:url' import Inspect from 'vite-plugin-inspect' import { isCI } from 'std-env' import { i18n } from './config/i18n' +import { pwa } from './config/pwa' const isPreview = process.env.PULL_REQUEST === 'true' export default defineNuxtConfig({ + typescript: { + tsConfig: { + exclude: ['../service-worker'], + }, + }, modules: [ '@vueuse/nuxt', '@unocss/nuxt', @@ -14,6 +20,7 @@ export default defineNuxtConfig({ '@nuxtjs/i18n', '~/modules/purge-comments', '~/modules/setup-components', + '~/modules/pwa/index', // change to '@vite-pwa/nuxt' once released and remove pwa module ], experimental: { reactivityTransform: true, @@ -66,6 +73,7 @@ export default defineNuxtConfig({ }, public: { env: isCI ? isPreview ? 'staging' : 'production' : 'local', + pwaEnabled: isCI || process.env.VITE_DEV_PWA === 'true', translateApi: '', // Masto uses Mastodon version checks to see what features are enabled. // Mastodon alternatives like GoToSocial will always fail these checks, so @@ -77,6 +85,13 @@ export default defineNuxtConfig({ fsBase: 'node_modules/.cache/servers', }, }, + routeRules: { + '/manifest.webmanifest': { + headers: { + 'Content-Type': 'application/manifest+json', + }, + }, + }, nitro: { publicAssets: [ ...(!isCI || isPreview ? [{ dir: fileURLToPath(new URL('./public-dev', import.meta.url)) }] : []), @@ -99,10 +114,14 @@ export default defineNuxtConfig({ { rel: 'alternate icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'icon', type: 'image/png', href: '/favicon-16x16.png', sizes: '16x16' }, { rel: 'icon', type: 'image/png', href: '/favicon-32x32.png', sizes: '32x32' }, + { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#ffffff' }, + { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' }, ], + meta: [{ name: 'theme-color', content: '#ffffff' }], }, }, i18n, + pwa, }) declare global { diff --git a/package.json b/package.json index 1fc059fe..35800927 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,21 @@ "homepage": "https://elk.zone/", "scripts": { "build": "nuxi build", + "build:pwa": "VITE_DEV_PWA=true nuxi build", + "build:netlify:pwa": "VITE_DEV_PWA=true NITRO_PRESET=netlify nuxi build", "dev": "nuxi dev --port 5314", + "dev:pwa": "VITE_DEV_PWA=true nuxi dev --port 5314", "dev:mocked": "nuxi dev --port 5314 --dotenv .env.mock", + "dev:mocked:pwa": "VITE_DEV_PWA=true nuxi dev --port 5314 --dotenv .env.mock", + "dev:mocked:pwa:ssl": "VITE_DEV_PWA=true nuxi dev --port 5314 --https --ssl-cert ./https-dev-config/localhost.crt --ssl-key ./https-dev-config/localhost.key --dotenv .env.mock", "start": "PORT=5314 node .output/server/index.mjs", + "start:https": "PORT=5314 node ./https-dev-config/local-https-server.mjs", "lint": "eslint .", "typecheck": "nuxi typecheck", "prepare": "esno scripts/prepare.ts", "generate": "nuxi generate", "test:unit": "vitest", - "test:typecheck": "vue-tsc --noEmit", + "test:typecheck": "vue-tsc --noEmit && vue-tsc --noEmit --project service-worker/tsconfig.json", "test": "nr test:unit", "postinstall": "nuxi prepare && simple-git-hooks" }, @@ -82,9 +88,11 @@ "ultrahtml": "^1.0.4", "unplugin-auto-import": "^0.12.0", "vite-plugin-inspect": "^0.7.9", + "vite-plugin-pwa": "^0.13.3", "vitest": "^0.25.3", "vue-tsc": "^1.0.11", - "vue-virtual-scroller": "2.0.0-beta.4" + "vue-virtual-scroller": "2.0.0-beta.4", + "workbox-window": "^6.5.4" }, "simple-git-hooks": { "pre-commit": "pnpm lint-staged" diff --git a/pages/notifications.vue b/pages/notifications.vue index 591a882c..2f059c5d 100644 --- a/pages/notifications.vue +++ b/pages/notifications.vue @@ -4,6 +4,8 @@ definePageMeta({ }) const { t } = useI18n() +const showSettings = ref(false) +const pwaEnabled = useRuntimeConfig().public.pwaEnabled const tabs = $computed(() => [ { @@ -17,6 +19,10 @@ const tabs = $computed(() => [ display: t('tab.notifications_mention'), }, ] as const) + +onActivated(() => { + showSettings.value = false +}) + + - + + + + + diff --git a/plugins/masto.ts b/plugins/masto.ts index aac2d301..3d33b925 100644 --- a/plugins/masto.ts +++ b/plugins/masto.ts @@ -43,7 +43,11 @@ export default defineNuxtPlugin(async (nuxtApp) => { if (process.client) { const { query } = useRoute() const user = typeof query.server === 'string' && typeof query.token === 'string' - ? { server: query.server, token: query.token } + ? { + server: query.server, + token: query.token, + vapidKey: typeof query.vapid_key === 'string' ? query.vapid_key : undefined, + } : currentUser.value nuxtApp.hook('app:suspense:resolve', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2d1a95e..71dafc17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,11 @@ specifiers: ultrahtml: ^1.0.4 unplugin-auto-import: ^0.12.0 vite-plugin-inspect: ^0.7.9 + vite-plugin-pwa: ^0.13.3 vitest: ^0.25.3 vue-tsc: ^1.0.11 vue-virtual-scroller: 2.0.0-beta.4 + workbox-window: ^6.5.4 dependencies: '@fnando/sparkline': 0.3.10 @@ -130,9 +132,11 @@ devDependencies: ultrahtml: 1.0.4 unplugin-auto-import: 0.12.0 vite-plugin-inspect: 0.7.9 + vite-plugin-pwa: 0.13.3 vitest: 0.25.3_jsdom@20.0.3 vue-tsc: 1.0.11_typescript@4.9.3 vue-virtual-scroller: 2.0.0-beta.4 + workbox-window: 6.5.4 packages: @@ -257,6 +261,18 @@ packages: resolution: {integrity: sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==} dev: true + /@apideck/better-ajv-errors/0.3.6_ajv@8.11.2: + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + dependencies: + ajv: 8.11.2 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + dev: true + /@babel/code-frame/7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} @@ -308,6 +324,14 @@ packages: '@babel/types': 7.20.5 dev: true + /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9: + resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-explode-assignable-expression': 7.18.6 + '@babel/types': 7.20.5 + dev: true + /@babel/helper-compilation-targets/7.20.0_@babel+core@7.20.5: resolution: {integrity: sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==} engines: {node: '>=6.9.0'} @@ -339,11 +363,45 @@ packages: - supports-color dev: true + /@babel/helper-create-regexp-features-plugin/7.20.5_@babel+core@7.20.5: + resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.2.2 + dev: true + + /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.20.5: + resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/helper-environment-visitor/7.18.9: resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} engines: {node: '>=6.9.0'} dev: true + /@babel/helper-explode-assignable-expression/7.18.6: + resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.5 + dev: true + /@babel/helper-function-name/7.19.0: resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} engines: {node: '>=6.9.0'} @@ -401,6 +459,21 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-wrap-function': 7.20.5 + '@babel/types': 7.20.5 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/helper-replace-supers/7.19.1: resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==} engines: {node: '>=6.9.0'} @@ -421,6 +494,13 @@ packages: '@babel/types': 7.20.5 dev: true + /@babel/helper-skip-transparent-expression-wrappers/7.20.0: + resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.5 + dev: true + /@babel/helper-split-export-declaration/7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} @@ -443,6 +523,18 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/helper-wrap-function/7.20.5: + resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.19.0 + '@babel/template': 7.18.10 + '@babel/traverse': 7.20.5 + '@babel/types': 7.20.5 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/helpers/7.20.6: resolution: {integrity: sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==} engines: {node: '>=6.9.0'} @@ -471,6 +563,277 @@ packages: '@babel/types': 7.20.5 dev: true + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-async-generator-functions/7.20.1_@babel+core@7.20.5: + resolution: {integrity: sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-object-rest-spread/7.20.2_@babel+core@7.20.5: + resolution: {integrity: sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.5 + '@babel/core': 7.20.5 + '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-transform-parameters': 7.20.5_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-optional-chaining/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.5 + dev: true + + /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-private-property-in-object/7.20.5_@babel+core@7.20.5: + resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.20.5: + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.20.5: + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.20.5: + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.20.5: + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.20.5: + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-import-assertions/7.20.0_@babel+core@7.20.5: + resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.20.5: + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.20.5: resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} engines: {node: '>=6.9.0'} @@ -481,6 +844,80 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.20.5: + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.20.5: + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.20.5: + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.20.5: + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.20.5: + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.20.5: + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.20.5: + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.20.5: + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-typescript/7.20.0_@babel+core@7.20.5: resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} engines: {node: '>=6.9.0'} @@ -491,6 +928,345 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-block-scoping/7.20.5_@babel+core@7.20.5: + resolution: {integrity: sha512-WvpEIW9Cbj9ApF3yJCjIEEf1EiNJLtXagOrL5LNWEZOo3jv8pmPoYTSNJQvqej8OavVlgOoOPw6/htGZro6IkA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-classes/7.20.2_@babel+core@7.20.5: + resolution: {integrity: sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.19.1 + '@babel/helper-split-export-declaration': 7.18.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-destructuring/7.20.2_@babel+core@7.20.5: + resolution: {integrity: sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.20.5: + resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-literals/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-modules-amd/7.19.6_@babel+core@7.20.5: + resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-module-transforms': 7.20.2 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs/7.19.6_@babel+core@7.20.5: + resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-module-transforms': 7.20.2 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-simple-access': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs/7.19.6_@babel+core@7.20.5: + resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-module-transforms': 7.20.2 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-identifier': 7.19.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-module-transforms': 7.20.2 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex/7.20.5_@babel+core@7.20.5: + resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.19.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-parameters/7.20.5_@babel+core@7.20.5: + resolution: {integrity: sha512-h7plkOmcndIUWXZFLgpbrh2+fXAi47zcUX7IrOQuZdLD0I0KvjJ6cvo3BEcAOsDOcZhVKGJqv07mkSqK0y2isQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-regenerator/7.20.5_@babel+core@7.20.5: + resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + regenerator-transform: 0.15.1 + dev: true + + /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-spread/7.19.0_@babel+core@7.20.5: + resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + dev: true + + /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.20.5: + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-typescript/7.20.2_@babel+core@7.20.5: resolution: {integrity: sha512-jvS+ngBfrnTUBfOQq8NfGnSbF9BrqlR6hjJ2yVxMkmO5nL/cdifNbI30EfjRlN4g5wYWNnMPyj5Sa6R1pbLeag==} engines: {node: '>=6.9.0'} @@ -505,6 +1281,133 @@ packages: - supports-color dev: true + /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.20.5: + resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.20.5: + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/preset-env/7.20.2_@babel+core@7.20.5: + resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.5 + '@babel/core': 7.20.5 + '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.18.6 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-proposal-async-generator-functions': 7.20.1_@babel+core@7.20.5 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-object-rest-spread': 7.20.2_@babel+core@7.20.5 + '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-proposal-private-property-in-object': 7.20.5_@babel+core@7.20.5 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.5 + '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.20.5 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-syntax-import-assertions': 7.20.0_@babel+core@7.20.5 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.5 + '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.5 + '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-block-scoping': 7.20.5_@babel+core@7.20.5 + '@babel/plugin-transform-classes': 7.20.2_@babel+core@7.20.5 + '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-transform-destructuring': 7.20.2_@babel+core@7.20.5 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.20.5 + '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-modules-amd': 7.19.6_@babel+core@7.20.5 + '@babel/plugin-transform-modules-commonjs': 7.19.6_@babel+core@7.20.5 + '@babel/plugin-transform-modules-systemjs': 7.19.6_@babel+core@7.20.5 + '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5_@babel+core@7.20.5 + '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-parameters': 7.20.5_@babel+core@7.20.5 + '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-regenerator': 7.20.5_@babel+core@7.20.5 + '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.20.5 + '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.20.5 + '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.20.5 + '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.5 + '@babel/preset-modules': 0.1.5_@babel+core@7.20.5 + '@babel/types': 7.20.5 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.20.5 + babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.5 + babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.5 + core-js-compat: 3.26.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules/0.1.5_@babel+core@7.20.5: + resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.5 + '@babel/types': 7.20.5 + esutils: 2.0.3 + dev: true + + /@babel/runtime/7.20.6: + resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: true + /@babel/standalone/7.20.6: resolution: {integrity: sha512-u5at/CbBLETf7kx2LOY4XdhseD79Y099WZKAOMXeT8qvd9OSR515my2UNBBLY4qIht/Qi9KySeQHQwQwxJN4Sw==} engines: {node: '>=6.9.0'} @@ -1183,6 +2086,23 @@ packages: slash: 4.0.0 dev: true + /@rollup/plugin-babel/5.3.1_opjstonlpkhafnz76jsxdwq25a: + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-module-imports': 7.18.6 + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + rollup: 2.79.1 + dev: true + /@rollup/plugin-commonjs/23.0.3_rollup@2.79.1: resolution: {integrity: sha512-31HxrT5emGfTyIfAs1lDQHj6EfYxTXcwtX5pIIhq+B/xZBNIqQ179d/CkYxlpYmFCxT78AeU4M8aL8Iv/IBxFA==} engines: {node: '>=14.0.0'} @@ -1229,6 +2149,21 @@ packages: rollup: 2.79.1 dev: true + /@rollup/plugin-node-resolve/11.2.1_rollup@2.79.1: + resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + '@types/resolve': 1.17.1 + builtin-modules: 3.3.0 + deepmerge: 4.2.2 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 2.79.1 + dev: true + /@rollup/plugin-node-resolve/15.0.1_rollup@2.79.1: resolution: {integrity: sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==} engines: {node: '>=14.0.0'} @@ -1247,6 +2182,26 @@ packages: rollup: 2.79.1 dev: true + /@rollup/plugin-replace/2.4.2_rollup@2.79.1: + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + magic-string: 0.25.9 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-replace/4.0.0_rollup@2.79.1: + resolution: {integrity: sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + magic-string: 0.25.9 + rollup: 2.79.1 + dev: true + /@rollup/plugin-replace/5.0.1_rollup@2.79.1: resolution: {integrity: sha512-Z3MfsJ4CK17BfGrZgvrcp/l6WXoKb0kokULO+zt/7bmcyayokDaQ2K3eDJcRLCTAlp5FPI4/gz9MHAsosz4Rag==} engines: {node: '>=14.0.0'} @@ -1273,6 +2228,18 @@ packages: rollup: 2.79.1 dev: true + /@rollup/pluginutils/3.1.0_rollup@2.79.1: + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + /@rollup/pluginutils/4.2.1: resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -1310,6 +2277,15 @@ packages: rollup: 2.79.1 dev: true + /@surma/rollup-plugin-off-main-thread/2.2.3: + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + dependencies: + ejs: 3.1.8 + json5: 2.2.1 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.8 + dev: true + /@tauri-apps/api/1.2.0: resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==} engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} @@ -1612,6 +2588,10 @@ packages: resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} dev: true + /@types/estree/0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + /@types/estree/1.0.0: resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} dev: true @@ -1656,6 +2636,12 @@ packages: resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==} dev: true + /@types/resolve/1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 18.11.10 + dev: true + /@types/resolve/1.20.2: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true @@ -1664,6 +2650,10 @@ packages: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true + /@types/trusted-types/2.0.2: + resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} + dev: true + /@types/unist/2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true @@ -2608,6 +3598,15 @@ packages: uri-js: 4.4.1 dev: true + /ajv/8.11.2: + resolution: {integrity: sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + /ansi-escapes/4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2763,6 +3762,11 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true + /at-least-node/1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + /autoprefixer/10.4.13_postcss@8.4.19: resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} engines: {node: ^10 || ^12 || >=14} @@ -2789,6 +3793,42 @@ packages: - debug dev: true + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.20.5: + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.5 + '@babel/core': 7.20.5 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.5 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.20.5: + resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.5 + core-js-compat: 3.26.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.20.5: + resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.5 + transitivePeerDependencies: + - supports-color + dev: true + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -3211,6 +4251,11 @@ packages: engines: {node: ^12.20.0 || >=14} dev: true + /common-tags/1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + /commondir/1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true @@ -3253,6 +4298,12 @@ packages: resolution: {integrity: sha512-RyZrFi6PNpBFbIaQjXDlFIhFVqV42QeKSZX1yQIl6ihImq6vcHNGMtqQ/QzY3RMPuYSkvsRwtnt5M9NeYxKt0g==} dev: true + /core-js-compat/3.26.1: + resolution: {integrity: sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==} + dependencies: + browserslist: 4.21.4 + dev: true + /core-util-is/1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true @@ -3284,6 +4335,11 @@ packages: which: 2.0.2 dev: true + /crypto-random-string/2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: true + /css-declaration-sorter/6.3.1_postcss@8.4.19: resolution: {integrity: sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==} engines: {node: ^10 || ^12 || >=14} @@ -3656,6 +4712,14 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true + /ejs/3.1.8: + resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.5 + dev: true + /electron-to-chromium/1.4.284: resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==} dev: true @@ -4385,6 +5449,10 @@ packages: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} dev: true + /estree-walker/1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + /estree-walker/2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true @@ -4511,6 +5579,12 @@ packages: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} dev: true + /filelist/1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.1 + dev: true + /fill-range/7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -4632,6 +5706,16 @@ packages: universalify: 2.0.0 dev: true + /fs-extra/9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + /fs-memo/1.2.0: resolution: {integrity: sha512-YEexkCpL4j03jn5SxaMHqcO6IuWuqm8JFUYhyCep7Ao89JIYmB8xoKhK7zXXJ9cCaNXpyNH5L3QtAmoxjoHW2w==} dev: true @@ -4721,6 +5805,10 @@ packages: has-symbols: 1.0.3 dev: true + /get-own-enumerable-property-symbols/3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + dev: true + /get-port-please/2.6.1: resolution: {integrity: sha512-4PDSrL6+cuMM1xs6w36ZIkaKzzE0xzfVBCfebHIJ3FE8iB9oic/ECwPw3iNiD4h1AoJ5XLLBhEviFAVrZsDC5A==} dependencies: @@ -5032,6 +6120,10 @@ packages: safer-buffer: 2.1.2 dev: true + /idb/7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: true + /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true @@ -5259,6 +6351,11 @@ packages: engines: {node: '>=0.12.0'} dev: true + /is-obj/1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + dev: true + /is-path-inside/3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -5291,6 +6388,11 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-regexp/1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + dev: true + /is-shared-array-buffer/1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: @@ -5367,6 +6469,17 @@ packages: ws: 8.11.0 dev: true + /jake/10.8.5: + resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.4 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: true + /jest-worker/26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} @@ -5467,6 +6580,14 @@ packages: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true + /json-schema-traverse/1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + /json-stable-stringify-without-jsonify/1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true @@ -5517,6 +6638,11 @@ packages: graceful-fs: 4.2.10 dev: true + /jsonpointer/5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + dev: true + /klona/2.0.5: resolution: {integrity: sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==} engines: {node: '>= 8'} @@ -5537,6 +6663,11 @@ packages: readable-stream: 2.3.7 dev: true + /leven/3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + /levn/0.3.0: resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} engines: {node: '>= 0.8.0'} @@ -5676,6 +6807,10 @@ packages: resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} dev: true + /lodash.sortby/4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + /lodash.template/4.5.0: resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} dependencies: @@ -6993,6 +8128,11 @@ packages: hasBin: true dev: true + /pretty-bytes/5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: true + /pretty-bytes/6.0.0: resolution: {integrity: sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==} engines: {node: ^14.13.1 || >=16.0.0} @@ -7201,6 +8341,27 @@ packages: redis-errors: 1.2.0 dev: true + /regenerate-unicode-properties/10.1.0: + resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate/1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: true + + /regenerator-transform/0.15.1: + resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} + dependencies: + '@babel/runtime': 7.20.6 + dev: true + /regexp-tree/0.1.24: resolution: {integrity: sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==} hasBin: true @@ -7220,6 +8381,22 @@ packages: engines: {node: '>=8'} dev: true + /regexpu-core/5.2.2: + resolution: {integrity: sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.0 + regjsgen: 0.7.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsgen/0.7.1: + resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==} + dev: true + /regjsparser/0.9.1: resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true @@ -7232,6 +8409,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /require-from-string/2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + /requires-port/1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true @@ -7598,6 +8780,13 @@ packages: engines: {node: '>= 8'} dev: true + /source-map/0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead @@ -7671,6 +8860,19 @@ packages: strip-ansi: 7.0.1 dev: true + /string.prototype.matchall/4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + get-intrinsic: 1.1.3 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + dev: true + /string.prototype.trimend/1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: @@ -7699,6 +8901,15 @@ packages: safe-buffer: 5.2.1 dev: true + /stringify-object/3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + dev: true + /strip-ansi/6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -7718,6 +8929,11 @@ packages: engines: {node: '>=4'} dev: true + /strip-comments/2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + dev: true + /strip-final-newline/2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -7854,6 +9070,21 @@ packages: yallist: 4.0.0 dev: true + /temp-dir/2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: true + + /tempy/0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + dev: true + /terser/5.16.1: resolution: {integrity: sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==} engines: {node: '>=10'} @@ -7945,6 +9176,12 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /tr46/1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.1.1 + dev: true + /tr46/3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} @@ -8008,6 +9245,11 @@ packages: engines: {node: '>=4'} dev: true + /type-fest/0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + dev: true + /type-fest/0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -8106,6 +9348,29 @@ packages: hookable: 5.4.2 dev: true + /unicode-canonical-property-names-ecmascript/2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript/2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript/2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript/2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + /unimport/1.0.1: resolution: {integrity: sha512-SEPKl3uyqUvi6c0MnyCmUF9H07CuC9j9p2p33F03LmegU0sxjpnjL0fLKAhh7BTfcKaJKj+1iOiAFtg7P3m5mQ==} dependencies: @@ -8142,6 +9407,13 @@ packages: - rollup dev: true + /unique-string/2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + dev: true + /unist-util-stringify-position/2.0.3: resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} dependencies: @@ -8320,6 +9592,11 @@ packages: - supports-color dev: true + /upath/1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + dev: true + /update-browserslist-db/1.0.10_browserslist@4.21.4: resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} hasBin: true @@ -8444,6 +9721,23 @@ packages: - supports-color dev: true + /vite-plugin-pwa/0.13.3: + resolution: {integrity: sha512-cjWXpZ7slAY14OKz7M8XdgTIi9wjf6OD6NkhiMAc+ogxnbUrecUwLdRtfGPCPsN2ftut5gaN1jTghb11p6IQAA==} + peerDependencies: + vite: ^3.1.0 + dependencies: + '@rollup/plugin-replace': 4.0.0_rollup@2.79.1 + debug: 4.3.4 + fast-glob: 3.2.12 + pretty-bytes: 6.0.0 + rollup: 2.79.1 + workbox-build: 6.5.4 + workbox-window: 6.5.4 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: true + /vite/3.2.4: resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -8772,6 +10066,10 @@ packages: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true + /webidl-conversions/4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + /webidl-conversions/7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -8813,6 +10111,14 @@ packages: webidl-conversions: 3.0.1 dev: true + /whatwg-url/7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -8842,6 +10148,152 @@ packages: engines: {node: '>=0.10.0'} dev: true + /workbox-background-sync/6.5.4: + resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==} + dependencies: + idb: 7.1.1 + workbox-core: 6.5.4 + dev: true + + /workbox-broadcast-update/6.5.4: + resolution: {integrity: sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-build/6.5.4: + resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==} + engines: {node: '>=10.0.0'} + dependencies: + '@apideck/better-ajv-errors': 0.3.6_ajv@8.11.2 + '@babel/core': 7.20.5 + '@babel/preset-env': 7.20.2_@babel+core@7.20.5 + '@babel/runtime': 7.20.6 + '@rollup/plugin-babel': 5.3.1_opjstonlpkhafnz76jsxdwq25a + '@rollup/plugin-node-resolve': 11.2.1_rollup@2.79.1 + '@rollup/plugin-replace': 2.4.2_rollup@2.79.1 + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.11.2 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.1 + rollup-plugin-terser: 7.0.2_rollup@2.79.1 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 6.5.4 + workbox-broadcast-update: 6.5.4 + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-google-analytics: 6.5.4 + workbox-navigation-preload: 6.5.4 + workbox-precaching: 6.5.4 + workbox-range-requests: 6.5.4 + workbox-recipes: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + workbox-streams: 6.5.4 + workbox-sw: 6.5.4 + workbox-window: 6.5.4 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: true + + /workbox-cacheable-response/6.5.4: + resolution: {integrity: sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-core/6.5.4: + resolution: {integrity: sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==} + dev: true + + /workbox-expiration/6.5.4: + resolution: {integrity: sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==} + dependencies: + idb: 7.1.1 + workbox-core: 6.5.4 + dev: true + + /workbox-google-analytics/6.5.4: + resolution: {integrity: sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==} + dependencies: + workbox-background-sync: 6.5.4 + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: true + + /workbox-navigation-preload/6.5.4: + resolution: {integrity: sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-precaching/6.5.4: + resolution: {integrity: sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==} + dependencies: + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: true + + /workbox-range-requests/6.5.4: + resolution: {integrity: sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-recipes/6.5.4: + resolution: {integrity: sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==} + dependencies: + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-precaching: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: true + + /workbox-routing/6.5.4: + resolution: {integrity: sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-strategies/6.5.4: + resolution: {integrity: sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-streams/6.5.4: + resolution: {integrity: sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==} + dependencies: + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + dev: true + + /workbox-sw/6.5.4: + resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==} + dev: true + + /workbox-window/6.5.4: + resolution: {integrity: sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==} + dependencies: + '@types/trusted-types': 2.0.2 + workbox-core: 6.5.4 + dev: true + /wrap-ansi/6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} diff --git a/public-dev/apple-touch-icon.png b/public-dev/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f134f6da8c8e62746ac0664469d52c38effbd74d GIT binary patch literal 6552 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 literal 0 HcmV?d00001 diff --git a/public-dev/pwa-192x192.png b/public-dev/pwa-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..a1aeffba310016a803e14232e0ce90a684858518 GIT binary patch literal 6947 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 AlK=n! literal 0 HcmV?d00001 diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 00000000..78d04f7c --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/pwa-192x192.png b/public/pwa-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..660e94e385194da50e9458a549678fb2e9944e43 GIT binary patch literal 7365 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 Lda9q4Z6p5&`=7F^ literal 0 HcmV?d00001 diff --git a/public/pwa-512x512.png b/public/pwa-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..48c1e18484b3fe446896804df911e51431d35dd5 GIT binary patch literal 20140 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 n)YaZl+f=JsD=FgI^Y`k4FUs + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + + + + diff --git a/server/api/[server]/oauth.ts b/server/api/[server]/oauth.ts index 8c8e33a0..84f6a4aa 100644 --- a/server/api/[server]/oauth.ts +++ b/server/api/[server]/oauth.ts @@ -32,6 +32,6 @@ export default defineEventHandler(async (event) => { }, }) - const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token })}` + const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token, vapid_key: app.vapid_key })}` await sendRedirect(event, url, 302) }) diff --git a/service-worker/sw.ts b/service-worker/sw.ts new file mode 100644 index 00000000..6294655c --- /dev/null +++ b/service-worker/sw.ts @@ -0,0 +1,65 @@ +/// +/// +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 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) diff --git a/service-worker/tsconfig.json b/service-worker/tsconfig.json new file mode 100644 index 00000000..78e5707a --- /dev/null +++ b/service-worker/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "WebWorker"], + "types": ["vite/client", "service-worker"] + }, + "include": ["./"], + "extends": "../tsconfig.json" +} diff --git a/service-worker/types.ts b/service-worker/types.ts new file mode 100644 index 00000000..7c6aec3a --- /dev/null +++ b/service-worker/types.ts @@ -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 +} diff --git a/service-worker/web-push-notifications.ts b/service-worker/web-push-notifications.ts new file mode 100644 index 00000000..18e66d9a --- /dev/null +++ b/service-worker/web-push-notifications.ts @@ -0,0 +1,85 @@ +/// +/// +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))) +} diff --git a/shims.d.ts b/shims.d.ts index d791bcd2..e82d75f0 100644 --- a/shims.d.ts +++ b/shims.d.ts @@ -1 +1,3 @@ /// +/// +/// diff --git a/types/index.ts b/types/index.ts index c8b2df51..c1b5f852 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,4 +1,4 @@ -import type { Account, AccountCredentials, Attachment, CreateStatusParams, Emoji, Instance, MastoClient, Notification, Status } from 'masto' +import type { Account, AccountCredentials, Attachment, CreateStatusParams, Emoji, Instance, MastoClient, Notification, PushSubscription, Status } from 'masto' import type { Ref } from 'vue' import type { Mutable } from './utils' @@ -16,6 +16,8 @@ export interface UserLogin { server: string token?: string account: AccountCredentials + vapidKey?: string + pushSubscription?: PushSubscription } export interface ElkMasto extends MastoClient {