forked from Mirrors/elk
feat(pwa): allow access elk users from service worker (#662)
Co-authored-by: patak <matias.capeletto@gmail.com>
This commit is contained in:
parent
ca93f1a813
commit
496da96072
7 changed files with 413 additions and 39 deletions
|
@ -35,7 +35,8 @@ export const usePushManager = () => {
|
||||||
poll: currentUser.value?.pushSubscription?.alerts.poll ?? true,
|
poll: currentUser.value?.pushSubscription?.alerts.poll ?? true,
|
||||||
policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all',
|
policy: configuredPolicy.value[currentUser.value?.account?.acct ?? ''] ?? 'all',
|
||||||
})
|
})
|
||||||
const { history, commit, clear } = useManualRefHistory(pushNotificationData, { clone: true })
|
// don't clone, we're using indexeddb
|
||||||
|
const { history, commit, clear } = useManualRefHistory(pushNotificationData)
|
||||||
const saveEnabled = computed(() => {
|
const saveEnabled = computed(() => {
|
||||||
const current = pushNotificationData.value
|
const current = pushNotificationData.value
|
||||||
const previous = history.value?.[0]?.snapshot
|
const previous = history.value?.[0]?.snapshot
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { login as loginMasto } from 'masto'
|
import { login as loginMasto } from 'masto'
|
||||||
|
import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval'
|
||||||
import type { Account, AccountCredentials, Instance, MastoClient, WsEvents } from 'masto'
|
import type { Account, AccountCredentials, Instance, MastoClient, WsEvents } from 'masto'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
import type { RemovableRef } from '@vueuse/core'
|
||||||
import type { ElkMasto, UserLogin } from '~/types'
|
import type { ElkMasto, UserLogin } from '~/types'
|
||||||
import {
|
import {
|
||||||
DEFAULT_POST_CHARS_LIMIT,
|
DEFAULT_POST_CHARS_LIMIT,
|
||||||
|
@ -14,7 +16,31 @@ import {
|
||||||
import type { PushNotificationPolicy, PushNotificationRequest } from '~/composables/push-notifications/types'
|
import type { PushNotificationPolicy, PushNotificationRequest } from '~/composables/push-notifications/types'
|
||||||
|
|
||||||
const mock = process.mock
|
const mock = process.mock
|
||||||
const users = useLocalStorage<UserLogin[]>(STORAGE_KEY_USERS, mock ? [mock.user] : [], { deep: true })
|
|
||||||
|
const initializeUsers = (): Ref<UserLogin[]> | RemovableRef<UserLogin[]> => {
|
||||||
|
let defaultUsers = mock ? [mock.user] : []
|
||||||
|
|
||||||
|
// Backward compatibility with localStorage
|
||||||
|
let removeUsersOnLocalStorage = false
|
||||||
|
if (globalThis?.localStorage) {
|
||||||
|
const usersOnLocalStorageString = globalThis.localStorage.getItem(STORAGE_KEY_USERS)
|
||||||
|
if (usersOnLocalStorageString) {
|
||||||
|
defaultUsers = JSON.parse(usersOnLocalStorageString)
|
||||||
|
removeUsersOnLocalStorage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = process.server
|
||||||
|
? ref<UserLogin[]>(defaultUsers)
|
||||||
|
: useIDBKeyval<UserLogin[]>(STORAGE_KEY_USERS, defaultUsers, { deep: true })
|
||||||
|
|
||||||
|
if (removeUsersOnLocalStorage)
|
||||||
|
globalThis.localStorage.removeItem(STORAGE_KEY_USERS)
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = initializeUsers()
|
||||||
const instances = useLocalStorage<Record<string, Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
|
const instances = useLocalStorage<Record<string, Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
|
||||||
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '')
|
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '')
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"focus-trap": "^7.2.0",
|
"focus-trap": "^7.2.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
|
"idb-keyval": "^6.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lru-cache": "^7.14.1",
|
"lru-cache": "^7.14.1",
|
||||||
"masto": "^4.11.1",
|
"masto": "^4.11.1",
|
||||||
|
|
|
@ -48,6 +48,7 @@ specifiers:
|
||||||
form-data: ^4.0.0
|
form-data: ^4.0.0
|
||||||
fs-extra: ^11.1.0
|
fs-extra: ^11.1.0
|
||||||
fuse.js: ^6.6.2
|
fuse.js: ^6.6.2
|
||||||
|
idb-keyval: ^6.2.0
|
||||||
js-yaml: ^4.1.0
|
js-yaml: ^4.1.0
|
||||||
jsdom: ^20.0.3
|
jsdom: ^20.0.3
|
||||||
lint-staged: ^13.1.0
|
lint-staged: ^13.1.0
|
||||||
|
@ -93,13 +94,14 @@ dependencies:
|
||||||
'@tiptap/suggestion': 2.0.0-beta.204
|
'@tiptap/suggestion': 2.0.0-beta.204
|
||||||
'@tiptap/vue-3': 2.0.0-beta.204
|
'@tiptap/vue-3': 2.0.0-beta.204
|
||||||
'@vueuse/core': 9.9.0
|
'@vueuse/core': 9.9.0
|
||||||
'@vueuse/integrations': 9.9.0_7zhv6s73i5wtygx2wkeytrmn7q
|
'@vueuse/integrations': 9.9.0_ha7ivgav6uqpoo2b5thfugqwjq
|
||||||
blurhash: 2.0.4
|
blurhash: 2.0.4
|
||||||
browser-fs-access: 0.31.1
|
browser-fs-access: 0.31.1
|
||||||
floating-vue: 2.0.0-beta.20
|
floating-vue: 2.0.0-beta.20
|
||||||
focus-trap: 7.2.0
|
focus-trap: 7.2.0
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
fuse.js: 6.6.2
|
fuse.js: 6.6.2
|
||||||
|
idb-keyval: 6.2.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
lru-cache: 7.14.1
|
lru-cache: 7.14.1
|
||||||
masto: 4.11.1
|
masto: 4.11.1
|
||||||
|
@ -157,7 +159,7 @@ devDependencies:
|
||||||
typescript: 4.9.4
|
typescript: 4.9.4
|
||||||
unplugin-auto-import: 0.12.1_@vueuse+core@9.9.0
|
unplugin-auto-import: 0.12.1_@vueuse+core@9.9.0
|
||||||
vite-plugin-inspect: 0.7.11
|
vite-plugin-inspect: 0.7.11
|
||||||
vite-plugin-pwa: 0.13.3_workbox-window@6.5.4
|
vite-plugin-pwa: 0.13.3
|
||||||
vitest: 0.26.2_jsdom@20.0.3
|
vitest: 0.26.2_jsdom@20.0.3
|
||||||
vue-tsc: 1.0.16_typescript@4.9.4
|
vue-tsc: 1.0.16_typescript@4.9.4
|
||||||
workbox-window: 6.5.4
|
workbox-window: 6.5.4
|
||||||
|
@ -1621,8 +1623,8 @@ packages:
|
||||||
vue-i18n:
|
vue-i18n:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/message-compiler': 9.3.0-beta.11
|
'@intlify/message-compiler': 9.3.0-beta.12
|
||||||
'@intlify/shared': 9.3.0-beta.11
|
'@intlify/shared': 9.3.0-beta.12
|
||||||
jsonc-eslint-parser: 1.4.1
|
jsonc-eslint-parser: 1.4.1
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
vue-i18n: 9.3.0-beta.10
|
vue-i18n: 9.3.0-beta.10
|
||||||
|
@ -1654,8 +1656,8 @@ packages:
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@intlify/message-compiler/9.3.0-beta.11:
|
/@intlify/message-compiler/9.3.0-beta.12:
|
||||||
resolution: {integrity: sha512-gGGfBGzM7JBXp1Q9gbDAy5jELz9ho3ILqnpxp2yp64+gkqohrqc2YXIvCdwZoc6AtKIh/Zmv4sWVqxkvMsBWtQ==}
|
resolution: {integrity: sha512-A8/s7pb3v8nf6HG77qFPJntxgQKI9GXxGnkn7aO+b03/X/GkF/4WceDSAIk3i+yLeIgszeBn9GZ23tSg4sTEHA==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/shared': 9.3.0-beta.11
|
'@intlify/shared': 9.3.0-beta.11
|
||||||
|
@ -1672,6 +1674,11 @@ packages:
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@intlify/shared/9.3.0-beta.12:
|
||||||
|
resolution: {integrity: sha512-WsmaS54sA8xuwezPKpa/OMoaX1v2VF2fCgAmYS6prDr2ir0CkUFWPm9A8ilmxzv4nkS61/v8+vf4lGGkn5uBdA==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@intlify/unplugin-vue-i18n/0.8.0_vue-i18n@9.3.0-beta.10:
|
/@intlify/unplugin-vue-i18n/0.8.0_vue-i18n@9.3.0-beta.10:
|
||||||
resolution: {integrity: sha512-bqMDYrbmV0oMLGHTdYMUXfcEsy2rPwQnGrQAg4gvw5FimvJfTQt3RliLVayT5ldOfeT2g0IUc/0t7LPeGrFUag==}
|
resolution: {integrity: sha512-bqMDYrbmV0oMLGHTdYMUXfcEsy2rPwQnGrQAg4gvw5FimvJfTQt3RliLVayT5ldOfeT2g0IUc/0t7LPeGrFUag==}
|
||||||
engines: {node: '>= 14.16'}
|
engines: {node: '>= 14.16'}
|
||||||
|
@ -1688,7 +1695,7 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/bundle-utils': 3.4.0_vue-i18n@9.3.0-beta.10
|
'@intlify/bundle-utils': 3.4.0_vue-i18n@9.3.0-beta.10
|
||||||
'@intlify/shared': 9.3.0-beta.11
|
'@intlify/shared': 9.3.0-beta.12
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
'@vue/compiler-sfc': 3.2.45
|
'@vue/compiler-sfc': 3.2.45
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
|
@ -3514,7 +3521,7 @@ packages:
|
||||||
vue: 3.2.45
|
vue: 3.2.45
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@vueuse/integrations/9.9.0_7zhv6s73i5wtygx2wkeytrmn7q:
|
/@vueuse/integrations/9.9.0_ha7ivgav6uqpoo2b5thfugqwjq:
|
||||||
resolution: {integrity: sha512-/wr3jrMlzbPNd38dO85NOT4j7vga9+eQewEZFXHJAFEvKnRxBy/Ytp1pt4Sz8dVOLLYMBHfSaVAra91ftfIh0w==}
|
resolution: {integrity: sha512-/wr3jrMlzbPNd38dO85NOT4j7vga9+eQewEZFXHJAFEvKnRxBy/Ytp1pt4Sz8dVOLLYMBHfSaVAra91ftfIh0w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
async-validator: '*'
|
async-validator: '*'
|
||||||
|
@ -3556,6 +3563,7 @@ packages:
|
||||||
'@vueuse/shared': 9.9.0
|
'@vueuse/shared': 9.9.0
|
||||||
focus-trap: 7.2.0
|
focus-trap: 7.2.0
|
||||||
fuse.js: 6.6.2
|
fuse.js: 6.6.2
|
||||||
|
idb-keyval: 6.2.0
|
||||||
vue-demi: 0.13.11
|
vue-demi: 0.13.11
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
|
@ -6186,6 +6194,12 @@ packages:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/idb-keyval/6.2.0:
|
||||||
|
resolution: {integrity: sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==}
|
||||||
|
dependencies:
|
||||||
|
safari-14-idb-fix: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/idb/7.1.1:
|
/idb/7.1.1:
|
||||||
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -8590,6 +8604,10 @@ packages:
|
||||||
tslib: 2.4.1
|
tslib: 2.4.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/safari-14-idb-fix/3.0.0:
|
||||||
|
resolution: {integrity: sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/safe-buffer/5.1.2:
|
/safe-buffer/5.1.2:
|
||||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
|
||||||
|
@ -9817,11 +9835,10 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite-plugin-pwa/0.13.3_workbox-window@6.5.4:
|
/vite-plugin-pwa/0.13.3:
|
||||||
resolution: {integrity: sha512-cjWXpZ7slAY14OKz7M8XdgTIi9wjf6OD6NkhiMAc+ogxnbUrecUwLdRtfGPCPsN2ftut5gaN1jTghb11p6IQAA==}
|
resolution: {integrity: sha512-cjWXpZ7slAY14OKz7M8XdgTIi9wjf6OD6NkhiMAc+ogxnbUrecUwLdRtfGPCPsN2ftut5gaN1jTghb11p6IQAA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^3.1.0
|
vite: ^3.1.0
|
||||||
workbox-window: ^6.5.4
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/plugin-replace': 4.0.0_rollup@2.79.1
|
'@rollup/plugin-replace': 4.0.0_rollup@2.79.1
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
|
@ -10069,7 +10086,7 @@ packages:
|
||||||
vue-router:
|
vue-router:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/shared': 9.3.0-beta.11
|
'@intlify/shared': 9.3.0-beta.12
|
||||||
'@intlify/vue-i18n-bridge': 0.8.0_vue-i18n@9.3.0-beta.10
|
'@intlify/vue-i18n-bridge': 0.8.0_vue-i18n@9.3.0-beta.10
|
||||||
'@intlify/vue-router-bridge': 0.8.0
|
'@intlify/vue-router-bridge': 0.8.0
|
||||||
ufo: 1.0.1
|
ufo: 1.0.1
|
||||||
|
|
106
service-worker/notification.ts
Normal file
106
service-worker/notification.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { get } from 'idb-keyval'
|
||||||
|
import type { MastoNotification, NotificationInfo, PushPayload, UserLogin } from './types'
|
||||||
|
|
||||||
|
export const findNotification = async (
|
||||||
|
{ access_token, notification_id/* , notification_type */ }: PushPayload,
|
||||||
|
): Promise<NotificationInfo | undefined> => {
|
||||||
|
const users = await get<UserLogin[]>('elk-users')
|
||||||
|
if (!users)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
const filteredUsers = users.filter(user => user.token === access_token)
|
||||||
|
if (!filteredUsers || filteredUsers.length === 0)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
for (const user of filteredUsers) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://${user.server}/api/v1/notifications/${notification_id}`, {
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${user.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// assume it is ok to return the first notification: backend should return 404 if not found
|
||||||
|
if (response && response.ok) {
|
||||||
|
const notification: MastoNotification = await response.json()
|
||||||
|
return { user, notification }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// just ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNotificationOptions(
|
||||||
|
pushPayload: PushPayload,
|
||||||
|
notificationInfo?: NotificationInfo,
|
||||||
|
): NotificationOptions {
|
||||||
|
const {
|
||||||
|
access_token,
|
||||||
|
body,
|
||||||
|
icon,
|
||||||
|
notification_id,
|
||||||
|
notification_type,
|
||||||
|
preferred_locale,
|
||||||
|
} = pushPayload
|
||||||
|
|
||||||
|
const url = notification_type === 'mention' ? 'notifications/mention' : 'notifications'
|
||||||
|
|
||||||
|
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().getTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationInfo) {
|
||||||
|
const { user, notification } = notificationInfo
|
||||||
|
notificationOptions.tag = notification.id
|
||||||
|
/*
|
||||||
|
if (notification.account.avatar_static)
|
||||||
|
notificationOptions.icon = notification.account.avatar_static
|
||||||
|
*/
|
||||||
|
if (notification.created_at)
|
||||||
|
notificationOptions.timestamp = new Date(notification.created_at).getTime()
|
||||||
|
|
||||||
|
/* TODO: add spolier when actions available, checking also notification type
|
||||||
|
if (notification.status && (notification.status.spoilerText || notification.status.sensitive)) {
|
||||||
|
if (notification.status.spoilerText)
|
||||||
|
notificationOptions.body = notification.status.spoilerText
|
||||||
|
|
||||||
|
notificationOptions.image = undefined
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (notification.status) {
|
||||||
|
// notificationOptions.body = htmlToPlainText(notification.status.content)
|
||||||
|
if (notification.status.media_attachments && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url)
|
||||||
|
notificationOptions.image = notification.status.media_attachments[0].preview_url
|
||||||
|
|
||||||
|
if (notification.type === 'favourite' || notification.type === 'reblog' || notification.type === 'mention')
|
||||||
|
notificationOptions.data.url = `${user.server}/@${user.account.username}/${notification.status.id}`
|
||||||
|
}
|
||||||
|
else if (notification.type === 'follow') {
|
||||||
|
notificationOptions.data.url = `${user.server}/@${notification.account.acct}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notificationOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
function htmlToPlainText(html: string) {
|
||||||
|
return decodeURIComponent(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, ''))
|
||||||
|
}
|
||||||
|
*/
|
|
@ -1,9 +1,248 @@
|
||||||
|
// masto types and notification types differs
|
||||||
|
// Any type used from masto api retrieving notification from push notification id is no camel case, it is snake case
|
||||||
|
// I just copy/paste any entry from masto api and convert it to snake case, reusing types not including camel case props
|
||||||
|
import type {
|
||||||
|
AccountCredentials,
|
||||||
|
AttachmentMeta,
|
||||||
|
AttachmentType,
|
||||||
|
Card,
|
||||||
|
Mention,
|
||||||
|
StatusVisibility,
|
||||||
|
Tag,
|
||||||
|
} from 'masto'
|
||||||
|
|
||||||
|
export type NotificationType = 'mention' | 'status' | 'reblog' | 'follow' | 'follow_request' | 'favourite' | 'poll' | 'update' | 'admin.sign_up' | 'admin.report'
|
||||||
|
|
||||||
export interface PushPayload {
|
export interface PushPayload {
|
||||||
access_token: string
|
access_token: string
|
||||||
notification_id: string
|
notification_id: string
|
||||||
notification_type: 'follow' | 'favourite' | 'reblog' | 'mention' | 'poll'
|
notification_type: NotificationType
|
||||||
preferred_locale: string
|
preferred_locale: string
|
||||||
title: string
|
title: string
|
||||||
body: string
|
body: string
|
||||||
icon: string
|
icon: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserLogin {
|
||||||
|
server: string
|
||||||
|
token?: string
|
||||||
|
account: AccountCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationInfo {
|
||||||
|
user: UserLogin
|
||||||
|
notification: MastoNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PollOption {
|
||||||
|
/** The text value of the poll option. String. */
|
||||||
|
title: string
|
||||||
|
/** The number of received votes for this option. Number, or null if results are not published yet. */
|
||||||
|
votes_count?: number
|
||||||
|
/** Custom emoji to be used for rendering poll options. */
|
||||||
|
emojis: Emoji[]
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Represents a poll attached to a status.
|
||||||
|
* @see https://docs.joinmastodon.org/entities/poll/
|
||||||
|
*/
|
||||||
|
interface Poll {
|
||||||
|
/** The ID of the poll in the database. */
|
||||||
|
id: string
|
||||||
|
/** When the poll ends. */
|
||||||
|
expires_at?: string | null
|
||||||
|
/** Is the poll currently expired? */
|
||||||
|
expired: boolean
|
||||||
|
/** Does the poll allow multiple-choice answers? */
|
||||||
|
multiple: boolean
|
||||||
|
/** How many votes have been received. */
|
||||||
|
votes_count: number
|
||||||
|
/** How many unique accounts have voted on a multiple-choice poll. */
|
||||||
|
voters_count?: number | null
|
||||||
|
/** When called with a user token, has the authorized user voted? */
|
||||||
|
voted?: boolean
|
||||||
|
/**
|
||||||
|
* When called with a user token, which options has the authorized user chosen?
|
||||||
|
* Contains an array of index values for options.
|
||||||
|
*/
|
||||||
|
own_votes?: number[] | null
|
||||||
|
/** Possible answers for the poll. */
|
||||||
|
options: PollOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Attachment {
|
||||||
|
/** The ID of the attachment in the database. */
|
||||||
|
id: string
|
||||||
|
/** The type of the attachment. */
|
||||||
|
type: AttachmentType
|
||||||
|
/** The location of the original full-size attachment. */
|
||||||
|
url?: string | null
|
||||||
|
/** The location of a scaled-down preview of the attachment. */
|
||||||
|
preview_url: string
|
||||||
|
/** The location of the full-size original attachment on the remote website. */
|
||||||
|
remote_url?: string | null
|
||||||
|
/** Remote version of previewUrl */
|
||||||
|
preview_remote_url?: string | null
|
||||||
|
/** A shorter URL for the attachment. */
|
||||||
|
text_url?: string | null
|
||||||
|
/** Metadata returned by Paperclip. */
|
||||||
|
meta?: AttachmentMeta | null
|
||||||
|
/**
|
||||||
|
* Alternate text that describes what is in the media attachment,
|
||||||
|
* to be used for the visually impaired or when media attachments do not load.
|
||||||
|
*/
|
||||||
|
description?: string | null
|
||||||
|
/**
|
||||||
|
* A hash computed by the BlurHash algorithm,
|
||||||
|
* for generating colorful preview thumbnails when media has not been downloaded yet.
|
||||||
|
*/
|
||||||
|
blurhash?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Emoji {
|
||||||
|
/** The name of the custom emoji. */
|
||||||
|
shortcode: string
|
||||||
|
/** A link to the custom emoji. */
|
||||||
|
url: string
|
||||||
|
/** A link to a static copy of the custom emoji. */
|
||||||
|
static_url: string
|
||||||
|
/** Whether this Emoji should be visible in the picker or unlisted. */
|
||||||
|
visible_in_picker: boolean
|
||||||
|
/** Used for sorting custom emoji in the picker. */
|
||||||
|
category?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
/** ID of the status in the database. */
|
||||||
|
id: string
|
||||||
|
/** URI of the status used for federation. */
|
||||||
|
uri: string
|
||||||
|
/** The date when this status was created. */
|
||||||
|
created_at: string
|
||||||
|
/** Timestamp of when the status was last edited. */
|
||||||
|
edited_at: string | null
|
||||||
|
/** The account that authored this status. */
|
||||||
|
account: MastoAccount
|
||||||
|
/** HTML-encoded status content. */
|
||||||
|
content: string
|
||||||
|
/** Visibility of this status. */
|
||||||
|
visibility: StatusVisibility
|
||||||
|
/** Is this status marked as sensitive content? */
|
||||||
|
sensitive: boolean
|
||||||
|
/** Subject or summary line, below which status content is collapsed until expanded. */
|
||||||
|
spoiler_text: string
|
||||||
|
/** Media that is attached to this status. */
|
||||||
|
media_attachments: Attachment[]
|
||||||
|
/** The application used to post this status. */
|
||||||
|
// application: Application
|
||||||
|
/** Mentions of users within the status content. */
|
||||||
|
mentions: Mention[]
|
||||||
|
/** Hashtags used within the status content. */
|
||||||
|
tags: Tag[]
|
||||||
|
/** Custom emoji to be used when rendering status content. */
|
||||||
|
emojis: Emoji[]
|
||||||
|
/** How many boosts this status has received. */
|
||||||
|
reblogs_count: number
|
||||||
|
/** How many favourites this status has received. */
|
||||||
|
favourites_count: number
|
||||||
|
/** How many replies this status has received. */
|
||||||
|
replies_count: number
|
||||||
|
/** A link to the status's HTML representation. */
|
||||||
|
url?: string | null
|
||||||
|
/** ID of the status being replied. */
|
||||||
|
in_reply_to_id?: string | null
|
||||||
|
/** ID of the account being replied to. */
|
||||||
|
in_reply_to_account_id?: string | null
|
||||||
|
/** The status being reblogged. */
|
||||||
|
reblog?: Status | null
|
||||||
|
/** The poll attached to the status. */
|
||||||
|
poll?: Poll | null
|
||||||
|
/** Preview card for links included within status content. */
|
||||||
|
card?: Card | null
|
||||||
|
/** Primary language of this status. */
|
||||||
|
language?: string | null
|
||||||
|
/**
|
||||||
|
* Plain-text source of a status. Returned instead of `content` when status is deleted,
|
||||||
|
* so the user may redraft from the source text without the client having
|
||||||
|
* to reverse-engineer the original text from the HTML content.
|
||||||
|
*/
|
||||||
|
text?: string | null
|
||||||
|
/** Have you favourited this status? */
|
||||||
|
favourited?: boolean | null
|
||||||
|
/** Have you boosted this status? */
|
||||||
|
reblogged?: boolean | null
|
||||||
|
/** Have you muted notifications for this status's conversation? */
|
||||||
|
muted?: boolean | null
|
||||||
|
/** Have you bookmarked this status? */
|
||||||
|
bookmarked?: boolean | null
|
||||||
|
/** Have you pinned this status? Only appears if the status is pin-able. */
|
||||||
|
pinned?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Field {
|
||||||
|
/** The key of a given field's key-value pair. */
|
||||||
|
name: string
|
||||||
|
/** The value associated with the `name` key. */
|
||||||
|
value: string
|
||||||
|
/** Timestamp of when the server verified a URL value for a rel="me” link. */
|
||||||
|
verified_at?: string | null
|
||||||
|
}
|
||||||
|
export interface MastoAccount {
|
||||||
|
/** The account id */
|
||||||
|
id: string
|
||||||
|
/** The username of the account, not including domain */
|
||||||
|
username: string
|
||||||
|
/** The WebFinger account URI. Equal to `username` for local users, or `username@domain` for remote users. */
|
||||||
|
acct: string
|
||||||
|
/** The location of the user's profile page. */
|
||||||
|
url: string
|
||||||
|
/** The profile's display name. */
|
||||||
|
display_name: string
|
||||||
|
/** The profile's bio / description. */
|
||||||
|
note: string
|
||||||
|
/** An image icon that is shown next to statuses and in the profile. */
|
||||||
|
avatar: string
|
||||||
|
/** A static version of the `avatar`. Equal to avatar if its value is a static image; different if `avatar` is an animated GIF. */
|
||||||
|
avatar_static: string
|
||||||
|
/** An image banner that is shown above the profile and in profile cards. */
|
||||||
|
header: string
|
||||||
|
/** A static version of the header. Equal to `header` if its value is a static image; different if `header` is an animated GIF. */
|
||||||
|
header_static: string
|
||||||
|
/** Whether the account manually approves follow requests. */
|
||||||
|
locked: boolean
|
||||||
|
/** Custom emoji entities to be used when rendering the profile. If none, an empty array will be returned. */
|
||||||
|
emojis: Emoji[]
|
||||||
|
/** Whether the account has opted into discovery features such as the profile directory. */
|
||||||
|
discoverable: boolean
|
||||||
|
/** When the account was created. */
|
||||||
|
created_at: string
|
||||||
|
/** How many statuses are attached to this account. */
|
||||||
|
statuses_count: number
|
||||||
|
/** The reported followers of this profile. */
|
||||||
|
followers_count: number
|
||||||
|
/** The reported follows of this profile. */
|
||||||
|
following_count: number
|
||||||
|
/** Time of the last status posted */
|
||||||
|
last_status_at: string
|
||||||
|
/** Indicates that the profile is currently inactive and that its user has moved to a new account. */
|
||||||
|
moved?: boolean | null
|
||||||
|
/** An extra entity returned when an account is suspended. **/
|
||||||
|
suspended?: boolean | null
|
||||||
|
/** Additional metadata attached to a profile as name-value pairs. */
|
||||||
|
fields?: Field[] | null
|
||||||
|
/** Boolean to indicate that the account performs automated actions */
|
||||||
|
bot?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MastoNotification {
|
||||||
|
/** The id of the notification in the database. */
|
||||||
|
id: string
|
||||||
|
/** The type of event that resulted in the notification. */
|
||||||
|
type: NotificationType
|
||||||
|
/** The timestamp of the notification. */
|
||||||
|
created_at: string
|
||||||
|
/** The account that performed the action that generated the notification. */
|
||||||
|
account: MastoAccount
|
||||||
|
/** Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls. */
|
||||||
|
status?: Status | null
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/// <reference lib="WebWorker" />
|
/// <reference lib="WebWorker" />
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
import { createNotificationOptions, findNotification } from './notification'
|
||||||
import type { PushPayload } from '~/service-worker/types'
|
import type { PushPayload } from '~/service-worker/types'
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope
|
declare const self: ServiceWorkerGlobalScope
|
||||||
|
@ -10,32 +11,15 @@ export const onPush = (event: PushEvent) => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|
||||||
const options: PushPayload = event.data!.json()
|
const options: PushPayload = event.data!.json()
|
||||||
const {
|
|
||||||
access_token,
|
|
||||||
body,
|
|
||||||
icon,
|
|
||||||
notification_id,
|
|
||||||
notification_type,
|
|
||||||
preferred_locale,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const url = notification_type === 'mention' ? 'notifications/mention' : 'notifications'
|
return findNotification(options)
|
||||||
|
.catch((e) => {
|
||||||
const notificationOptions: NotificationOptions = {
|
console.error('unhandled error finding notification', e)
|
||||||
badge: '/pwa-192x192.png',
|
return Promise.resolve(undefined)
|
||||||
body,
|
})
|
||||||
data: {
|
.then((notificationInfo) => {
|
||||||
access_token,
|
return self.registration.showNotification(options.title, createNotificationOptions(options, notificationInfo))
|
||||||
preferred_locale,
|
})
|
||||||
url: `/${url}`,
|
|
||||||
},
|
|
||||||
dir: 'auto',
|
|
||||||
icon,
|
|
||||||
lang: preferred_locale,
|
|
||||||
tag: notification_id,
|
|
||||||
timestamp: new Date().getTime(),
|
|
||||||
}
|
|
||||||
return self.registration.showNotification(options.title, notificationOptions)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
event.waitUntil(promise)
|
event.waitUntil(promise)
|
||||||
|
|
Loading…
Reference in a new issue