KI-AGENT: Zentrale Benachrichtigungsengine mit Desktop Push umsetzen
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
import {formatTimeAgo} from '@vueuse/core'
|
||||
|
||||
const { isNotificationsSlideoverOpen } = useDashboard()
|
||||
const toast = useToast()
|
||||
const desktopPush = useDesktopPush()
|
||||
|
||||
watch(isNotificationsSlideoverOpen, async (newVal,oldVal) => {
|
||||
if(newVal === true) {
|
||||
@@ -13,7 +15,8 @@ const notifications = ref([])
|
||||
|
||||
const setup = async () => {
|
||||
try {
|
||||
notifications.value = await useNuxtApp().$api("/api/resource/notifications_items")
|
||||
notifications.value = await useNuxtApp().$api("/api/notifications")
|
||||
await desktopPush.loadConfig()
|
||||
} catch (e) {
|
||||
notifications.value = []
|
||||
}
|
||||
@@ -23,9 +26,8 @@ setup()
|
||||
|
||||
const setNotificationAsRead = async (notification) => {
|
||||
try {
|
||||
await useNuxtApp().$api(`/api/resource/notifications_items/${notification.id}`, {
|
||||
method: "PUT",
|
||||
body: { readAt: new Date() }
|
||||
await useNuxtApp().$api(`/api/notifications/${notification.id}/read`, {
|
||||
method: "POST"
|
||||
})
|
||||
} catch (e) {
|
||||
// noop: endpoint optional in older/newer backend variants
|
||||
@@ -33,15 +35,78 @@ const setNotificationAsRead = async (notification) => {
|
||||
setup()
|
||||
|
||||
}
|
||||
|
||||
const enableDesktopPush = async () => {
|
||||
const success = await desktopPush.subscribe()
|
||||
|
||||
toast.add({
|
||||
title: success ? "Desktop Push aktiviert" : "Desktop Push konnte nicht aktiviert werden",
|
||||
description: desktopPush.error.value || undefined,
|
||||
color: success ? "success" : "error"
|
||||
})
|
||||
}
|
||||
|
||||
const sendTestPush = async () => {
|
||||
try {
|
||||
await desktopPush.sendTestPush()
|
||||
toast.add({ title: "Testbenachrichtigung gesendet", color: "success" })
|
||||
await setup()
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Testbenachrichtigung fehlgeschlagen",
|
||||
description: error?.data?.error || error?.message,
|
||||
color: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USlideover v-model:open="isNotificationsSlideoverOpen" title="Benachrichtigungen" side="right">
|
||||
<template #body>
|
||||
<div class="mb-4 rounded-md border border-default bg-muted p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-highlighted">
|
||||
Desktop Push
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
<span v-if="!desktopPush.supported.value">Dieser Browser unterstützt Desktop Push nicht.</span>
|
||||
<span v-else-if="!desktopPush.configured.value">Desktop Push ist im Backend noch nicht konfiguriert.</span>
|
||||
<span v-else-if="desktopPush.permission.value === 'granted'">Benachrichtigungen sind im Browser erlaubt.</span>
|
||||
<span v-else>Aktiviere Browser-Benachrichtigungen für FEDEO.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<UButton
|
||||
icon="i-heroicons-bell-alert"
|
||||
size="xs"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
:loading="desktopPush.loading.value"
|
||||
:disabled="!desktopPush.supported.value || !desktopPush.configured.value"
|
||||
@click="enableDesktopPush"
|
||||
>
|
||||
Aktivieren
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-paper-airplane"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:disabled="desktopPush.permission.value !== 'granted'"
|
||||
@click="sendTestPush"
|
||||
>
|
||||
Test
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NuxtLink
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:to="notification.link"
|
||||
:to="notification.payload?.link || notification.link || '/'"
|
||||
class="p-3 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer flex items-center gap-3 relative"
|
||||
@click="setNotificationAsRead(notification)"
|
||||
>
|
||||
|
||||
89
frontend/composables/useDesktopPush.js
Normal file
89
frontend/composables/useDesktopPush.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const base64UrlToUint8Array = (value) => {
|
||||
const padding = "=".repeat((4 - value.length % 4) % 4)
|
||||
const base64 = `${value}${padding}`.replace(/-/g, "+").replace(/_/g, "/")
|
||||
const rawData = window.atob(base64)
|
||||
const output = new Uint8Array(rawData.length)
|
||||
|
||||
for (let i = 0; i < rawData.length; i += 1) {
|
||||
output[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export const useDesktopPush = () => {
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const supported = computed(() =>
|
||||
process.client &&
|
||||
"serviceWorker" in navigator &&
|
||||
"PushManager" in window &&
|
||||
"Notification" in window
|
||||
)
|
||||
const permission = ref(process.client && "Notification" in window ? Notification.permission : "default")
|
||||
const configured = ref(false)
|
||||
const loading = ref(false)
|
||||
const error = ref("")
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (!supported.value) return { configured: false, publicKey: "" }
|
||||
|
||||
const config = await $api("/api/notifications/push/config")
|
||||
configured.value = Boolean(config.configured && config.publicKey)
|
||||
return config
|
||||
}
|
||||
|
||||
const subscribe = async () => {
|
||||
error.value = ""
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const config = await loadConfig()
|
||||
if (!configured.value) {
|
||||
throw new Error("Desktop Push ist im Backend noch nicht konfiguriert.")
|
||||
}
|
||||
|
||||
const nextPermission = await Notification.requestPermission()
|
||||
permission.value = nextPermission
|
||||
if (nextPermission !== "granted") {
|
||||
throw new Error("Benachrichtigungen wurden im Browser nicht erlaubt.")
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.register("/fedeo-push-sw.js")
|
||||
const existingSubscription = await registration.pushManager.getSubscription()
|
||||
const subscription = existingSubscription || await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: base64UrlToUint8Array(config.publicKey),
|
||||
})
|
||||
|
||||
await $api("/api/notifications/push/subscribe", {
|
||||
method: "POST",
|
||||
body: subscription.toJSON(),
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
error.value = err?.data?.error || err?.message || "Desktop Push konnte nicht aktiviert werden."
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const sendTestPush = async () => {
|
||||
return await $api("/api/notifications/test-push", {
|
||||
method: "POST",
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
supported,
|
||||
permission,
|
||||
configured,
|
||||
loading,
|
||||
error,
|
||||
loadConfig,
|
||||
subscribe,
|
||||
sendTestPush,
|
||||
}
|
||||
}
|
||||
@@ -395,7 +395,8 @@ const openMatrixCall = async (mode = "video") => {
|
||||
try {
|
||||
await leaveMatrixCall()
|
||||
const session = await $api(`${activeRoomEndpoint.value}/call-session`, {
|
||||
method: "POST"
|
||||
method: "POST",
|
||||
body: { mode }
|
||||
})
|
||||
matrixCallSession.value = session
|
||||
await connectMatrixCall(session, { video: mode === "video" })
|
||||
|
||||
50
frontend/public/fedeo-push-sw.js
Normal file
50
frontend/public/fedeo-push-sw.js
Normal file
@@ -0,0 +1,50 @@
|
||||
self.addEventListener("push", (event) => {
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
data = event.data ? event.data.json() : {}
|
||||
} catch (error) {
|
||||
data = {
|
||||
title: "FEDEO",
|
||||
message: event.data?.text() || "Neue Benachrichtigung",
|
||||
payload: {},
|
||||
}
|
||||
}
|
||||
|
||||
const payload = data.payload || {}
|
||||
const title = data.title || "FEDEO"
|
||||
const options = {
|
||||
body: data.message || "",
|
||||
icon: payload.icon || "/favicon.ico",
|
||||
badge: payload.badge || "/favicon.ico",
|
||||
data: {
|
||||
notificationId: data.id,
|
||||
link: payload.link || "/",
|
||||
},
|
||||
}
|
||||
|
||||
event.waitUntil(self.registration.showNotification(title, options))
|
||||
})
|
||||
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close()
|
||||
|
||||
const targetUrl = new URL(event.notification.data?.link || "/", self.location.origin).href
|
||||
|
||||
event.waitUntil((async () => {
|
||||
const windows = await self.clients.matchAll({
|
||||
type: "window",
|
||||
includeUncontrolled: true,
|
||||
})
|
||||
|
||||
for (const client of windows) {
|
||||
if ("focus" in client) {
|
||||
await client.focus()
|
||||
if ("navigate" in client) await client.navigate(targetUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await self.clients.openWindow(targetUrl)
|
||||
})())
|
||||
})
|
||||
Reference in New Issue
Block a user