165 lines
4.5 KiB
Vue
165 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import {
|
|
preferenceSet,
|
|
preferencesList,
|
|
testPushSend,
|
|
type NotificationChannel,
|
|
type NotificationPreference,
|
|
} from '@/api/notification.api.ts'
|
|
import { usePushNotifications } from '@/composables/usePushNotifications.ts'
|
|
import { useSnackbarStore } from '@/stores/snackbar.ts'
|
|
|
|
const CHANNEL_LABELS: Record<NotificationChannel, string> = {
|
|
EMAIL: 'Email',
|
|
PUSH: 'Push',
|
|
}
|
|
|
|
const snackbarStore = useSnackbarStore()
|
|
const push = usePushNotifications()
|
|
|
|
const preferences = ref<NotificationPreference[]>([])
|
|
const loading = ref(false)
|
|
const saving = ref<NotificationChannel | null>(null)
|
|
|
|
const permission = ref<NotificationPermission | 'unsupported'>(
|
|
'Notification' in window ? Notification.permission : 'unsupported',
|
|
)
|
|
const deviceSaving = ref(false)
|
|
const deviceEnabled = computed(() => !!push.installationId.value)
|
|
|
|
function showError(text: string, e: unknown) {
|
|
snackbarStore.add({ title: text, text: e instanceof Error ? e.message : '', color: 'error' })
|
|
}
|
|
|
|
async function loadPreferences() {
|
|
loading.value = true
|
|
try {
|
|
preferences.value = await preferencesList()
|
|
} catch (e) {
|
|
showError('Failed to load notification settings', e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function togglePreference(preference: NotificationPreference, enabled: boolean | null) {
|
|
const previous = preference.enabled
|
|
preference.enabled = !!enabled
|
|
saving.value = preference.channel
|
|
try {
|
|
await preferenceSet(preference.channel, preference.enabled)
|
|
} catch (e) {
|
|
preference.enabled = previous
|
|
showError('Failed to save notification settings', e)
|
|
} finally {
|
|
saving.value = null
|
|
}
|
|
}
|
|
|
|
async function toggleDevice(enabled: boolean | null) {
|
|
deviceSaving.value = true
|
|
try {
|
|
if (enabled) permission.value = await push.enable()
|
|
else await push.disable(true)
|
|
} catch (e) {
|
|
showError('Failed to change push in this browser', e)
|
|
} finally {
|
|
deviceSaving.value = false
|
|
}
|
|
}
|
|
|
|
const testSending = ref(false)
|
|
|
|
async function sendTestPush() {
|
|
testSending.value = true
|
|
try {
|
|
await testPushSend()
|
|
snackbarStore.add({
|
|
title: 'Test push sent',
|
|
text: 'It should arrive on all your devices shortly',
|
|
color: 'success',
|
|
})
|
|
} catch (e) {
|
|
showError('Failed to send test push', e)
|
|
} finally {
|
|
testSending.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await loadPreferences()
|
|
|
|
if (!navigator.permissions) return
|
|
const perm = await navigator.permissions.query({ name: 'notifications' })
|
|
perm.onchange = () => {
|
|
permission.value = Notification.permission
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<v-sheet border class="pa-4 d-flex flex-column ga-2">
|
|
<div class="text-uppercase text-title-large">Notifications</div>
|
|
|
|
<v-skeleton-loader v-if="loading && !preferences.length" type="list-item, list-item" />
|
|
<div v-else class="d-flex flex-column">
|
|
<v-switch
|
|
inset="material"
|
|
size="x-small"
|
|
true-icon="mdi-check"
|
|
false-icon="mdi-close"
|
|
v-for="preference in preferences"
|
|
:key="preference.channel"
|
|
:model-value="preference.enabled"
|
|
:label="CHANNEL_LABELS[preference.channel] ?? preference.channel"
|
|
:loading="saving === preference.channel"
|
|
:disabled="saving === preference.channel"
|
|
color="primary"
|
|
density="compact"
|
|
hide-details
|
|
@update:model-value="togglePreference(preference, $event)"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="text-uppercase">Push in this browser</div>
|
|
<div v-if="permission === 'unsupported'" class="text-medium-emphasis">
|
|
This browser does not support push notifications
|
|
</div>
|
|
<div v-else-if="permission === 'denied'" class="text-medium-emphasis">
|
|
Notifications are blocked — allow them in the browser site settings
|
|
</div>
|
|
<v-switch
|
|
v-else
|
|
inset="material"
|
|
size="x-small"
|
|
true-icon="mdi-check"
|
|
false-icon="mdi-close"
|
|
:model-value="deviceEnabled"
|
|
:label="deviceEnabled ? 'Enabled' : 'Disabled'"
|
|
:loading="deviceSaving"
|
|
:disabled="deviceSaving"
|
|
color="primary"
|
|
density="compact"
|
|
hide-details
|
|
@update:model-value="toggleDevice"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<v-btn
|
|
class="mt-2"
|
|
variant="tonal"
|
|
prepend-icon="mdi-bell-ring-outline"
|
|
:loading="testSending"
|
|
@click="sendTestPush"
|
|
>
|
|
Send test push
|
|
</v-btn>
|
|
</div>
|
|
</v-sheet>
|
|
</template>
|
|
|
|
<style scoped lang="scss"></style>
|