notifications
This commit is contained in:
parent
abb3a4ba0e
commit
8f523d65c0
@ -19,7 +19,10 @@ export async function GET(request: Request) {
|
|||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
const counts = await notificationService.getNotificationCount(userId);
|
const counts = await notificationService.getNotificationCount(userId);
|
||||||
|
|
||||||
return NextResponse.json(counts);
|
// Add Cache-Control header to help with client-side caching
|
||||||
|
const response = NextResponse.json(counts);
|
||||||
|
response.headers.set('Cache-Control', 'private, max-age=10'); // Cache for 10 seconds on client
|
||||||
|
return response;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error in notification count API:', error);
|
console.error('Error in notification count API:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -38,12 +38,15 @@ export async function GET(request: Request) {
|
|||||||
const notificationService = NotificationService.getInstance();
|
const notificationService = NotificationService.getInstance();
|
||||||
const notifications = await notificationService.getNotifications(userId, page, limit);
|
const notifications = await notificationService.getNotifications(userId, page, limit);
|
||||||
|
|
||||||
return NextResponse.json({
|
// Add Cache-Control header to help with client-side caching
|
||||||
|
const response = NextResponse.json({
|
||||||
notifications,
|
notifications,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
total: notifications.length
|
total: notifications.length
|
||||||
});
|
});
|
||||||
|
response.headers.set('Cache-Control', 'private, max-age=30'); // Cache for 30 seconds on client
|
||||||
|
return response;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error in notifications API:', error);
|
console.error('Error in notifications API:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Bell } from 'lucide-react';
|
import { Bell } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@ -8,7 +8,8 @@ interface NotificationBadgeProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationBadge({ className }: NotificationBadgeProps) {
|
// Use React.memo to prevent unnecessary re-renders
|
||||||
|
export const NotificationBadge = memo(function NotificationBadge({ className }: NotificationBadgeProps) {
|
||||||
const { notificationCount } = useNotifications();
|
const { notificationCount } = useNotifications();
|
||||||
const hasUnread = notificationCount.unread > 0;
|
const hasUnread = notificationCount.unread > 0;
|
||||||
|
|
||||||
@ -32,4 +33,4 @@ export function NotificationBadge({ className }: NotificationBadgeProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { Notification, NotificationCount } from '@/lib/types/notification';
|
import { Notification, NotificationCount } from '@/lib/types/notification';
|
||||||
|
|
||||||
@ -9,20 +9,47 @@ const defaultNotificationCount: NotificationCount = {
|
|||||||
sources: {}
|
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() {
|
export function useNotifications() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount);
|
const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(null);
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const lastFetchTimeRef = useRef<number>(0);
|
||||||
|
const isMountedRef = useRef<boolean>(false);
|
||||||
|
const isPollingRef = useRef<boolean>(false);
|
||||||
|
|
||||||
// Fetch notification count
|
// Minimum time between fetches (in milliseconds)
|
||||||
const fetchNotificationCount = useCallback(async () => {
|
const MIN_FETCH_INTERVAL = 5000; // 5 seconds
|
||||||
if (!session?.user) return;
|
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 {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
lastFetchTimeRef.current = now;
|
||||||
|
|
||||||
const response = await fetch('/api/notifications/count');
|
const response = await fetch('/api/notifications/count');
|
||||||
|
|
||||||
@ -34,19 +61,34 @@ export function useNotifications() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (isMountedRef.current) {
|
||||||
setNotificationCount(data);
|
setNotificationCount(data);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching notification count:', err);
|
console.error('Error fetching notification count:', err);
|
||||||
setError('Failed to fetch notification count');
|
setError('Failed to fetch notification count');
|
||||||
}
|
}
|
||||||
}, [session?.user]);
|
}, [session?.user]);
|
||||||
|
|
||||||
|
// Debounced version to prevent rapid successive calls
|
||||||
|
const debouncedFetchCount = useCallback(
|
||||||
|
debounce(fetchNotificationCount, 300),
|
||||||
|
[fetchNotificationCount]
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch notifications
|
// Fetch notifications
|
||||||
const fetchNotifications = useCallback(async (page = 1, limit = 20) => {
|
const fetchNotifications = useCallback(async (page = 1, limit = 20) => {
|
||||||
if (!session?.user) return;
|
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);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
lastFetchTimeRef.current = now;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`);
|
const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`);
|
||||||
@ -59,18 +101,22 @@ export function useNotifications() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (isMountedRef.current) {
|
||||||
setNotifications(data.notifications);
|
setNotifications(data.notifications);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching notifications:', err);
|
console.error('Error fetching notifications:', err);
|
||||||
setError('Failed to fetch notifications');
|
setError('Failed to fetch notifications');
|
||||||
} finally {
|
} finally {
|
||||||
|
if (isMountedRef.current) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [session?.user]);
|
}, [session?.user]);
|
||||||
|
|
||||||
// Mark notification as read
|
// Mark notification as read
|
||||||
const markAsRead = useCallback(async (notificationId: string) => {
|
const markAsRead = useCallback(async (notificationId: string) => {
|
||||||
if (!session?.user) return;
|
if (!session?.user) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/notifications/${notificationId}/read`, {
|
const response = await fetch(`/api/notifications/${notificationId}/read`, {
|
||||||
@ -96,18 +142,18 @@ export function useNotifications() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Refresh notification count
|
// Refresh notification count
|
||||||
fetchNotificationCount();
|
debouncedFetchCount(true);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error marking notification as read:', err);
|
console.error('Error marking notification as read:', err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [session?.user, fetchNotificationCount]);
|
}, [session?.user, debouncedFetchCount]);
|
||||||
|
|
||||||
// Mark all notifications as read
|
// Mark all notifications as read
|
||||||
const markAllAsRead = useCallback(async () => {
|
const markAllAsRead = useCallback(async () => {
|
||||||
if (!session?.user) return;
|
if (!session?.user) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/notifications/read-all', {
|
const response = await fetch('/api/notifications/read-all', {
|
||||||
@ -129,55 +175,62 @@ export function useNotifications() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Refresh notification count
|
// Refresh notification count
|
||||||
fetchNotificationCount();
|
debouncedFetchCount(true);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error marking all notifications as read:', err);
|
console.error('Error marking all notifications as read:', err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [session?.user, fetchNotificationCount]);
|
}, [session?.user, debouncedFetchCount]);
|
||||||
|
|
||||||
// Start polling for notification count
|
// Start polling for notification count
|
||||||
const startPolling = useCallback((interval = 30000) => {
|
const startPolling = useCallback(() => {
|
||||||
if (pollingInterval) {
|
if (isPollingRef.current) return;
|
||||||
clearInterval(pollingInterval);
|
|
||||||
|
isPollingRef.current = true;
|
||||||
|
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = setInterval(() => {
|
// Ensure we don't create multiple intervals
|
||||||
fetchNotificationCount();
|
pollingIntervalRef.current = setInterval(() => {
|
||||||
}, interval);
|
if (isMountedRef.current) {
|
||||||
|
debouncedFetchCount();
|
||||||
|
}
|
||||||
|
}, POLLING_INTERVAL);
|
||||||
|
|
||||||
setPollingInterval(id);
|
return () => stopPolling();
|
||||||
|
}, [debouncedFetchCount]);
|
||||||
return () => {
|
|
||||||
clearInterval(id);
|
|
||||||
setPollingInterval(null);
|
|
||||||
};
|
|
||||||
}, [fetchNotificationCount, pollingInterval]);
|
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
const stopPolling = useCallback(() => {
|
const stopPolling = useCallback(() => {
|
||||||
if (pollingInterval) {
|
if (pollingIntervalRef.current) {
|
||||||
clearInterval(pollingInterval);
|
clearInterval(pollingIntervalRef.current);
|
||||||
setPollingInterval(null);
|
pollingIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
}, [pollingInterval]);
|
isPollingRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Initialize fetching on component mount
|
// Initialize fetching on component mount and cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
if (status === 'authenticated' && session?.user) {
|
if (status === 'authenticated' && session?.user) {
|
||||||
fetchNotificationCount();
|
// Initial fetches
|
||||||
|
fetchNotificationCount(true);
|
||||||
fetchNotifications();
|
fetchNotifications();
|
||||||
|
|
||||||
// Start polling
|
// Start polling
|
||||||
const cleanup = startPolling();
|
startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cleanup();
|
isMountedRef.current = false;
|
||||||
|
stopPolling();
|
||||||
};
|
};
|
||||||
}
|
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]);
|
||||||
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
@ -185,10 +238,8 @@ export function useNotifications() {
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
fetchNotifications,
|
fetchNotifications,
|
||||||
fetchNotificationCount,
|
fetchNotificationCount: () => debouncedFetchCount(true),
|
||||||
markAsRead,
|
markAsRead,
|
||||||
markAllAsRead,
|
markAllAsRead
|
||||||
startPolling,
|
|
||||||
stopPolling
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -278,8 +278,27 @@ export class NotificationService {
|
|||||||
// Use setTimeout to make this non-blocking
|
// Use setTimeout to make this non-blocking
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
|
// Check if we've refreshed recently (within the last minute)
|
||||||
|
// to avoid excessive refreshes from multiple tabs/components
|
||||||
|
const refreshKey = `notifications:last_refresh:${userId}`;
|
||||||
|
const lastRefresh = await redis.get(refreshKey);
|
||||||
|
|
||||||
|
if (lastRefresh) {
|
||||||
|
const lastRefreshTime = parseInt(lastRefresh, 10);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// If refreshed in the last minute, skip
|
||||||
|
if (now - lastRefreshTime < 60000) {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Skipping background refresh for user ${userId} - refreshed recently`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[NOTIFICATION_SERVICE] Background refresh started for user ${userId}`);
|
console.log(`[NOTIFICATION_SERVICE] Background refresh started for user ${userId}`);
|
||||||
|
|
||||||
|
// Set last refresh time
|
||||||
|
await redis.set(refreshKey, Date.now().toString(), 'EX', 120); // 2 minute TTL
|
||||||
|
|
||||||
// Refresh counts and notifications (for first page)
|
// Refresh counts and notifications (for first page)
|
||||||
await this.getNotificationCount(userId);
|
await this.getNotificationCount(userId);
|
||||||
await this.getNotifications(userId, 1, 20);
|
await this.getNotifications(userId, 1, 20);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user