import { useEffect, useRef, useCallback } from 'react'; import { toast } from 'sonner'; import { Check, X, Bell, UserPlus } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { useNotifications } from '@/hooks/useNotifications'; import { useConnections } from '@/hooks/useConnections'; import axios from 'axios'; import { getErrorMessage } from '@/lib/api'; import type { AppNotification } from '@/types'; export default function NotificationToaster() { const { notifications, unreadCount, markRead } = useNotifications(); const { respond } = useConnections(); const queryClient = useQueryClient(); const maxSeenIdRef = useRef(0); const initializedRef = useRef(false); const prevUnreadRef = useRef(0); // Track in-flight request IDs so repeated clicks are blocked const respondingRef = useRef>(new Set()); // Always call the latest respond — Sonner toasts capture closures at creation time const respondRef = useRef(respond); respondRef.current = respond; const markReadRef = useRef(markRead); markReadRef.current = markRead; const handleConnectionRespond = useCallback( async (requestId: number, action: 'accept' | 'reject', toastId: string | number, notificationId: number) => { // Guard against double-clicks (Sonner toasts are static, no disabled prop) if (respondingRef.current.has(requestId)) return; respondingRef.current.add(requestId); // Immediately dismiss the custom toast and show a loading indicator toast.dismiss(toastId); const loadingId = toast.loading( action === 'accept' ? 'Accepting connection…' : 'Declining request…', ); try { await respondRef.current({ requestId, action }); toast.dismiss(loadingId); toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined'); markReadRef.current([notificationId]).catch(() => {}); } catch (err) { toast.dismiss(loadingId); // 409 means the request was already resolved (e.g. accepted via notification center) if (axios.isAxiosError(err) && err.response?.status === 409) { toast.success(action === 'accept' ? 'Connection already accepted' : 'Request already resolved'); markReadRef.current([notificationId]).catch(() => {}); } else { toast.error(getErrorMessage(err, 'Failed to respond to request')); } } finally { respondingRef.current.delete(requestId); } }, [], ); // Track unread count changes to force-refetch the list useEffect(() => { if (unreadCount > prevUnreadRef.current && initializedRef.current) { queryClient.invalidateQueries({ queryKey: ['notifications', 'list'] }); } prevUnreadRef.current = unreadCount; }, [unreadCount, queryClient]); // Show toasts for new notifications (ID > max seen) useEffect(() => { if (!notifications.length) return; // On first load, record the max ID without toasting if (!initializedRef.current) { maxSeenIdRef.current = Math.max(...notifications.map((n) => n.id)); initializedRef.current = true; return; } // Find unread notifications with IDs higher than our watermark const newNotifications = notifications.filter( (n) => !n.is_read && n.id > maxSeenIdRef.current, ); // Advance watermark const maxCurrent = Math.max(...notifications.map((n) => n.id)); if (maxCurrent > maxSeenIdRef.current) { maxSeenIdRef.current = maxCurrent; } // Eagerly refresh incoming requests when connection_request notifications arrive // so accept buttons work immediately on NotificationsPage / PeoplePage if (newNotifications.some((n) => n.type === 'connection_request')) { queryClient.invalidateQueries({ queryKey: ['connections', 'incoming'] }); } // Show toasts newNotifications.forEach((notification) => { if (notification.type === 'connection_request' && notification.source_id) { showConnectionRequestToast(notification); } else { toast(notification.title || 'New Notification', { description: notification.message || undefined, icon: , duration: 8000, }); } }); }, [notifications, handleConnectionRespond]); const showConnectionRequestToast = (notification: AppNotification) => { const requestId = notification.source_id!; toast.custom( (id) => (

Connection Request

{notification.message || 'Someone wants to connect with you'}

), { id: `connection-request-${requestId}`, duration: 30000 }, ); }; return null; }