import React, { createContext, useContext, useState, useEffect, useCallback, useRef, } from "react"; import { useAuth, useUser } from "@clerk/clerk-expo"; import { fetchNotifications, fetchUnreadCount, markAsRead, markAllAsRead, deleteNotification, type Notification, } from "../api/notifications"; import log from "../utils/logger"; interface NotificationsContextType { notifications: Notification[]; unreadCount: number; loading: boolean; refetchNotifications: () => Promise; markNotificationAsRead: (id: string) => Promise; markAllAsReadAction: () => Promise; deleteNotificationAction: (id: string) => Promise; } const NotificationsContext = createContext< NotificationsContextType | undefined >(undefined); export function NotificationsProvider({ children, }: { children: React.ReactNode; }) { const { getToken, isSignedIn } = useAuth(); const { user } = useUser(); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [loading, setLoading] = useState(false); const fetchInProgressRef = useRef(false); const lastFetchTimeRef = useRef(0); const refetchNotifications = useCallback(async () => { if (!isSignedIn) return; const now = Date.now(); const timeSinceLastFetch = now - lastFetchTimeRef.current; // Prevent duplicate concurrent fetches if (fetchInProgressRef.current) { log.debug("Skipping duplicate notification fetch (already in progress)"); return; } // Debounce: prevent fetches within 1 second of the last fetch if (timeSinceLastFetch < 1000) { log.debug("Skipping duplicate notification fetch (debounced)", { timeSinceLastFetch: `${timeSinceLastFetch}ms`, }); return; } try { fetchInProgressRef.current = true; lastFetchTimeRef.current = now; setLoading(true); const token = await getToken(); // Fetch notifications and unread count in parallel const [notificationsData, count] = await Promise.all([ fetchNotifications(token), fetchUnreadCount(token), ]); setNotifications(notificationsData); setUnreadCount(count); log.debug("Notifications fetched", { total: notificationsData.length, unread: count, }); } catch (error) { log.error("Failed to fetch notifications", error); } finally { setLoading(false); fetchInProgressRef.current = false; } }, [getToken, isSignedIn]); const markNotificationAsRead = useCallback( async (id: string) => { try { const token = await getToken(); await markAsRead(id, token); // Update local state setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)), ); setUnreadCount((prev) => Math.max(0, prev - 1)); log.debug("Notification marked as read", { id }); } catch (error) { log.error("Failed to mark notification as read", error); throw error; } }, [getToken], ); const markAllAsReadAction = useCallback(async () => { try { const token = await getToken(); await markAllAsRead(token); // Update local state setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); setUnreadCount(0); log.debug("All notifications marked as read"); } catch (error) { log.error("Failed to mark all notifications as read", error); throw error; } }, [getToken]); const deleteNotificationAction = useCallback( async (id: string) => { try { const token = await getToken(); await deleteNotification(id, token); // Update local state const wasUnread = notifications.find((n) => n.id === id)?.read === false; setNotifications((prev) => prev.filter((n) => n.id !== id)); if (wasUnread) { setUnreadCount((prev) => Math.max(0, prev - 1)); } log.debug("Notification deleted", { id }); } catch (error) { log.error("Failed to delete notification", error); throw error; } }, [getToken, notifications], ); // Initial fetch on mount useEffect(() => { if (isSignedIn) { refetchNotifications(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSignedIn]); // Only run when sign-in state changes useEffect(() => { setNotifications([]); setUnreadCount(0); setLoading(false); fetchInProgressRef.current = false; lastFetchTimeRef.current = 0; if (user?.id) { log.debug("Notifications state reset for user", { userId: user.id }); } else { log.debug("Notifications state reset on sign-out"); } }, [user?.id]); // Periodic refresh every 30 seconds useEffect(() => { if (!isSignedIn) return; const intervalId = setInterval(() => { refetchNotifications(); }, 30000); return () => clearInterval(intervalId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSignedIn]); // Only run when sign-in state changes const value: NotificationsContextType = { notifications, unreadCount, loading, refetchNotifications, markNotificationAsRead, markAllAsReadAction, deleteNotificationAction, }; return ( {children} ); } export function useNotifications() { const context = useContext(NotificationsContext); if (context === undefined) { throw new Error( "useNotifications must be used within a NotificationsProvider", ); } return context; }