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'); const response = await fetch('/api/notifications/count', { credentials: 'include' // Ensure cookies are sent with the request }); 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) { 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 setNotifications(prev => prev.map(notification => notification.id === notificationId ? { ...notification, isRead: true } : notification ) ); // Refresh notification count debouncedFetchCount(true); return true; } catch (err) { console.error('Error marking notification as read:', err); return false; } }, [session?.user, debouncedFetchCount]); // 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 setNotifications(prev => prev.map(notification => ({ ...notification, isRead: true })) ); // Refresh notification count debouncedFetchCount(true); return true; } catch (err) { console.error('Error marking all notifications as read:', err); return false; } }, [session?.user, debouncedFetchCount]); // 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 }; }