267 lines
7.9 KiB
TypeScript
267 lines
7.9 KiB
TypeScript
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<T extends (...args: any[]) => any>(
|
|
func: T,
|
|
wait: number
|
|
): (...args: Parameters<T>) => void {
|
|
let timeout: NodeJS.Timeout | null = null;
|
|
|
|
return function(...args: Parameters<T>) {
|
|
if (timeout) clearTimeout(timeout);
|
|
timeout = setTimeout(() => func(...args), wait);
|
|
};
|
|
}
|
|
|
|
export function useNotifications() {
|
|
const { data: session, status } = useSession();
|
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
const lastFetchTimeRef = useRef<number>(0);
|
|
const isMountedRef = useRef<boolean>(false);
|
|
const isPollingRef = useRef<boolean>(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
|
|
};
|
|
}
|