diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index a21d4fe..a39733b 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -1,11 +1,13 @@ import { getToken } from '@/config/func' import { useSocket } from '@/pages/pager/hooks/useSocket' -import { type FC, useEffect, useCallback, useState } from 'react' +import { type FC, useEffect, useCallback, useState, useRef } from 'react' import type { NotificationPayload } from '@/types/notification.types' import { NotificationSubject } from '@/types/notification.types' import NotificationItem from '@/components/NotificationItem' import { useQueryClient } from '@tanstack/react-query' +const NOTIFICATION_SOUND_SRC = '/mixkit-correct-answer-tone-2870.wav' + interface NotificationItemData extends NotificationPayload { id: string timestamp: number @@ -13,13 +15,26 @@ interface NotificationItemData extends NotificationPayload { const Notification: FC = () => { const [notifications, setNotifications] = useState([]) - const [audioEnabled, setAudioEnabled] = useState(false) + const audioRef = useRef(null) + const audioUnlockedRef = useRef(false) const queryClient = useQueryClient() const token = getToken() const socketUrl = `${import.meta.env.VITE_SOCKET_URL}/notifications` const { connect, socket, error } = useSocket(socketUrl, token!) + useEffect(() => { + const audio = new Audio(NOTIFICATION_SOUND_SRC) + audio.preload = 'auto' + audio.volume = 0.5 + audioRef.current = audio + + return () => { + audio.pause() + audioRef.current = null + } + }, []) + useEffect(() => { if (error) { console.error('❌ Notification: خطا در اتصال:', error) @@ -31,35 +46,54 @@ const Notification: FC = () => { }, [connect]) useEffect(() => { - const enableAudio = async () => { + const unlockAudio = async () => { + if (audioUnlockedRef.current || !audioRef.current) { + return + } + try { - const audio = new Audio('/mixkit-correct-answer-tone-2870.wav') - audio.volume = 0 + const audio = audioRef.current + audio.muted = true await audio.play() audio.pause() audio.currentTime = 0 - setAudioEnabled(true) + audio.muted = false + audioUnlockedRef.current = true } catch { - console.warn('صدا هنوز فعال نشده است') + // Browser may still block until a later gesture — keep listening. } } const handleUserInteraction = () => { - if (!audioEnabled) { - enableAudio() - } + void unlockAudio() } - window.addEventListener('click', handleUserInteraction, { once: true }) - window.addEventListener('touchstart', handleUserInteraction, { once: true }) - window.addEventListener('keydown', handleUserInteraction, { once: true }) + window.addEventListener('click', handleUserInteraction) + window.addEventListener('touchstart', handleUserInteraction) + window.addEventListener('keydown', handleUserInteraction) return () => { window.removeEventListener('click', handleUserInteraction) window.removeEventListener('touchstart', handleUserInteraction) window.removeEventListener('keydown', handleUserInteraction) } - }, [audioEnabled]) + }, []) + + const playNotificationSound = useCallback(() => { + const audio = audioRef.current + if (!audio || !audioUnlockedRef.current) { + return + } + + try { + audio.currentTime = 0 + void audio.play().catch((err) => { + console.warn('خطا در پخش صدا:', err) + }) + } catch (err) { + console.warn('خطا در پخش صدا:', err) + } + }, []) const handleGetNotification = useCallback((data: NotificationPayload) => { const newNotification: NotificationItemData = { @@ -68,14 +102,14 @@ const Notification: FC = () => { timestamp: Date.now(), } setNotifications((prev) => [...prev, newNotification]) + playNotificationSound() - // Refetch بر اساس نوع نوتیفیکیشن if (data.subject === NotificationSubject.PAGER_CREATED) { queryClient.invalidateQueries({ queryKey: ['pagers'] }) } else if (data.subject === NotificationSubject.ORDER_CREATED) { queryClient.invalidateQueries({ queryKey: ['orders'] }) } - }, [queryClient]) + }, [playNotificationSound, queryClient]) const handleRemoveNotification = useCallback((id: string) => { setNotifications((prev) => prev.filter((notif) => notif.id !== id)) @@ -105,7 +139,6 @@ const Notification: FC = () => { subject={notification.subject} body={notification.body} onRemove={handleRemoveNotification} - audioEnabled={audioEnabled} /> ))} diff --git a/src/components/NotificationItem.tsx b/src/components/NotificationItem.tsx index 7d07dad..5155f2e 100644 --- a/src/components/NotificationItem.tsx +++ b/src/components/NotificationItem.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect } from 'react' +import { type FC } from 'react' import { CloseCircle, NotificationBing, ShoppingBag, MessageText1 } from 'iconsax-react' import Button from '@/components/Button' import { NotificationSubject } from '@/types/notification.types' @@ -8,32 +8,9 @@ interface NotificationItemProps { subject: string body: string onRemove: (id: string) => void - audioEnabled: boolean } -const NotificationItem: FC = ({ id, subject, body, onRemove, audioEnabled }) => { - useEffect(() => { - if (!audioEnabled) { - return - } - - const playNotificationSound = async () => { - try { - const audio = new Audio('/mixkit-correct-answer-tone-2870.wav') - audio.volume = 0.5 - - audio.addEventListener('error', (err) => { - console.error('❌ خطا در لود فایل صوتی:', err) - }, { once: true }) - - await audio.play() - } catch (err) { - console.warn('خطا در پخش صدا:', err) - } - } - - playNotificationSound() - }, [audioEnabled]) +const NotificationItem: FC = ({ id, subject, body, onRemove }) => { const getNotificationConfig = (subjectType: string) => { switch (subjectType) { case NotificationSubject.PAGER_CREATED: