feat: Implement push notification system for admin announcements

- Add a notification manager to handle push notifications and integrate with Nostr events.
- Create a push notification service to manage subscription and permission requests.
- Introduce components for notification settings and permission prompts in the UI.
- Update Nostr store to manage push notification state and enable/disable functionality.
- Enhance NostrFeed to send notifications for new admin announcements.
- Implement test notification functionality for development purposes.
This commit is contained in:
padreug 2025-07-03 08:34:05 +02:00
parent 6c1caac84b
commit c05f40f1ec
17 changed files with 1316 additions and 13 deletions

View file

@ -0,0 +1,222 @@
<template>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Bell class="h-5 w-5" />
Notification Settings
</CardTitle>
<CardDescription>
Configure when and how you receive push notifications
</CardDescription>
</CardHeader>
<CardContent class="space-y-6">
<!-- Main notification toggle -->
<div class="flex items-center justify-between">
<div>
<Label class="text-base font-medium">Push Notifications</Label>
<p class="text-sm text-muted-foreground">
Receive notifications even when the app is closed
</p>
</div>
<div class="flex items-center gap-2">
<Button
v-if="!notificationsEnabled && !isBlocked"
@click="enableNotifications"
:disabled="isLoading"
size="sm"
>
<Bell class="h-3 w-3 mr-1" />
{{ isLoading ? 'Enabling...' : 'Enable' }}
</Button>
<Button
v-else-if="notificationsEnabled"
@click="disableNotifications"
variant="outline"
size="sm"
>
<BellOff class="h-3 w-3 mr-1" />
Disable
</Button>
<Badge v-else-if="isBlocked" variant="destructive">
Blocked
</Badge>
</div>
</div>
<!-- Notification type preferences -->
<div v-if="notificationsEnabled" class="space-y-4">
<div class="border-t pt-4">
<Label class="text-sm font-medium mb-3 block">Notification Types</Label>
<div class="space-y-3">
<div class="flex items-center justify-between">
<div>
<Label class="text-sm">Admin Announcements</Label>
<p class="text-xs text-muted-foreground">
Important updates from community administrators
</p>
</div>
<input
type="checkbox"
v-model="options.adminAnnouncements"
@change="updateOptions"
class="h-4 w-4 rounded border-gray-300"
/>
</div>
<div class="flex items-center justify-between">
<div>
<Label class="text-sm">Mentions</Label>
<p class="text-xs text-muted-foreground">
When someone mentions you in a post
</p>
</div>
<input
type="checkbox"
v-model="options.mentions"
@change="updateOptions"
class="h-4 w-4 rounded border-gray-300"
/>
</div>
<div class="flex items-center justify-between">
<div>
<Label class="text-sm">Replies</Label>
<p class="text-xs text-muted-foreground">
When someone replies to your posts
</p>
</div>
<input
type="checkbox"
v-model="options.replies"
@change="updateOptions"
class="h-4 w-4 rounded border-gray-300"
/>
</div>
</div>
</div>
<!-- Test notification -->
<div class="border-t pt-4">
<Button
@click="sendTestNotification"
variant="outline"
size="sm"
:disabled="isTestLoading"
>
<TestTube class="h-3 w-3 mr-1" />
{{ isTestLoading ? 'Sending...' : 'Send Test Notification' }}
</Button>
</div>
</div>
<!-- Help text for blocked notifications -->
<div v-if="isBlocked" class="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
<div class="flex items-start gap-2">
<AlertCircle class="h-4 w-4 text-destructive mt-0.5" />
<div class="text-sm">
<p class="text-destructive font-medium mb-1">Notifications Blocked</p>
<p class="text-destructive/80">
You've blocked notifications for this site. To enable them:
</p>
<ol class="list-decimal list-inside mt-2 text-destructive/80 space-y-1">
<li>Click the lock icon in your browser's address bar</li>
<li>Select "Allow" for notifications</li>
<li>Refresh this page</li>
</ol>
</div>
</div>
</div>
<!-- Not supported message -->
<div v-if="!isSupported" class="bg-orange-50 dark:bg-orange-950 border border-orange-200 dark:border-orange-800 rounded-lg p-3">
<div class="flex items-center gap-2">
<AlertCircle class="h-4 w-4 text-orange-600 dark:text-orange-400" />
<span class="text-sm text-orange-800 dark:text-orange-200">
Push notifications are not supported by your browser
</span>
</div>
</div>
</CardContent>
</Card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Bell, BellOff, TestTube, AlertCircle } from 'lucide-vue-next'
import { useNostrStore } from '@/stores/nostr'
import { notificationManager } from '@/lib/notifications/manager'
import { pushService } from '@/lib/notifications/push'
import { configUtils } from '@/lib/config'
import { toast } from 'vue-sonner'
import { storeToRefs } from 'pinia'
const nostrStore = useNostrStore()
const { notificationsEnabled } = storeToRefs(nostrStore)
const isLoading = ref(false)
const isTestLoading = ref(false)
const options = ref(notificationManager.getOptions())
const isSupported = pushService.isSupported() && configUtils.hasPushConfig()
const isBlocked = ref(false)
onMounted(async () => {
await checkStatus()
})
async function checkStatus() {
isBlocked.value = pushService.getPermission() === 'denied'
await nostrStore.checkPushNotificationStatus()
}
async function enableNotifications() {
try {
isLoading.value = true
await nostrStore.enablePushNotifications()
toast.success('Push notifications enabled!')
await checkStatus()
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to enable notifications'
toast.error(message)
} finally {
isLoading.value = false
}
}
async function disableNotifications() {
try {
await nostrStore.disablePushNotifications()
toast.success('Push notifications disabled')
await checkStatus()
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to disable notifications'
toast.error(message)
}
}
function updateOptions() {
notificationManager.updateOptions(options.value)
toast.success('Notification preferences updated')
}
async function sendTestNotification() {
try {
isTestLoading.value = true
await notificationManager.sendTestNotification()
toast.success('Test notification sent!')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to send test notification'
toast.error(message)
} finally {
isTestLoading.value = false
}
}
</script>