import { useState, useEffect, useCallback, useRef } from 'react'; import { useSession } from 'next-auth/react'; import { Notification, NotificationCount } from '@/lib/types/notification'; import { useUnifiedRefresh } from './use-unified-refresh'; import { REFRESH_INTERVALS } from '@/lib/constants/refresh-intervals'; import { requestDeduplicator } from '@/lib/utils/request-deduplication'; // Default empty notification count const defaultNotificationCount: NotificationCount = { total: 0, unread: 0, sources: {} }; export function useNotifications() { const { data: session, status } = useSession(); const [notifications, setNotifications] = useState([]); const [notificationCount, setNotificationCount] = useState(defaultNotificationCount); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const isMountedRef = useRef(false); // Fetch notification count with request deduplication const fetchNotificationCount = useCallback(async (force = false) => { if (!session?.user || !isMountedRef.current) return; try { setError(null); console.log('[useNotifications] Fetching notification count', { force }); // Use request deduplication to prevent duplicate calls const requestKey = `notifications-count-${session.user.id}`; const url = force ? `/api/notifications/count?force=true&_t=${Date.now()}` : '/api/notifications/count'; const data = await requestDeduplicator.execute( requestKey, async () => { const response = await fetch(url, { credentials: 'include', cache: force ? 'no-store' : 'default', }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to fetch notification count:', { status: response.status, body: errorText }); throw new Error(errorText || 'Failed to fetch notification count'); } return response.json(); }, 2000 // 2 second deduplication window ); if (isMountedRef.current) { console.log('[useNotifications] Received notification count:', data); setNotificationCount(data); } } catch (err: any) { console.error('Error fetching notification count:', err); if (isMountedRef.current) { setError(err.message || 'Failed to fetch notification count'); } } }, [session?.user]); // Mark all notifications as read (when dropdown is opened) const markAllAsRead = useCallback(async () => { if (!session?.user || !isMountedRef.current) return false; try { const response = await fetch('/api/notifications/mark-all-read', { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error('Failed to mark all notifications as read'); } // Update local state - reset count to 0 setNotificationCount({ total: 0, unread: 0, sources: { email: { total: 0, unread: 0 }, rocketchat: { total: 0, unread: 0 }, leantime: { total: 0, unread: 0 }, calendar: { total: 0, unread: 0 }, }, }); return true; } catch (err: any) { console.error('Error marking all notifications as read:', err); setError(err.message || 'Failed to mark all notifications as read'); return false; } }, [session?.user]); // Mark notification as read const markAsRead = useCallback(async (notificationId: string) => { if (!session?.user || !isMountedRef.current) return false; try { const response = await fetch(`/api/notifications/${notificationId}/read`, { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error('Failed to mark notification as read'); } // Update local state - remove from list and update count setNotifications(prev => prev.filter(n => n.id !== notificationId)); setNotificationCount(prev => ({ ...prev, unread: Math.max(0, prev.unread - 1), })); return true; } catch (err: any) { console.error('Error marking notification as read:', err); setError(err.message || 'Failed to mark notification as read'); return false; } }, [session?.user]); // Fetch notifications with request deduplication const fetchNotifications = useCallback(async (page = 1, limit = 20, source?: string) => { if (!session?.user || !isMountedRef.current) return; setLoading(true); setError(null); try { console.log('[useNotifications] Fetching notifications', { page, limit, source }); // Use request deduplication to prevent duplicate calls const requestKey = `notifications-${session.user.id}-${page}-${limit}-${source || 'all'}`; const url = `/api/notifications?limit=${limit}${source ? `&source=${source}` : ''}`; const data = await requestDeduplicator.execute( requestKey, async () => { const response = await fetch(url, { credentials: 'include', cache: 'no-store', }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to fetch notifications:', { status: response.status, body: errorText }); throw new Error(errorText || 'Failed to fetch notifications'); } return response.json(); }, 2000 // 2 second deduplication window ); if (isMountedRef.current) { // Filter by source if specified let filtered = data.notifications || []; if (source) { filtered = filtered.filter((n: Notification) => n.source === source); } setNotifications(filtered); } } catch (err: any) { console.error('Error fetching notifications:', err); if (isMountedRef.current) { setError(err.message || 'Failed to fetch notifications'); } } finally { if (isMountedRef.current) { setLoading(false); } } }, [session?.user]); // Use unified refresh system for notification count // Note: Widgets update the count via /api/notifications/update // This polling is a fallback to ensure count is refreshed periodically const { refresh: refreshCount } = useUnifiedRefresh({ resource: 'notifications-count', interval: REFRESH_INTERVALS.NOTIFICATIONS_COUNT, enabled: status === 'authenticated', onRefresh: async () => { await fetchNotificationCount(false); // Use cache, widgets update it }, priority: 'high', }); // Listen for custom events to trigger immediate refresh useEffect(() => { if (status !== 'authenticated') return; const handleNotificationUpdate = (event: CustomEvent) => { console.log('[useNotifications] Received notification update event', event.detail); // Refresh count immediately when widget updates // Use a small delay to ensure the registry has been updated setTimeout(() => { fetchNotificationCount(false); // Use cache, widget already updated it }, 100); }; // Listen for custom event from widgets window.addEventListener('notification-updated', handleNotificationUpdate as EventListener); return () => { window.removeEventListener('notification-updated', handleNotificationUpdate as EventListener); }; }, [status, fetchNotificationCount]); // Initialize fetching on component mount and cleanup on unmount useEffect(() => { isMountedRef.current = true; if (status === 'authenticated' && session?.user) { // Initial fetches - use cache, widgets will update it fetchNotificationCount(false); fetchNotifications(); } return () => { isMountedRef.current = false; }; }, [status, session?.user, fetchNotificationCount, fetchNotifications]); return { notifications, notificationCount, loading, error, fetchNotifications, fetchNotificationCount: () => fetchNotificationCount(true), markAsRead, markAllAsRead, }; }