NeahNew/hooks/use-notifications.ts
2025-05-04 12:12:10 +02:00

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
};
}