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 [markingProgress, setMarkingProgress] = useState<{ current: number; total: number } | null>(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?_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]); // Fetch notifications with request deduplication const fetchNotifications = useCallback(async (page = 1, limit = 20) => { if (!session?.user || !isMountedRef.current) return; setLoading(true); setError(null); try { console.log('[useNotifications] Fetching notifications', { page, limit }); // Use request deduplication to prevent duplicate calls const requestKey = `notifications-${session.user.id}-${page}-${limit}`; const data = await requestDeduplicator.execute( requestKey, async () => { const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`, { credentials: 'include' }); 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) { setNotifications(data.notifications); } } 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]); // Mark notification as read const markAsRead = useCallback(async (notificationId: string) => { if (!session?.user) return false; try { console.log('[useNotifications] Marking notification as read:', notificationId); const response = await fetch(`/api/notifications/${notificationId}/read`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include' // Ensure cookies are sent with the request }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to mark notification as read:', { status: response.status, body: errorText }); return false; } // Update local state optimistically (only if we're confident it will succeed) setNotifications(prev => prev.map(notification => notification.id === notificationId ? { ...notification, isRead: true } : notification ) ); // Update count optimistically (decrement unread count) setNotificationCount(prev => ({ ...prev, unread: Math.max(0, prev.unread - 1), // Ensure it doesn't go below 0 total: prev.total, // Keep total the same })); // Refresh notification count after a delay to ensure cache is invalidated // Poll until count matches expected value or timeout let pollCount = 0; const maxPolls = 5; const pollInterval = 500; // 500ms between polls const pollForCount = async () => { if (pollCount >= maxPolls) return; pollCount++; await new Promise(resolve => setTimeout(resolve, pollInterval)); await fetchNotificationCount(true); // Check if count matches expected (unread should be prev - 1) // If not, poll again if (pollCount < maxPolls) { setTimeout(pollForCount, pollInterval); } }; // Start polling after initial delay setTimeout(pollForCount, 300); return true; } catch (err) { console.error('Error marking notification as read:', err); return false; } }, [session?.user, fetchNotificationCount]); // Mark all notifications as read with progress tracking const markAllAsRead = useCallback(async () => { if (!session?.user) return false; try { console.log('[useNotifications] Marking all notifications as read'); // Show loading state instead of optimistic update setMarkingProgress({ current: 0, total: notificationCount.unread }); const response = await fetch('/api/notifications/read-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to mark all notifications as read:', { status: response.status, body: errorText }); setMarkingProgress(null); return false; } // Update local state optimistically after successful response setNotifications(prev => prev.map(notification => ({ ...notification, isRead: true })) ); // Update count optimistically setNotificationCount(prev => ({ ...prev, unread: 0, total: prev.total, sources: Object.fromEntries( Object.entries(prev.sources).map(([key, value]) => [ key, { ...value, unread: 0 } ]) ), })); // Clear progress setMarkingProgress(null); // Refresh notification count after a delay to ensure cache is invalidated setTimeout(() => { fetchNotificationCount(true); }, 500); return true; } catch (err) { console.error('Error marking all notifications as read:', err); setMarkingProgress(null); return false; } }, [session?.user, fetchNotificationCount, notificationCount.unread]); // Use unified refresh system for notification count const { refresh: refreshCount } = useUnifiedRefresh({ resource: 'notifications-count', interval: REFRESH_INTERVALS.NOTIFICATIONS_COUNT, enabled: status === 'authenticated', onRefresh: async () => { await fetchNotificationCount(false); }, priority: 'high', }); // Initialize fetching on component mount and cleanup on unmount useEffect(() => { isMountedRef.current = true; if (status === 'authenticated' && session?.user) { // Initial fetches fetchNotificationCount(true); fetchNotifications(); } return () => { isMountedRef.current = false; }; }, [status, session?.user, fetchNotificationCount, fetchNotifications]); return { notifications, notificationCount, loading, error, markingProgress, // Progress for mark all as read fetchNotifications, fetchNotificationCount: () => fetchNotificationCount(true), markAsRead, markAllAsRead }; }