Files
lidify/frontend/hooks/useNotifications.ts
2025-12-25 18:58:06 -06:00

235 lines
7.2 KiB
TypeScript

"use client";
import { useCallback } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
export interface Notification {
id: string;
userId: string;
type: string;
title: string;
message?: string;
metadata?: Record<string, unknown>;
read: boolean;
cleared: boolean;
createdAt: string;
}
export interface DownloadHistoryItem {
id: string;
subject: string;
type: string;
status: string;
error?: string;
createdAt: string;
updatedAt: string;
completedAt?: string;
metadata?: Record<string, unknown>;
}
/**
* Hook for managing notifications using React Query as single source of truth.
* All components using this hook share the same cache and update together.
*/
export function useNotifications() {
const queryClient = useQueryClient();
// Single source of truth - React Query cache
const {
data: notifications = [],
isLoading,
error,
refetch,
} = useQuery<Notification[]>({
queryKey: ["notifications"],
queryFn: () => api.get<Notification[]>("/notifications"),
refetchInterval: 30000,
});
// Derive unread count from data (computed, not stored)
const unreadCount = notifications.filter((n) => !n.read).length;
// Mark as read mutation with optimistic update
const markAsReadMutation = useMutation({
mutationFn: (id: string) => api.post(`/notifications/${id}/read`),
onMutate: async (id: string) => {
await queryClient.cancelQueries({ queryKey: ["notifications"] });
const previous = queryClient.getQueryData<Notification[]>(["notifications"]);
queryClient.setQueryData<Notification[]>(["notifications"], (old) =>
old?.map((n) => (n.id === id ? { ...n, read: true } : n)) || []
);
return { previous };
},
onError: (_err, _id, context) => {
if (context?.previous) {
queryClient.setQueryData(["notifications"], context.previous);
}
},
});
// Mark all as read mutation with optimistic update
const markAllAsReadMutation = useMutation({
mutationFn: () => api.post("/notifications/read-all"),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ["notifications"] });
const previous = queryClient.getQueryData<Notification[]>(["notifications"]);
queryClient.setQueryData<Notification[]>(["notifications"], (old) =>
old?.map((n) => ({ ...n, read: true })) || []
);
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(["notifications"], context.previous);
}
},
});
// Clear notification mutation with optimistic update
const clearMutation = useMutation({
mutationFn: (id: string) => api.post(`/notifications/${id}/clear`),
onMutate: async (id: string) => {
await queryClient.cancelQueries({ queryKey: ["notifications"] });
const previous = queryClient.getQueryData<Notification[]>(["notifications"]);
queryClient.setQueryData<Notification[]>(["notifications"], (old) =>
old?.filter((n) => n.id !== id) || []
);
return { previous };
},
onError: (_err, _id, context) => {
if (context?.previous) {
queryClient.setQueryData(["notifications"], context.previous);
}
},
});
// Clear all mutation with optimistic update
const clearAllMutation = useMutation({
mutationFn: () => api.post("/notifications/clear-all"),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ["notifications"] });
const previous = queryClient.getQueryData<Notification[]>(["notifications"]);
queryClient.setQueryData<Notification[]>(["notifications"], []);
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(["notifications"], context.previous);
}
},
});
return {
notifications,
unreadCount,
isLoading,
error: error instanceof Error ? error.message : null,
refetch,
markAsRead: (id: string) => markAsReadMutation.mutate(id),
markAllAsRead: () => markAllAsReadMutation.mutate(),
clearNotification: (id: string) => clearMutation.mutate(id),
clearAll: () => clearAllMutation.mutate(),
};
}
/**
* Hook for download history - unchanged from original
*/
export function useDownloadHistory() {
const fetchHistory = useCallback(async () => {
return api.get<DownloadHistoryItem[]>("/notifications/downloads/history");
}, []);
const {
data: history = [],
isLoading,
error,
refetch,
} = useQuery<DownloadHistoryItem[]>({
queryKey: ["download-history"],
queryFn: fetchHistory,
refetchInterval: 10000,
});
const queryClient = useQueryClient();
const clearDownload = useCallback(async (id: string) => {
try {
await api.post(`/notifications/downloads/${id}/clear`);
queryClient.setQueryData<DownloadHistoryItem[]>(
["download-history"],
(old) => old?.filter((d) => d.id !== id) || []
);
} catch (err: unknown) {
console.error("Failed to clear download:", err);
}
}, [queryClient]);
const clearAll = useCallback(async () => {
try {
await api.post("/notifications/downloads/clear-all");
queryClient.setQueryData<DownloadHistoryItem[]>(["download-history"], []);
} catch (err: unknown) {
console.error("Failed to clear all:", err);
}
}, [queryClient]);
const retryDownload = useCallback(async (id: string) => {
try {
await api.post(`/notifications/downloads/${id}/retry`);
queryClient.setQueryData<DownloadHistoryItem[]>(
["download-history"],
(old) => old?.filter((d) => d.id !== id) || []
);
} catch (err: unknown) {
console.error("Failed to retry download:", err);
}
}, [queryClient]);
return {
history,
isLoading,
error: error instanceof Error ? error.message : null,
refetch,
clearDownload,
clearAll,
retryDownload,
};
}
/**
* Hook for active downloads - unchanged from original
*/
export function useActiveDownloads() {
const fetchDownloads = useCallback(async () => {
return api.get<DownloadHistoryItem[]>("/notifications/downloads/active");
}, []);
const {
data: downloads = [],
isLoading,
error,
refetch,
} = useQuery<DownloadHistoryItem[]>({
queryKey: ["active-downloads"],
queryFn: fetchDownloads,
refetchInterval: 3000,
});
return {
downloads,
isLoading,
error: error instanceof Error ? error.message : null,
refetch,
};
}