import { useState, useEffect, useCallback, useRef } from 'react'; import { useSession } from 'next-auth/react'; import { Notification, NotificationCount } from '@/lib/types/notification'; // Default empty notification count const defaultNotificationCount: NotificationCount = { total: 0, unread: 0, sources: {} }; // Debounce function to limit API calls function debounce any>( func: T, wait: number ): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; return function(...args: Parameters) { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } 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 pollingIntervalRef = useRef(null); const lastFetchTimeRef = useRef(0); const isMountedRef = useRef(false); const isPollingRef = useRef(false); // Minimum time between fetches (in milliseconds) const MIN_FETCH_INTERVAL = 5000; // 5 seconds const POLLING_INTERVAL = 60000; // 1 minute // Fetch notification count with rate limiting const fetchNotificationCount = useCallback(async (force = false) => { if (!session?.user || !isMountedRef.current) return; const now = Date.now(); if (!force && now - lastFetchTimeRef.current < MIN_FETCH_INTERVAL) { console.log('Skipping notification count fetch - too soon'); return; } try { setError(null); lastFetchTimeRef.current = now; console.log('[useNotifications] Fetching notification count', { force }); // Add cache-busting parameter when force is true to ensure fresh data const url = force ? `/api/notifications/count?_t=${Date.now()}` : '/api/notifications/count'; const response = await fetch(url, { credentials: 'include', // Ensure cookies are sent with the request cache: force ? 'no-store' : 'default', // Disable cache when forcing refresh }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to fetch notification count:', { status: response.status, body: errorText }); setError(errorText || 'Failed to fetch notification count'); return; } const data = await response.json(); if (isMountedRef.current) { console.log('[useNotifications] Received notification count:', data); setNotificationCount(data); } } catch (err) { console.error('Error fetching notification count:', err); setError('Failed to fetch notification count'); } }, [session?.user]); // Debounced version to prevent rapid successive calls const debouncedFetchCount = useCallback( debounce(fetchNotificationCount, 300), [fetchNotificationCount] ); // Fetch notifications const fetchNotifications = useCallback(async (page = 1, limit = 20) => { if (!session?.user || !isMountedRef.current) return; const now = Date.now(); if (now - lastFetchTimeRef.current < MIN_FETCH_INTERVAL) { console.log('Skipping notifications fetch - too soon'); return; } setLoading(true); setError(null); lastFetchTimeRef.current = now; try { console.log('[useNotifications] Fetching notifications', { page, limit }); const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`, { credentials: 'include' // Ensure cookies are sent with the request }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to fetch notifications:', { status: response.status, body: errorText }); setError(errorText || 'Failed to fetch notifications'); return; } const data = await response.json(); if (isMountedRef.current) { setNotifications(data.notifications); } } catch (err) { console.error('Error fetching notifications:', err); setError('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 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 })); // Immediately refresh notification count (not debounced) to get accurate data // Use a small delay to ensure server cache is invalidated setTimeout(() => { fetchNotificationCount(true); }, 100); return true; } catch (err) { console.error('Error marking notification as read:', err); return false; } }, [session?.user, fetchNotificationCount]); // Mark all notifications as read const markAllAsRead = useCallback(async () => { if (!session?.user) return false; try { console.log('[useNotifications] Marking all notifications as read'); const response = await fetch('/api/notifications/read-all', { 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 all notifications as read:', { status: response.status, body: errorText }); return false; } // Update local state optimistically setNotifications(prev => prev.map(notification => ({ ...notification, isRead: true })) ); // Update count optimistically (set unread to 0 immediately for instant UI feedback) setNotificationCount(prev => ({ ...prev, unread: 0, total: prev.total, // Keep total the same sources: Object.fromEntries( Object.entries(prev.sources).map(([key, value]) => [ key, { ...value, unread: 0 } ]) ), })); // Immediately refresh notification count (not debounced) to get accurate data from server // Use a small delay to ensure server cache is invalidated setTimeout(() => { fetchNotificationCount(true); }, 200); return true; } catch (err) { console.error('Error marking all notifications as read:', err); return false; } }, [session?.user, fetchNotificationCount]); // Start polling for notification count const startPolling = useCallback(() => { if (isPollingRef.current) return; isPollingRef.current = true; if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); } // Ensure we don't create multiple intervals pollingIntervalRef.current = setInterval(() => { if (isMountedRef.current) { debouncedFetchCount(); } }, POLLING_INTERVAL); return () => stopPolling(); }, [debouncedFetchCount]); // Stop polling const stopPolling = useCallback(() => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } isPollingRef.current = false; }, []); // Initialize fetching on component mount and cleanup on unmount useEffect(() => { isMountedRef.current = true; if (status === 'authenticated' && session?.user) { // Initial fetches fetchNotificationCount(true); fetchNotifications(); // Start polling startPolling(); } return () => { isMountedRef.current = false; stopPolling(); }; }, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]); return { notifications, notificationCount, loading, error, fetchNotifications, fetchNotificationCount: () => debouncedFetchCount(true), markAsRead, markAllAsRead }; }