fix(notification+vlog upload)
This commit is contained in:
@@ -1,10 +1,28 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import axios from '@/utils/axios'
|
||||
|
||||
// Utilitaire pour convertir la clé VAPID
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
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
|
||||
}
|
||||
|
||||
class NotificationService {
|
||||
constructor() {
|
||||
this.pollingInterval = null
|
||||
this.pollInterval = 30000 // 30 secondes
|
||||
this.isPolling = false
|
||||
this.vapidPublicKey = null
|
||||
}
|
||||
|
||||
// Détecter iOS
|
||||
@@ -25,6 +43,14 @@ class NotificationService {
|
||||
console.warn('Notifications API not supported')
|
||||
return false
|
||||
}
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
console.warn('Service Worker API not supported')
|
||||
return false
|
||||
}
|
||||
if (!('PushManager' in window)) {
|
||||
console.warn('Push API not supported')
|
||||
return false
|
||||
}
|
||||
|
||||
// Sur iOS, les notifications push ne fonctionnent que si la PWA est installée (iOS 16.4+)
|
||||
if (this.isIOS()) {
|
||||
@@ -49,6 +75,8 @@ class NotificationService {
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
// Le polling est conservé comme fallback pour les notifications in-app
|
||||
// ou si le push n'est pas supporté/activé
|
||||
if (this.isPolling) return
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -81,40 +109,9 @@ class NotificationService {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authStore.fetchNotifications()
|
||||
|
||||
// Si de nouvelles notifications non lues ont été détectées
|
||||
if (result && result.hasNewNotifications && result.newCount > result.previousCount) {
|
||||
// Trouver les nouvelles notifications non lues (les plus récentes en premier)
|
||||
const newUnreadNotifications = authStore.notifications
|
||||
.filter(n => !n.is_read)
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||
.slice(0, result.newCount - result.previousCount)
|
||||
|
||||
if (newUnreadNotifications.length > 0) {
|
||||
// Ne pas afficher de notification si l'app est en focus
|
||||
// (pour éviter les doublons avec les notifications dans l'app)
|
||||
if (document.hasFocus()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Afficher une notification push pour la plus récente
|
||||
const latestNotification = newUnreadNotifications[0]
|
||||
|
||||
// Gérer les liens de posts différemment
|
||||
let link = latestNotification.link || '/'
|
||||
if (link.startsWith('/posts/')) {
|
||||
const postId = link.split('/posts/')[1]
|
||||
link = `/posts?highlight=${postId}`
|
||||
}
|
||||
|
||||
await this.showPushNotification(latestNotification.title, {
|
||||
body: latestNotification.message,
|
||||
link: link,
|
||||
data: { notificationId: latestNotification.id }
|
||||
})
|
||||
}
|
||||
}
|
||||
// Juste mettre à jour le store (compteur, liste)
|
||||
// Les notifications push sont gérées par le service worker
|
||||
await authStore.fetchNotifications()
|
||||
} catch (error) {
|
||||
console.error('Error polling notifications:', error)
|
||||
}
|
||||
@@ -128,34 +125,70 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer la clé publique VAPID depuis le backend
|
||||
async getVapidPublicKey() {
|
||||
if (this.vapidPublicKey) return this.vapidPublicKey
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/push/vapid-public-key')
|
||||
this.vapidPublicKey = response.data.public_key
|
||||
return this.vapidPublicKey
|
||||
} catch (error) {
|
||||
console.error('Error fetching VAPID public key:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// S'abonner aux notifications push
|
||||
async subscribeToPush() {
|
||||
if (!this.isNotificationSupported()) return false
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
const publicKey = await this.getVapidPublicKey()
|
||||
|
||||
if (!publicKey) {
|
||||
console.error('No VAPID public key available')
|
||||
return false
|
||||
}
|
||||
|
||||
const convertedVapidKey = urlBase64ToUint8Array(publicKey)
|
||||
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: convertedVapidKey
|
||||
})
|
||||
|
||||
console.log('✅ Push subscription successful:', subscription)
|
||||
|
||||
// Envoyer l'abonnement au backend
|
||||
await axios.post('/api/push/subscribe', {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('p256dh')))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
auth: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('auth')))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error subscribing to push:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion des notifications push PWA
|
||||
async requestNotificationPermission() {
|
||||
console.log('🔔 requestNotificationPermission appelée')
|
||||
console.log('🔔 isIOS:', this.isIOS())
|
||||
console.log('🔔 isPWAInstalled:', this.isPWAInstalled())
|
||||
console.log('🔔 Notification API disponible:', 'Notification' in window)
|
||||
console.log('🔔 Permission actuelle:', Notification.permission)
|
||||
|
||||
if (!this.isNotificationSupported()) {
|
||||
console.warn('⚠️ Notifications not supported on this platform')
|
||||
if (this.isIOS()) {
|
||||
if (!this.isPWAInstalled()) {
|
||||
console.warn('⚠️ iOS: PWA must be installed (added to home screen)')
|
||||
}
|
||||
const iosVersion = navigator.userAgent.match(/OS (\d+)_(\d+)/)
|
||||
if (iosVersion) {
|
||||
const major = parseInt(iosVersion[1], 10)
|
||||
const minor = parseInt(iosVersion[2], 10)
|
||||
if (major < 16 || (major === 16 && minor < 4)) {
|
||||
console.warn(`⚠️ iOS: Version ${major}.${minor} - Push notifications require iOS 16.4+`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
console.log('✅ Notification permission already granted')
|
||||
// S'assurer qu'on est bien abonné au push
|
||||
this.subscribeToPush()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -167,7 +200,6 @@ class NotificationService {
|
||||
// Sur iOS, s'assurer que la PWA est installée avant de demander
|
||||
if (this.isIOS() && !this.isPWAInstalled()) {
|
||||
console.warn('⚠️ iOS: Cannot request notification permission - PWA must be installed first')
|
||||
console.warn('⚠️ Instructions: Add the app to home screen, then open it from home screen')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -176,29 +208,9 @@ class NotificationService {
|
||||
const permission = await Notification.requestPermission()
|
||||
const granted = permission === 'granted'
|
||||
|
||||
console.log('🔔 Résultat de la demande:', permission)
|
||||
|
||||
if (granted) {
|
||||
console.log('✅ Notification permission granted')
|
||||
|
||||
// Sur iOS, vérifier que le service worker est prêt
|
||||
if (this.isIOS() && 'serviceWorker' in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
console.log('✅ Service worker ready on iOS')
|
||||
|
||||
// Tester une notification pour vérifier que ça fonctionne
|
||||
await registration.showNotification('Test LeDiscord', {
|
||||
body: 'Les notifications sont activées !',
|
||||
icon: '/icon-192x192.png',
|
||||
badge: '/icon-96x96.png',
|
||||
tag: 'test-notification'
|
||||
})
|
||||
console.log('✅ Test notification sent successfully')
|
||||
} catch (error) {
|
||||
console.error('❌ Error testing notification on iOS:', error)
|
||||
}
|
||||
}
|
||||
await this.subscribeToPush()
|
||||
} else {
|
||||
console.warn('❌ Notification permission denied:', permission)
|
||||
}
|
||||
@@ -211,115 +223,30 @@ class NotificationService {
|
||||
}
|
||||
|
||||
async showPushNotification(title, options = {}) {
|
||||
console.log('🔔 showPushNotification appelée:', { title, options })
|
||||
// Cette méthode est maintenant principalement utilisée pour les tests
|
||||
// ou les notifications locales générées par le client
|
||||
|
||||
if (!this.isNotificationSupported()) {
|
||||
console.warn('⚠️ Cannot show notification - not supported on this platform')
|
||||
return null
|
||||
}
|
||||
|
||||
const hasPermission = await this.requestNotificationPermission()
|
||||
if (!hasPermission) {
|
||||
console.warn('⚠️ Cannot show notification - permission not granted')
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('✅ Permission granted, affichage de la notification...')
|
||||
|
||||
// Préparer les options de notification (iOS ne supporte pas vibrate)
|
||||
const notificationOptions = {
|
||||
body: options.body || options.message || '',
|
||||
icon: '/icon-192x192.png',
|
||||
badge: '/icon-96x96.png',
|
||||
tag: 'lediscord-notification',
|
||||
requireInteraction: false,
|
||||
data: {
|
||||
link: options.link || '/',
|
||||
notificationId: options.data?.notificationId
|
||||
},
|
||||
...options
|
||||
}
|
||||
|
||||
// Retirer vibrate sur iOS (non supporté)
|
||||
if (!this.isIOS()) {
|
||||
notificationOptions.vibrate = [200, 100, 200]
|
||||
}
|
||||
if (!this.isNotificationSupported()) return null
|
||||
if (Notification.permission !== 'granted') return null
|
||||
|
||||
// Toujours utiliser le service worker pour les notifications push
|
||||
// Cela permet aux notifications de fonctionner même quand l'app est fermée
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
console.log('🔔 Tentative d\'affichage via service worker...')
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
console.log('🔔 Service worker ready, active:', !!registration.active)
|
||||
if (!registration.active) return null
|
||||
|
||||
if (!registration.active) {
|
||||
console.warn('⚠️ Service worker not active, using fallback')
|
||||
throw new Error('Service worker not active')
|
||||
}
|
||||
|
||||
// Sur iOS, utiliser directement l'API du service worker
|
||||
// (les messages peuvent ne pas fonctionner correctement)
|
||||
if (this.isIOS()) {
|
||||
console.log('🔔 iOS: Utilisation directe de showNotification')
|
||||
await registration.showNotification(title, notificationOptions)
|
||||
console.log('✅ Notification affichée via service worker (iOS)')
|
||||
} else {
|
||||
// Envoyer un message au service worker pour afficher la notification
|
||||
registration.active.postMessage({
|
||||
type: 'SHOW_NOTIFICATION',
|
||||
title,
|
||||
options: notificationOptions
|
||||
})
|
||||
|
||||
// Aussi utiliser l'API directe du service worker
|
||||
await registration.showNotification(title, notificationOptions)
|
||||
console.log('✅ Notification affichée via service worker')
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('❌ Error showing notification via service worker:', error)
|
||||
console.error('❌ Error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
await registration.showNotification(title, {
|
||||
icon: '/icon-192x192.png',
|
||||
badge: '/icon-96x96.png',
|
||||
tag: 'lediscord-notification',
|
||||
...options
|
||||
})
|
||||
// Continuer avec le fallback
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error showing notification:', error)
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Service worker not available')
|
||||
}
|
||||
|
||||
// Fallback: notification native du navigateur (seulement si le SW n'est pas disponible)
|
||||
try {
|
||||
const notification = new Notification(title, notificationOptions)
|
||||
|
||||
notification.onclick = () => {
|
||||
window.focus()
|
||||
notification.close()
|
||||
|
||||
if (options.link) {
|
||||
// Utiliser le router si disponible
|
||||
if (window.location.pathname !== options.link) {
|
||||
window.location.href = options.link
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fermer automatiquement après 5 secondes (sauf sur iOS où c'est géré par le système)
|
||||
if (!this.isIOS()) {
|
||||
setTimeout(() => {
|
||||
notification.close()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
console.log('Notification shown via native API')
|
||||
return notification
|
||||
} catch (error) {
|
||||
console.error('Error showing notification:', error)
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Écouter les messages du service worker pour les notifications push
|
||||
@@ -327,8 +254,8 @@ class NotificationService {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'NOTIFICATION') {
|
||||
const { title, options } = event.data
|
||||
this.showPushNotification(title, options)
|
||||
// Notification reçue via message (fallback)
|
||||
console.log('Notification message received:', event.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user