Files
thrilltrack-explorer/src/hooks/moderation/useModerationQueueManager.ts
gpt-engineer-app[bot] ef913fcc8d Fix count query for sorting
2025-10-13 01:37:19 +00:00

1011 lines
33 KiB
TypeScript

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<typeof useToast>["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<typeof useModerationFilters>;
pagination: ReturnType<typeof usePagination>;
sort: ReturnType<typeof useModerationSort>;
queue: ReturnType<typeof useModerationQueue>;
// Realtime
newItemsCount: number;
pendingNewItems: ModerationItem[];
showNewItems: () => void;
// Interaction tracking
interactingWith: Set<string>;
markInteracting: (id: string, interacting: boolean) => void;
// Actions
refresh: () => void;
performAction: (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => Promise<void>;
deleteSubmission: (item: ModerationItem) => Promise<void>;
resetToPending: (item: ModerationItem) => Promise<void>;
retryFailedItems: (item: ModerationItem) => Promise<void>;
// Caches (for QueueItem enrichment)
entityCache: ReturnType<typeof useEntityCache>;
profileCache: ReturnType<typeof useProfileCache>;
}
/**
* 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<ModerationItem[]>([]);
const [loadingState, setLoadingState] = useState<LoadingState>("initial");
const [actionLoading, setActionLoading] = useState<string | null>(null);
const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set());
const [pendingNewItems, setPendingNewItems] = useState<ModerationItem[]>([]);
const [newItemsCount, setNewItemsCount] = useState(0);
// Refs for tracking
const recentlyRemovedRef = useRef<Set<string>>(new Set());
const fetchInProgressRef = useRef(false);
const itemsRef = useRef<ModerationItem[]>([]);
const lastFetchTimeRef = useRef<number>(0);
const pauseFetchingRef = useRef(false);
const initialFetchCompleteRef = useRef(false);
const isMountingRef = useRef(true);
const fetchItemsRef = useRef<((silent?: boolean) => Promise<void>) | 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,
};
}