import { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react'; import toast from 'react-hot-toast'; import { useAuth } from './AuthContext'; import api from '../services/api'; const NotificationContext = createContext(); export function NotificationProvider({ children }) { const { user } = useAuth(); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); // Track seen IDs to detect genuinely new arrivals and show toasts const seenIds = useRef(new Set()); const initialized = useRef(false); const fetch = useCallback(async () => { if (!user) return; try { const res = await api.get('/notifications'); const incoming = res.data.notifications || []; setNotifications(incoming); setUnreadCount(res.data.unreadCount || 0); // First fetch: just seed the seen-set without toasting if (!initialized.current) { incoming.forEach(n => seenIds.current.add(n.id)); initialized.current = true; return; } // Subsequent fetches: toast new unread notifications const newItems = incoming.filter(n => !n.read && !seenIds.current.has(n.id)); newItems.forEach(n => { seenIds.current.add(n.id); const icon = notificationIcon(n.type); toast(`${icon} ${n.title}`, { duration: 5000 }); }); } catch { /* silent – server may not be reachable */ } }, [user]); useEffect(() => { if (!user) { setNotifications([]); setUnreadCount(0); seenIds.current = new Set(); initialized.current = false; return; } fetch(); const interval = setInterval(fetch, 30_000); return () => clearInterval(interval); }, [user, fetch]); const markRead = async (id) => { try { await api.post(`/notifications/${id}/read`); setNotifications(prev => prev.map(n => (n.id === id ? { ...n, read: 1 } : n)), ); setUnreadCount(prev => Math.max(0, prev - 1)); } catch { /* silent */ } }; const markAllRead = async () => { try { await api.post('/notifications/read-all'); setNotifications(prev => prev.map(n => ({ ...n, read: 1 }))); setUnreadCount(0); } catch { /* silent */ } }; return ( {children} ); } export function useNotifications() { const ctx = useContext(NotificationContext); if (!ctx) throw new Error('useNotifications must be used within NotificationProvider'); return ctx; } function notificationIcon(type) { switch (type) { case 'room_share_added': return '🔗'; case 'room_share_removed': return '🚫'; case 'federation_invite_received': return '📩'; default: return '🔔'; } }