215 lines
5.6 KiB
TypeScript
215 lines
5.6 KiB
TypeScript
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<void>;
|
|
markNotificationAsRead: (id: string) => Promise<void>;
|
|
markAllAsReadAction: () => Promise<void>;
|
|
deleteNotificationAction: (id: string) => Promise<void>;
|
|
}
|
|
|
|
const NotificationsContext = createContext<
|
|
NotificationsContextType | undefined
|
|
>(undefined);
|
|
|
|
export function NotificationsProvider({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const { getToken, isSignedIn } = useAuth();
|
|
const { user } = useUser();
|
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
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 (
|
|
<NotificationsContext.Provider value={value}>
|
|
{children}
|
|
</NotificationsContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useNotifications() {
|
|
const context = useContext(NotificationsContext);
|
|
if (context === undefined) {
|
|
throw new Error(
|
|
"useNotifications must be used within a NotificationsProvider",
|
|
);
|
|
}
|
|
return context;
|
|
}
|