247 lines
8.2 KiB
TypeScript
247 lines
8.2 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
|
|
fetchNotificationCount(false); // Use cache, widget already updated it
|
|
};
|
|
|
|
// 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,
|
|
};
|
|
}
|