NeahStable/hooks/use-notifications.ts
2026-01-16 01:39:42 +01:00

250 lines
8.3 KiB
TypeScript

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<Notification[]>([]);
const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const isMountedRef = useRef<boolean>(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?force=true&_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]);
// Mark all notifications as read (when dropdown is opened)
const markAllAsRead = useCallback(async () => {
if (!session?.user || !isMountedRef.current) return false;
try {
const response = await fetch('/api/notifications/mark-all-read', {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to mark all notifications as read');
}
// Update local state - reset count to 0
setNotificationCount({
total: 0,
unread: 0,
sources: {
email: { total: 0, unread: 0 },
rocketchat: { total: 0, unread: 0 },
leantime: { total: 0, unread: 0 },
calendar: { total: 0, unread: 0 },
},
});
return true;
} catch (err: any) {
console.error('Error marking all notifications as read:', err);
setError(err.message || 'Failed to mark all notifications as read');
return false;
}
}, [session?.user]);
// Mark notification as read
const markAsRead = useCallback(async (notificationId: string) => {
if (!session?.user || !isMountedRef.current) return false;
try {
const response = await fetch(`/api/notifications/${notificationId}/read`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to mark notification as read');
}
// Update local state - remove from list and update count
setNotifications(prev => prev.filter(n => n.id !== notificationId));
setNotificationCount(prev => ({
...prev,
unread: Math.max(0, prev.unread - 1),
}));
return true;
} catch (err: any) {
console.error('Error marking notification as read:', err);
setError(err.message || 'Failed to mark notification as read');
return false;
}
}, [session?.user]);
// Fetch notifications with request deduplication
const fetchNotifications = useCallback(async (page = 1, limit = 20, source?: string) => {
if (!session?.user || !isMountedRef.current) return;
setLoading(true);
setError(null);
try {
console.log('[useNotifications] Fetching notifications', { page, limit, source });
// Use request deduplication to prevent duplicate calls
const requestKey = `notifications-${session.user.id}-${page}-${limit}-${source || 'all'}`;
const url = `/api/notifications?limit=${limit}${source ? `&source=${source}` : ''}`;
const data = await requestDeduplicator.execute(
requestKey,
async () => {
const response = await fetch(url, {
credentials: 'include',
cache: 'no-store',
});
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) {
// Filter by source if specified
let filtered = data.notifications || [];
if (source) {
filtered = filtered.filter((n: Notification) => n.source === source);
}
setNotifications(filtered);
}
} 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]);
// Use unified refresh system for notification count
// Note: Widgets update the count via /api/notifications/update
// This polling is a fallback to ensure count is refreshed periodically
const { refresh: refreshCount } = useUnifiedRefresh({
resource: 'notifications-count',
interval: REFRESH_INTERVALS.NOTIFICATIONS_COUNT,
enabled: status === 'authenticated',
onRefresh: async () => {
await fetchNotificationCount(false); // Use cache, widgets update it
},
priority: 'high',
});
// Listen for custom events to trigger immediate refresh
useEffect(() => {
if (status !== 'authenticated') return;
const handleNotificationUpdate = (event: CustomEvent) => {
console.log('[useNotifications] Received notification update event', event.detail);
// Refresh count immediately when widget updates
// Use a small delay to ensure the registry has been updated
setTimeout(() => {
fetchNotificationCount(false); // Use cache, widget already updated it
}, 100);
};
// Listen for custom event from widgets
window.addEventListener('notification-updated', handleNotificationUpdate as EventListener);
return () => {
window.removeEventListener('notification-updated', handleNotificationUpdate as EventListener);
};
}, [status, fetchNotificationCount]);
// Initialize fetching on component mount and cleanup on unmount
useEffect(() => {
isMountedRef.current = true;
if (status === 'authenticated' && session?.user) {
// Initial fetches - use cache, widgets will update it
fetchNotificationCount(false);
fetchNotifications();
}
return () => {
isMountedRef.current = false;
};
}, [status, session?.user, fetchNotificationCount, fetchNotifications]);
return {
notifications,
notificationCount,
loading,
error,
fetchNotifications,
fetchNotificationCount: () => fetchNotificationCount(true),
markAsRead,
markAllAsRead,
};
}