c65aad6639
- TipTap 2 → 3: Extension from @tiptap/core, Placeholder from @tiptap/extensions, TaskList/TaskItem from @tiptap/extension-list, link: false in StarterKit (now bundles Link), @tiptap/core added - marked 15 → 17: heading renderer updated to tokens/parseInline API - Pinia 2 → 3, Vue Router 4 → 5 (no code changes required) - Vite 6 → 7, @vitejs/plugin-vue 5 → 6, vue-tsc 2 → 3 - TypeScript 5.6 → 5.9: fixed Uint8Array<ArrayBuffer> strictness in push.ts, removed unused bodyEl ref in NoteViewerView.vue - .npmrc: legacy-peer-deps=true for TipTap v3 peer dep resolution TipTap 3 new capabilities now available: static renderer (createStaticRenderer), MarkViews, @tiptap/extensions package. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
|
|
function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer> {
|
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
|
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
|
const rawData = window.atob(base64)
|
|
const buf = new ArrayBuffer(rawData.length)
|
|
const outputArray = new Uint8Array<ArrayBuffer>(buf)
|
|
for (let i = 0; i < rawData.length; ++i) {
|
|
outputArray[i] = rawData.charCodeAt(i)
|
|
}
|
|
return outputArray
|
|
}
|
|
|
|
export const usePushStore = defineStore('push', () => {
|
|
const isSupported = ref(
|
|
'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window
|
|
)
|
|
const permission = ref<NotificationPermission>(
|
|
'Notification' in window ? Notification.permission : 'denied'
|
|
)
|
|
const isSubscribed = ref(false)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
async function checkSubscription(): Promise<void> {
|
|
if (!isSupported.value) return
|
|
try {
|
|
const reg = await navigator.serviceWorker.ready
|
|
const sub = await reg.pushManager.getSubscription()
|
|
isSubscribed.value = !!sub
|
|
} catch {
|
|
isSubscribed.value = false
|
|
}
|
|
}
|
|
|
|
async function subscribe(): Promise<void> {
|
|
if (!isSupported.value) {
|
|
error.value = 'Push notifications not supported in this browser'
|
|
return
|
|
}
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
// Request permission
|
|
permission.value = await Notification.requestPermission()
|
|
if (permission.value !== 'granted') {
|
|
error.value = 'Notification permission denied'
|
|
return
|
|
}
|
|
|
|
// Fetch VAPID public key
|
|
const resp = await fetch('/api/push/vapid-public-key')
|
|
if (!resp.ok) {
|
|
error.value = 'Push notifications not configured on server'
|
|
return
|
|
}
|
|
const { publicKey } = await resp.json()
|
|
|
|
// Register service worker
|
|
const reg = await navigator.serviceWorker.register('/sw.js')
|
|
await navigator.serviceWorker.ready
|
|
|
|
// Subscribe
|
|
const sub = await reg.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
|
})
|
|
|
|
// Save to server
|
|
const saveResp = await fetch('/api/push/subscribe', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(sub.toJSON()),
|
|
})
|
|
if (!saveResp.ok) throw new Error('Failed to save subscription')
|
|
isSubscribed.value = true
|
|
} catch (e: unknown) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to subscribe'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function unsubscribe(): Promise<void> {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const reg = await navigator.serviceWorker.ready
|
|
const sub = await reg.pushManager.getSubscription()
|
|
if (sub) {
|
|
await fetch('/api/push/subscribe', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ endpoint: sub.endpoint }),
|
|
})
|
|
await sub.unsubscribe()
|
|
}
|
|
isSubscribed.value = false
|
|
} catch (e: unknown) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to unsubscribe'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
return { isSupported, permission, isSubscribed, loading, error, checkSubscription, subscribe, unsubscribe }
|
|
})
|