refactor: migrate to nuxt compatibilityVersion: 4 (#3298)
This commit is contained in:
parent
46e4433e1c
commit
a3fbc056a9
342 changed files with 1200 additions and 2932 deletions
135
app/composables/push-notifications/createPushSubscription.ts
Normal file
135
app/composables/push-notifications/createPushSubscription.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
import type { mastodon } from 'masto'
|
||||
import type {
|
||||
CreatePushNotification,
|
||||
PushManagerSubscriptionInfo,
|
||||
RequiredUserLogin,
|
||||
} from '~/composables/push-notifications/types'
|
||||
import { PushSubscriptionError } from '~/composables/push-notifications/types'
|
||||
|
||||
export async function createPushSubscription(
|
||||
user: RequiredUserLogin,
|
||||
notificationData: CreatePushNotification,
|
||||
policy: mastodon.v1.WebPushSubscriptionPolicy = 'all',
|
||||
force = false,
|
||||
): Promise<mastodon.v1.WebPushSubscription | undefined> {
|
||||
const { server: serverEndpoint, vapidKey } = user
|
||||
|
||||
return await getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(({ registration, subscription }): Promise<mastodon.v1.WebPushSubscription | undefined> => {
|
||||
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 && (!force && user.pushSubscription)) {
|
||||
return Promise.resolve(user.pushSubscription)
|
||||
}
|
||||
else if (user.pushSubscription) {
|
||||
// if we have a subscription, but it is not valid or forcing renew, we need to remove it
|
||||
// we need to prevent removing push notification data
|
||||
return unsubscribeFromBackend(false, false)
|
||||
.catch(removePushNotificationDataOnError)
|
||||
.then(() => subscribe(registration, vapidKey))
|
||||
.then(subscription => sendSubscriptionToBackend(subscription, notificationData, policy))
|
||||
}
|
||||
}
|
||||
|
||||
return subscribe(registration, vapidKey).then(
|
||||
subscription => sendSubscriptionToBackend(subscription, notificationData, policy),
|
||||
)
|
||||
})
|
||||
.catch((error) => {
|
||||
let useError: PushSubscriptionError | Error = error
|
||||
if (error.code === 11 && error.name === 'InvalidStateError')
|
||||
useError = new PushSubscriptionError('too_many_registrations', 'Too many registrations')
|
||||
else if (error.code === 20 && error.name === 'AbortError')
|
||||
useError = new PushSubscriptionError('vapid_not_supported', 'Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.')
|
||||
else if (error.code === 5 && error.name === 'InvalidCharacterError')
|
||||
useError = new PushSubscriptionError('invalid_vapid_key', `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)
|
||||
})
|
||||
.finally(() => {
|
||||
return Promise.reject(useError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 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<PushManagerSubscriptionInfo> {
|
||||
const subscription = await registration.pushManager.getSubscription()
|
||||
return { registration, subscription }
|
||||
}
|
||||
|
||||
async function subscribe(
|
||||
registration: ServiceWorkerRegistration,
|
||||
applicationServerKey: string,
|
||||
): Promise<PushSubscription> {
|
||||
return await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
|
||||
})
|
||||
}
|
||||
|
||||
async function unsubscribeFromBackend(fromSWPushManager: boolean, removePushNotification = true) {
|
||||
const cu = currentUser.value
|
||||
if (cu) {
|
||||
await removePushNotifications(cu)
|
||||
if (removePushNotification)
|
||||
await removePushNotificationData(cu, fromSWPushManager)
|
||||
}
|
||||
}
|
||||
|
||||
async function removePushNotificationDataOnError(e: Error) {
|
||||
const cu = currentUser.value
|
||||
if (cu)
|
||||
await removePushNotificationData(cu, true)
|
||||
|
||||
throw e
|
||||
}
|
||||
|
||||
async function sendSubscriptionToBackend(
|
||||
subscription: PushSubscription,
|
||||
data: CreatePushNotification,
|
||||
policy: mastodon.v1.WebPushSubscriptionPolicy,
|
||||
): Promise<mastodon.v1.WebPushSubscription> {
|
||||
const { endpoint, keys } = subscription.toJSON()
|
||||
return await useMastoClient().v1.push.subscription.create({
|
||||
policy,
|
||||
subscription: {
|
||||
endpoint: endpoint!,
|
||||
keys: {
|
||||
p256dh: keys!.p256dh!,
|
||||
auth: keys!.auth!,
|
||||
},
|
||||
},
|
||||
data,
|
||||
})
|
||||
}
|
36
app/composables/push-notifications/types.ts
Normal file
36
app/composables/push-notifications/types.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { UserLogin } from '#shared/types'
|
||||
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
export type SubscriptionResult = 'subscribed' | 'notification-denied' | 'not-supported' | 'invalid-vapid-key' | 'no-user'
|
||||
export interface PushManagerSubscriptionInfo {
|
||||
registration: ServiceWorkerRegistration
|
||||
subscription: PushSubscription | null
|
||||
}
|
||||
|
||||
export interface RequiredUserLogin extends Required<Omit<UserLogin, 'account' | 'pushSubscription'>> {
|
||||
pushSubscription?: mastodon.v1.WebPushSubscription
|
||||
}
|
||||
|
||||
export interface CreatePushNotification {
|
||||
alerts?: Partial<mastodon.v1.WebPushSubscriptionAlerts> | null
|
||||
policy?: mastodon.v1.WebPushSubscriptionPolicy
|
||||
}
|
||||
|
||||
export type PushNotificationRequest = Record<string, boolean>
|
||||
export type PushNotificationPolicy = Record<string, mastodon.v1.WebPushSubscriptionPolicy>
|
||||
|
||||
export interface CustomEmojisInfo {
|
||||
lastUpdate: number
|
||||
emojis: mastodon.v1.CustomEmoji[]
|
||||
}
|
||||
|
||||
export type PushSubscriptionErrorCode = 'too_many_registrations' | 'vapid_not_supported' | 'invalid_vapid_key'
|
||||
|
||||
export class PushSubscriptionError extends Error {
|
||||
code: PushSubscriptionErrorCode
|
||||
constructor(code: PushSubscriptionErrorCode, message?: string) {
|
||||
super(message)
|
||||
this.code = code
|
||||
}
|
||||
}
|
215
app/composables/push-notifications/usePushManager.ts
Normal file
215
app/composables/push-notifications/usePushManager.ts
Normal file
|
@ -0,0 +1,215 @@
|
|||
import type { mastodon } from 'masto'
|
||||
|
||||
import type {
|
||||
CreatePushNotification,
|
||||
PushNotificationPolicy,
|
||||
PushNotificationRequest,
|
||||
SubscriptionResult,
|
||||
} from '~/composables/push-notifications/types'
|
||||
import { STORAGE_KEY_NOTIFICATION, STORAGE_KEY_NOTIFICATION_POLICY } from '~/constants'
|
||||
|
||||
const supportsPushNotifications = typeof window !== 'undefined'
|
||||
&& 'serviceWorker' in navigator
|
||||
&& 'PushManager' in window
|
||||
&& 'getKey' in PushSubscription.prototype
|
||||
|
||||
export function usePushManager() {
|
||||
const { client } = useMasto()
|
||||
const isSubscribed = ref(false)
|
||||
const notificationPermission = ref<PermissionState | undefined>(
|
||||
Notification.permission === 'denied'
|
||||
? 'denied'
|
||||
: Notification.permission === 'granted'
|
||||
? 'granted'
|
||||
: Notification.permission === 'default'
|
||||
? 'prompt'
|
||||
: undefined,
|
||||
)
|
||||
const isSupported = computed(() => supportsPushNotifications)
|
||||
const hiddenNotification = useLocalStorage<PushNotificationRequest>(STORAGE_KEY_NOTIFICATION, {})
|
||||
const configuredPolicy = useLocalStorage<PushNotificationPolicy>(STORAGE_KEY_NOTIFICATION_POLICY, {})
|
||||
const pushNotificationData = ref(createRawSettings(
|
||||
currentUser.value?.pushSubscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
))
|
||||
const oldPushNotificationData = ref(createRawSettings(
|
||||
currentUser.value?.pushSubscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
))
|
||||
const saveEnabled = computed(() => {
|
||||
const current = pushNotificationData.value
|
||||
const previous = oldPushNotificationData.value
|
||||
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 = createRawSettings(
|
||||
subscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
)
|
||||
oldPushNotificationData.value = createRawSettings(
|
||||
subscription,
|
||||
configuredPolicy.value[currentUser.value?.account?.acct ?? ''],
|
||||
)
|
||||
}, { immediate: true, flush: 'post' })
|
||||
|
||||
const subscribe = async (
|
||||
notificationData?: CreatePushNotification,
|
||||
policy?: mastodon.v1.WebPushSubscriptionPolicy,
|
||||
force?: boolean,
|
||||
): Promise<SubscriptionResult> => {
|
||||
if (!isSupported.value)
|
||||
return 'not-supported'
|
||||
|
||||
if (!currentUser.value)
|
||||
return 'no-user'
|
||||
|
||||
const { pushSubscription, server, token, vapidKey, account: { acct } } = currentUser.value
|
||||
|
||||
if (!token || !server || !vapidKey)
|
||||
return 'invalid-vapid-key'
|
||||
|
||||
// always request permission, browsers should remember user decision
|
||||
const permission = await Promise.resolve(Notification.requestPermission()).then((p) => {
|
||||
return p === 'default' ? 'prompt' : p
|
||||
})
|
||||
|
||||
if (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',
|
||||
force,
|
||||
)
|
||||
await nextTick()
|
||||
notificationPermission.value = permission
|
||||
hiddenNotification.value[acct] = true
|
||||
|
||||
return 'subscribed'
|
||||
}
|
||||
|
||||
const unsubscribe = async () => {
|
||||
if (!isSupported.value || !isSubscribed.value || !currentUser.value)
|
||||
return false
|
||||
|
||||
await removePushNotifications(currentUser.value)
|
||||
await removePushNotificationData(currentUser.value)
|
||||
}
|
||||
|
||||
const saveSettings = async (policy?: mastodon.v1.WebPushSubscriptionPolicy) => {
|
||||
if (policy)
|
||||
pushNotificationData.value.policy = policy
|
||||
|
||||
const current = pushNotificationData.value
|
||||
oldPushNotificationData.value = {
|
||||
favourite: current.favourite,
|
||||
reblog: current.reblog,
|
||||
mention: current.mention,
|
||||
follow: current.follow,
|
||||
poll: current.poll,
|
||||
policy: current.policy,
|
||||
}
|
||||
|
||||
if (policy)
|
||||
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = policy
|
||||
else
|
||||
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = pushNotificationData.value.policy
|
||||
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const undoChanges = () => {
|
||||
const previous = oldPushNotificationData.value
|
||||
pushNotificationData.value = {
|
||||
favourite: previous.favourite,
|
||||
reblog: previous.reblog,
|
||||
mention: previous.mention,
|
||||
follow: previous.follow,
|
||||
poll: previous.poll,
|
||||
policy: previous.policy,
|
||||
}
|
||||
configuredPolicy.value[currentUser.value!.account.acct ?? ''] = previous.policy
|
||||
}
|
||||
|
||||
const updateSubscription = async () => {
|
||||
if (currentUser.value) {
|
||||
const previous = oldPushNotificationData.value
|
||||
// const previous = history.value[0].snapshot
|
||||
const data = {
|
||||
alerts: {
|
||||
follow: pushNotificationData.value.follow,
|
||||
favourite: pushNotificationData.value.favourite,
|
||||
reblog: pushNotificationData.value.reblog,
|
||||
mention: pushNotificationData.value.mention,
|
||||
poll: pushNotificationData.value.poll,
|
||||
},
|
||||
}
|
||||
|
||||
const policy = pushNotificationData.value.policy
|
||||
|
||||
const policyChanged = previous.policy !== policy
|
||||
|
||||
// to change policy we need to resubscribe
|
||||
if (policyChanged)
|
||||
await subscribe(data, policy, true)
|
||||
else
|
||||
currentUser.value.pushSubscription = await client.value.v1.push.subscription.update({ data })
|
||||
|
||||
if (policyChanged)
|
||||
await nextTick()
|
||||
|
||||
// force change policy when changed: watch is resetting it on push subscription update
|
||||
await saveSettings(policyChanged ? policy : undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pushNotificationData,
|
||||
saveEnabled,
|
||||
undoChanges,
|
||||
hiddenNotification,
|
||||
isSupported,
|
||||
isSubscribed,
|
||||
notificationPermission,
|
||||
updateSubscription,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
}
|
||||
}
|
||||
|
||||
function createRawSettings(
|
||||
pushSubscription?: mastodon.v1.WebPushSubscription,
|
||||
subscriptionPolicy?: mastodon.v1.WebPushSubscriptionPolicy,
|
||||
) {
|
||||
return {
|
||||
follow: pushSubscription?.alerts.follow ?? true,
|
||||
favourite: pushSubscription?.alerts.favourite ?? true,
|
||||
reblog: pushSubscription?.alerts.reblog ?? true,
|
||||
mention: pushSubscription?.alerts.mention ?? true,
|
||||
poll: pushSubscription?.alerts.poll ?? true,
|
||||
policy: subscriptionPolicy ?? 'all',
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue