import { useState, useCallback, useRef, useEffect } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useToast } from "@/hooks/use-toast"; import type { User } from "@supabase/supabase-js"; import { useEntityCache, useProfileCache, useModerationFilters, useModerationSort, usePagination, useRealtimeSubscriptions, } from "./index"; import { useModerationQueue } from "@/hooks/useModerationQueue"; import { smartMergeArray } from "@/lib/smartStateUpdate"; import type { ModerationItem, EntityFilter, StatusFilter, LoadingState, SortConfig } from "@/types/moderation"; /** * Configuration for useModerationQueueManager */ export interface ModerationQueueManagerConfig { user: User | null; isAdmin: boolean; isSuperuser: boolean; toast: ReturnType["toast"]; settings: { refreshMode: "auto" | "manual"; pollInterval: number; refreshStrategy: "notify" | "merge" | "replace"; preserveInteraction: boolean; useRealtimeQueue: boolean; refreshOnTabVisible: boolean; }; } /** * Return type for useModerationQueueManager */ export interface ModerationQueueManager { // State items: ModerationItem[]; loadingState: LoadingState; actionLoading: string | null; // Sub-hooks (exposed for granular control) filters: ReturnType; pagination: ReturnType; sort: ReturnType; queue: ReturnType; // Realtime newItemsCount: number; pendingNewItems: ModerationItem[]; showNewItems: () => void; // Interaction tracking interactingWith: Set; markInteracting: (id: string, interacting: boolean) => void; // Actions refresh: () => void; performAction: (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => Promise; deleteSubmission: (item: ModerationItem) => Promise; resetToPending: (item: ModerationItem) => Promise; retryFailedItems: (item: ModerationItem) => Promise; // Caches (for QueueItem enrichment) entityCache: ReturnType; profileCache: ReturnType; } /** * Orchestrator hook for moderation queue management * Consolidates all queue-related logic into a single hook */ export function useModerationQueueManager(config: ModerationQueueManagerConfig): ModerationQueueManager { const { user, isAdmin, isSuperuser, toast, settings } = config; // Initialize sub-hooks const filters = useModerationFilters({ initialEntityFilter: "all", initialStatusFilter: "pending", initialTab: "mainQueue", debounceDelay: 300, persist: true, storageKey: "moderationQueue_filters", }); const pagination = usePagination({ initialPage: 1, initialPageSize: 25, persist: false, onPageChange: (page) => { if (page > 1) { setLoadingState("loading"); } }, onPageSizeChange: () => { setLoadingState("loading"); }, }); const sort = useModerationSort({ initialConfig: { field: "created_at", direction: "asc" }, persist: true, storageKey: "moderationQueue_sortConfig", }); const queue = useModerationQueue(); const entityCache = useEntityCache(); const profileCache = useProfileCache(); // Core state const [items, setItems] = useState([]); const [loadingState, setLoadingState] = useState("initial"); const [actionLoading, setActionLoading] = useState(null); const [interactingWith, setInteractingWith] = useState>(new Set()); const [pendingNewItems, setPendingNewItems] = useState([]); const [newItemsCount, setNewItemsCount] = useState(0); // Refs for tracking const recentlyRemovedRef = useRef>(new Set()); const fetchInProgressRef = useRef(false); const itemsRef = useRef([]); const lastFetchTimeRef = useRef(0); const pauseFetchingRef = useRef(false); const initialFetchCompleteRef = useRef(false); const isMountingRef = useRef(true); const fetchItemsRef = useRef<((silent?: boolean) => Promise) | null>(null); const FETCH_COOLDOWN_MS = 1000; // Store settings in refs to avoid re-creating fetchItems const settingsRef = useRef(settings); useEffect(() => { settingsRef.current = settings; }, [settings]); // Sync items with ref useEffect(() => { itemsRef.current = items; }, [items]); /** * Fetch queue items from database */ const fetchItems = useCallback( async (silent = false) => { if (!user) return; // Get caller info const callerStack = new Error().stack; const callerLine = callerStack?.split("\n")[2]?.trim(); console.log("πŸ”„ [FETCH ITEMS] Called", { silent, pauseFetchingRef: pauseFetchingRef.current, documentHidden: document.hidden, caller: callerLine, timestamp: new Date().toISOString(), }); // Check if fetching is paused (controlled by visibility handler if enabled) if (pauseFetchingRef.current) { console.log("⏸️ Fetch paused by pauseFetchingRef"); return; } // Prevent concurrent calls if (fetchInProgressRef.current) { console.log("⚠️ Fetch already in progress, skipping"); return; } // Cooldown check const now = Date.now(); const timeSinceLastFetch = now - lastFetchTimeRef.current; if (timeSinceLastFetch < FETCH_COOLDOWN_MS && lastFetchTimeRef.current > 0) { console.log(`⏸️ Fetch cooldown active (${timeSinceLastFetch}ms)`); return; } fetchInProgressRef.current = true; lastFetchTimeRef.current = now; console.log("πŸ” fetchItems called:", { entityFilter: filters.debouncedEntityFilter, statusFilter: filters.debouncedStatusFilter, silent, timestamp: new Date().toISOString(), }); try { // Set loading states if (!silent) { setLoadingState("loading"); } else { setLoadingState("refreshing"); } // Build base query let submissionsQuery = supabase .from("content_submissions") .select( ` id, submission_type, status, content, created_at, user_id, reviewed_at, reviewer_id, reviewer_notes, escalated, assigned_to, locked_until, submission_items ( id, item_type, item_data, status ) `, ); // Validate sort field is an actual column in content_submissions const validSortFields = ['created_at', 'submission_type', 'status', 'escalated', 'submitted_at']; let sortField = sort.config.field; if (!validSortFields.includes(sortField)) { console.warn('[Query] Invalid sort field:', sortField, '- falling back to created_at'); sortField = 'created_at'; } console.log('[Query] Sorting by:', { field: sortField, direction: sort.config.direction, ascending: sort.config.direction === 'asc' }); // Apply sorting: escalated first (desc), then user's chosen field submissionsQuery = submissionsQuery .order("escalated", { ascending: false }) .order(sortField, { ascending: sort.config.direction === 'asc' }); // Apply tab-based status filtering const tab = filters.activeTab; const statusFilter = filters.debouncedStatusFilter; const entityFilter = filters.debouncedEntityFilter; if (tab === "mainQueue") { if (statusFilter === "all") { submissionsQuery = submissionsQuery.in("status", ["pending", "flagged", "partially_approved"]); } else if (statusFilter === "pending") { submissionsQuery = submissionsQuery.in("status", ["pending", "partially_approved"]); } else { submissionsQuery = submissionsQuery.eq("status", statusFilter); } } else { if (statusFilter === "all") { submissionsQuery = submissionsQuery.in("status", ["approved", "rejected"]); } else { submissionsQuery = submissionsQuery.eq("status", statusFilter); } } // Apply entity type filter if (entityFilter === "photos") { submissionsQuery = submissionsQuery.eq("submission_type", "photo"); } else if (entityFilter === "submissions") { submissionsQuery = submissionsQuery.neq("submission_type", "photo"); } // Apply access control if (!isAdmin && !isSuperuser) { const now = new Date().toISOString(); submissionsQuery = submissionsQuery.or( `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`, ); } // Get total count - rebuild query with same filters let countQuery = supabase .from("content_submissions") .select("*", { count: "exact", head: true }); // Apply the exact same filters as the main query if (tab === "mainQueue") { if (statusFilter === "all") { countQuery = countQuery.in("status", ["pending", "flagged", "partially_approved"]); } else if (statusFilter === "pending") { countQuery = countQuery.in("status", ["pending", "partially_approved"]); } else { countQuery = countQuery.eq("status", statusFilter); } } else { if (statusFilter === "all") { countQuery = countQuery.in("status", ["approved", "rejected"]); } else { countQuery = countQuery.eq("status", statusFilter); } } // Apply entity type filter if (entityFilter === "photos") { countQuery = countQuery.eq("submission_type", "photo"); } else if (entityFilter === "submissions") { countQuery = countQuery.neq("submission_type", "photo"); } // Apply access control if (!isAdmin && !isSuperuser) { const now = new Date().toISOString(); countQuery = countQuery.or( `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`, ); } const { count } = await countQuery; pagination.setTotalCount(count || 0); // Apply pagination const startIndex = pagination.startIndex; const endIndex = pagination.endIndex; submissionsQuery = submissionsQuery.range(startIndex, endIndex); const { data: submissions, error: submissionsError } = await submissionsQuery; if (submissionsError) throw submissionsError; // Fetch related profiles and entities const userIds = [ ...new Set([ ...(submissions?.map((s) => s.user_id) || []), ...(submissions?.map((s) => s.reviewer_id).filter(Boolean) || []), ]), ]; if (userIds.length > 0) { await profileCache.bulkFetch(userIds); } // Collect entity IDs if (submissions) { await entityCache.fetchRelatedEntities(submissions); } // Map to ModerationItems const moderationItems: ModerationItem[] = submissions?.map((submission) => { const content = submission.content as any; let entityName = content?.name || "Unknown"; let parkName: string | undefined; // Resolve entity names from cache if (submission.submission_type === "ride" && content?.entity_id) { const ride = entityCache.getCached("rides", content.entity_id); if (ride) { entityName = ride.name; if (ride.park_id) { const park = entityCache.getCached("parks", ride.park_id); if (park) parkName = park.name; } } } else if (submission.submission_type === "park" && content?.entity_id) { const park = entityCache.getCached("parks", content.entity_id); if (park) entityName = park.name; } else if ( ["manufacturer", "operator", "designer", "property_owner"].includes(submission.submission_type) && content?.entity_id ) { const company = entityCache.getCached("companies", content.entity_id); if (company) entityName = company.name; } const userProfile = profileCache.getCached(submission.user_id); const reviewerProfile = submission.reviewer_id ? profileCache.getCached(submission.reviewer_id) : undefined; return { id: submission.id, type: "content_submission", content: submission.content, created_at: submission.created_at, user_id: submission.user_id, status: submission.status, submission_type: submission.submission_type, user_profile: userProfile, entity_name: entityName, park_name: parkName, reviewed_at: submission.reviewed_at || undefined, reviewed_by: submission.reviewer_id || undefined, reviewer_notes: submission.reviewer_notes || undefined, reviewer_profile: reviewerProfile, escalated: submission.escalated, assigned_to: submission.assigned_to || undefined, locked_until: submission.locked_until || undefined, submission_items: submission.submission_items || undefined, }; }) || []; // Apply refresh strategy const currentRefreshStrategy = settingsRef.current.refreshStrategy; const currentPreserveInteraction = settingsRef.current.preserveInteraction; if (silent) { // Background polling: detect new submissions const currentDisplayedIds = new Set(itemsRef.current.map((item) => item.id)); const newSubmissions = moderationItems.filter((item) => !currentDisplayedIds.has(item.id)); if (newSubmissions.length > 0) { console.log("πŸ†• Detected new submissions:", newSubmissions.length); setPendingNewItems((prev) => { const existingIds = new Set(prev.map((p) => p.id)); const uniqueNew = newSubmissions.filter((item) => !existingIds.has(item.id)); if (uniqueNew.length > 0) { setNewItemsCount((prev) => prev + uniqueNew.length); } return [...prev, ...uniqueNew]; }); } // Apply refresh strategy switch (currentRefreshStrategy) { case "notify": console.log("βœ… Queue frozen (notify mode)"); break; case "merge": if (newSubmissions.length > 0) { const currentIds = new Set(itemsRef.current.map((item) => item.id)); const trulyNewSubmissions = newSubmissions.filter((item) => !currentIds.has(item.id)); if (trulyNewSubmissions.length > 0) { setItems((prev) => [...prev, ...trulyNewSubmissions]); console.log("πŸ”€ Queue merged - added", trulyNewSubmissions.length, "items"); } } break; case "replace": const mergeResult = smartMergeArray(itemsRef.current, moderationItems, { compareFields: [ "status", "content", "reviewed_at", "reviewed_by", "reviewer_notes", "assigned_to", "locked_until", ], preserveOrder: false, addToTop: false, }); if (mergeResult.hasChanges) { setItems(mergeResult.items); console.log("πŸ”„ Queue updated (replace mode)"); } if (!currentPreserveInteraction) { setPendingNewItems([]); setNewItemsCount(0); } break; } } else { // Manual refresh const mergeResult = smartMergeArray(itemsRef.current, moderationItems, { compareFields: [ "status", "content", "reviewed_at", "reviewed_by", "reviewer_notes", "assigned_to", "locked_until", ], preserveOrder: false, addToTop: false, }); if (mergeResult.hasChanges) { setItems(mergeResult.items); console.log("πŸ”„ Queue updated (manual refresh)"); } setPendingNewItems([]); setNewItemsCount(0); } } catch (error: any) { console.error("Error fetching moderation items:", error); toast({ title: "Error", description: error.message || "Failed to fetch moderation queue", variant: "destructive", }); } finally { fetchInProgressRef.current = false; setLoadingState("ready"); } }, [user, isAdmin, isSuperuser, filters, pagination, sort, profileCache, entityCache, toast], ); // Store fetchItems in ref to avoid re-creating visibility listener useEffect(() => { fetchItemsRef.current = fetchItems; }, [fetchItems]); /** * Show pending new items by merging them into the queue */ const showNewItems = useCallback(() => { if (pendingNewItems.length > 0) { setItems((prev) => [...pendingNewItems, ...prev]); setPendingNewItems([]); setNewItemsCount(0); console.log("βœ… New items merged into queue:", pendingNewItems.length); } }, [pendingNewItems]); /** * Mark an item as being interacted with (prevents realtime updates) */ const markInteracting = useCallback((id: string, interacting: boolean) => { setInteractingWith((prev) => { const next = new Set(prev); if (interacting) { next.add(id); } else { next.delete(id); } return next; }); }, []); /** * Perform moderation action (approve/reject) */ const performAction = useCallback( async (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => { if (actionLoading === item.id) return; setActionLoading(item.id); // Optimistic update const shouldRemove = (filters.statusFilter === "pending" || filters.statusFilter === "flagged") && (action === "approved" || action === "rejected"); if (shouldRemove) { setItems((prev) => prev.map((i) => (i.id === item.id ? { ...i, _removing: true } : i))); setTimeout(() => { setItems((prev) => prev.filter((i) => i.id !== item.id)); recentlyRemovedRef.current.add(item.id); setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000); }, 300); } // Release lock if claimed if (queue.currentLock?.submissionId === item.id) { await queue.releaseLock(item.id); } try { // Handle photo submissions if (action === "approved" && item.submission_type === "photo") { const { data: photoSubmission } = await supabase .from("photo_submissions") .select(`*, items:photo_submission_items(*), submission:content_submissions!inner(user_id)`) .eq("submission_id", item.id) .single(); if (photoSubmission && photoSubmission.items) { const { data: existingPhotos } = await supabase.from("photos").select("id").eq("submission_id", item.id); if (!existingPhotos || existingPhotos.length === 0) { const photoRecords = photoSubmission.items.map((photoItem: any) => ({ entity_id: photoSubmission.entity_id, entity_type: photoSubmission.entity_type, cloudflare_image_id: photoItem.cloudflare_image_id, cloudflare_image_url: photoItem.cloudflare_image_url, title: photoItem.title || null, caption: photoItem.caption || null, date_taken: photoItem.date_taken || null, order_index: photoItem.order_index, submission_id: photoSubmission.submission_id, submitted_by: photoSubmission.submission?.user_id, approved_by: user?.id, approved_at: new Date().toISOString(), })); await supabase.from("photos").insert(photoRecords); } } } // Check for submission items const { data: submissionItems } = await supabase .from("submission_items") .select("id, status") .eq("submission_id", item.id) .in("status", ["pending", "rejected"]); if (submissionItems && submissionItems.length > 0) { if (action === "approved") { await supabase.functions.invoke("process-selective-approval", { body: { itemIds: submissionItems.map((i) => i.id), submissionId: item.id, }, }); toast({ title: "Submission Approved", description: `Successfully processed ${submissionItems.length} item(s)`, }); return; } else if (action === "rejected") { await supabase .from("submission_items") .update({ status: "rejected", rejection_reason: moderatorNotes || "Parent submission rejected", updated_at: new Date().toISOString(), }) .eq("submission_id", item.id) .eq("status", "pending"); } } // Standard update const table = item.type === "review" ? "reviews" : "content_submissions"; const statusField = item.type === "review" ? "moderation_status" : "status"; const timestampField = item.type === "review" ? "moderated_at" : "reviewed_at"; const reviewerField = item.type === "review" ? "moderated_by" : "reviewer_id"; const updateData: any = { [statusField]: action, [timestampField]: new Date().toISOString(), }; if (user) { updateData[reviewerField] = user.id; } if (moderatorNotes) { updateData.reviewer_notes = moderatorNotes; } const { error } = await supabase.from(table).update(updateData).eq("id", item.id); if (error) throw error; toast({ title: `Content ${action}`, description: `The ${item.type} has been ${action}`, }); } catch (error: any) { console.error("Error moderating content:", error); // Revert optimistic update setItems((prev) => { const exists = prev.find((i) => i.id === item.id); if (exists) { return prev.map((i) => (i.id === item.id ? item : i)); } else { return [...prev, item]; } }); toast({ title: "Error", description: error.message || `Failed to ${action} content`, variant: "destructive", }); } finally { setActionLoading(null); } }, [actionLoading, filters.statusFilter, queue, user, toast], ); /** * Delete a submission permanently */ const deleteSubmission = useCallback( async (item: ModerationItem) => { if (item.type !== "content_submission") return; if (actionLoading === item.id) return; setActionLoading(item.id); setItems((prev) => prev.filter((i) => i.id !== item.id)); try { const { error } = await supabase.from("content_submissions").delete().eq("id", item.id); if (error) throw error; toast({ title: "Submission deleted", description: "The submission has been permanently deleted", }); } catch (error: any) { console.error("Error deleting submission:", error); setItems((prev) => { if (prev.some((i) => i.id === item.id)) return prev; return [...prev, item]; }); toast({ title: "Error", description: "Failed to delete submission", variant: "destructive", }); } finally { setActionLoading(null); } }, [actionLoading, toast], ); /** * Reset submission to pending status */ const resetToPending = useCallback( async (item: ModerationItem) => { setActionLoading(item.id); try { const { resetRejectedItemsToPending } = await import("@/lib/submissionItemsService"); await resetRejectedItemsToPending(item.id); toast({ title: "Reset Complete", description: "Submission and all items have been reset to pending status", }); setItems((prev) => prev.filter((i) => i.id !== item.id)); } catch (error: any) { console.error("Error resetting submission:", error); toast({ title: "Reset Failed", description: error.message, variant: "destructive", }); } finally { setActionLoading(null); } }, [toast], ); /** * Retry failed items in a submission */ const retryFailedItems = useCallback( async (item: ModerationItem) => { setActionLoading(item.id); const shouldRemove = filters.statusFilter === "pending" || filters.statusFilter === "flagged" || filters.statusFilter === "partially_approved"; if (shouldRemove) { requestAnimationFrame(() => { setItems((prev) => prev.filter((i) => i.id !== item.id)); recentlyRemovedRef.current.add(item.id); setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000); }); } try { const { data: failedItems } = await supabase .from("submission_items") .select("id") .eq("submission_id", item.id) .eq("status", "rejected"); if (!failedItems || failedItems.length === 0) { toast({ title: "No Failed Items", description: "All items have been processed successfully", }); return; } await supabase.functions.invoke("process-selective-approval", { body: { itemIds: failedItems.map((i) => i.id), submissionId: item.id, }, }); toast({ title: "Retry Complete", description: `Processed ${failedItems.length} failed item(s)`, }); } catch (error: any) { console.error("Error retrying failed items:", error); toast({ title: "Retry Failed", description: error.message, variant: "destructive", }); } finally { setActionLoading(null); } }, [filters.statusFilter, toast], ); // Initial fetch on mount useEffect(() => { if (!user) return; isMountingRef.current = true; fetchItems(false).then(() => { initialFetchCompleteRef.current = true; requestAnimationFrame(() => { isMountingRef.current = false; }); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id]); // Filter changes trigger refetch useEffect(() => { if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return; pagination.reset(); fetchItems(true); }, [filters.debouncedEntityFilter, filters.debouncedStatusFilter]); // Sort changes trigger refetch useEffect(() => { if (!user || !initialFetchCompleteRef.current || isMountingRef.current) { return; } console.log('[Sort Change] Refetching with:', sort.field, sort.direction); pagination.reset(); fetchItemsRef.current?.(true); }, [sort.field, sort.direction, user]); // Pagination changes trigger refetch useEffect(() => { if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return; fetchItemsRef.current?.(true); }, [pagination.currentPage, pagination.pageSize]); // Polling effect (when realtime disabled) useEffect(() => { if (!user || settings.refreshMode !== "auto" || loadingState === "initial" || settings.useRealtimeQueue) { return; } console.log("⚠️ Polling ENABLED - interval:", settings.pollInterval); const interval = setInterval(() => { console.log("πŸ”„ Polling refresh triggered"); fetchItemsRef.current?.(true); }, settings.pollInterval); return () => { clearInterval(interval); console.log("πŸ›‘ Polling stopped"); }; }, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue]); // Visibility change handler useEffect(() => { // HARD CHECK: Explicit boolean comparison to prevent any truthy coercion const isEnabled = settings.refreshOnTabVisible === true; console.log("πŸ” [VISIBILITY EFFECT] Hard check", { refreshOnTabVisible: settings.refreshOnTabVisible, typeOf: typeof settings.refreshOnTabVisible, isEnabled, willAttachListener: isEnabled, timestamp: new Date().toISOString(), }); // Early return if feature is disabled if (!isEnabled) { console.log(" βœ… Feature DISABLED - skipping all visibility logic"); console.log(" βœ… Tab focus will NOT trigger refreshes"); // Cleanup: ensure no lingering handlers return () => { console.log(" 🧹 Cleanup: Ensuring no visibility listeners exist"); }; } console.error(" ❌ Setting is TRUE - listener WILL be attached"); console.error(" ❌ THIS MEANS TAB FOCUS **WILL** TRIGGER REFRESHES"); console.error(" ⚠️ If you disabled this setting, it is NOT working properly"); const handleVisibilityChange = () => { // Double-check setting before doing anything (defensive check) if (!settings.refreshOnTabVisible) { console.log("⚠️ Visibility handler called but setting is disabled - ignoring"); return; } if (document.hidden) { console.log("πŸ‘οΈ [VISIBILITY HANDLER] Tab hidden - pausing fetches"); pauseFetchingRef.current = true; } else { console.error("πŸ‘οΈ [VISIBILITY HANDLER] Tab visible - THIS IS WHERE THE REFRESH HAPPENS"); console.error(" πŸ”΄ TAB FOCUS REFRESH TRIGGERED HERE"); console.error(" πŸ“ Stack trace below:"); console.trace(); pauseFetchingRef.current = false; if (initialFetchCompleteRef.current && !isMountingRef.current && fetchItemsRef.current) { console.error(" ➑️ Calling fetchItems(true) NOW"); fetchItemsRef.current(true); } else { console.log(" ⏭️ Skipping refresh (initial fetch not complete or mounting)"); } } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); console.log("🧹 Visibility listener removed"); }; }, [settings.refreshOnTabVisible]); // Initialize realtime subscriptions useRealtimeSubscriptions({ enabled: settings.useRealtimeQueue && !!user, filters: { entityFilter: filters.debouncedEntityFilter, statusFilter: filters.debouncedStatusFilter, }, onNewItem: (item: ModerationItem) => { if (recentlyRemovedRef.current.has(item.id)) return; setPendingNewItems((prev) => { if (prev.some((p) => p.id === item.id)) return prev; return [...prev, item]; }); setNewItemsCount((prev) => prev + 1); toast({ title: "πŸ†• New Submission", description: `${item.submission_type} - ${item.entity_name}`, }); }, onUpdateItem: (item: ModerationItem, shouldRemove: boolean) => { if (recentlyRemovedRef.current.has(item.id)) return; if (interactingWith.has(item.id)) return; if (shouldRemove) { setItems((prev) => prev.filter((i) => i.id !== item.id)); } else { setItems((prev) => { const exists = prev.some((i) => i.id === item.id); if (exists) { return prev.map((i) => (i.id === item.id ? item : i)); } else { return [item, ...prev]; } }); } }, onItemRemoved: (itemId: string) => { setItems((prev) => prev.filter((i) => i.id !== itemId)); }, entityCache, profileCache, recentlyRemovedIds: recentlyRemovedRef.current, interactingWithIds: interactingWith, currentItems: items, }); return { items, loadingState, actionLoading, filters, pagination, sort, queue, newItemsCount, pendingNewItems, showNewItems, interactingWith, markInteracting, refresh: async () => { await fetchItems(false); }, performAction, deleteSubmission, resetToPending, retryFailedItems, entityCache, profileCache, }; }