Files
fitaiProto/apps/mobile/src/contexts/NotificationsContext.tsx
T

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;
}