Files
thrilltrack-explorer/src/hooks/moderation/useModerationQueueManager.ts
pac7 a9e723c055 Improve moderation queue filtering and sorting for faster feedback
Refactor count query logic in useModerationQueueManager to apply all filters and reduce sort debounce time in useModerationSort to 0ms.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ef7037e7-a631-48a2-94d1-9a4b52d7c35a
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7cdf4e95-3f41-4180-b8e3-8ef56d032c0e/ef7037e7-a631-48a2-94d1-9a4b52d7c35a/kq6AhNt
2025-10-13 15:46:41 +00:00

1046 lines
34 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,
usePagination,
useModerationSort,
useRealtimeSubscriptions,
} from "./index";
import { useModerationQueue } from "@/hooks/useModerationQueue";
import { smartMergeArray } from "@/lib/smartStateUpdate";
import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } 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 {
console.log('🚀 [QUEUE MANAGER] Hook mounting/rendering', {
hasUser: !!config.user,
isAdmin: config.isAdmin,
timestamp: new Date().toISOString()
});
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();
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,
sortField: sort.debouncedConfig.field,
sortDirection: sort.debouncedConfig.direction,
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,
sortField: sort.debouncedConfig.field,
sortDirection: sort.debouncedConfig.direction,
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
)
`,
);
// CRITICAL: Multi-level ordering
console.log('📊 [SORT QUERY] Applying multi-level sort:', {
level1: 'escalated DESC',
level2: `${sort.debouncedConfig.field} ${sort.debouncedConfig.direction.toUpperCase()}`,
level3: sort.debouncedConfig.field !== 'created_at' ? 'created_at ASC' : 'none'
});
// Level 1: Always sort by escalated first (descending)
submissionsQuery = submissionsQuery.order('escalated', { ascending: false });
// Level 2: Apply user-selected sort (use debounced config)
submissionsQuery = submissionsQuery.order(
sort.debouncedConfig.field,
{ ascending: sort.debouncedConfig.direction === 'asc' }
);
// Level 3: Tertiary sort by created_at (if not already primary)
if (sort.debouncedConfig.field !== 'created_at') {
submissionsQuery = submissionsQuery.order('created_at', { ascending: true });
}
// 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 - build separate query with same filters
let countQuery = supabase
.from("content_submissions")
.select("*", { count: "exact", head: true });
// Apply same filters as 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);
}
}
if (entityFilter === "photos") {
countQuery = countQuery.eq("submission_type", "photo");
} else if (entityFilter === "submissions") {
countQuery = countQuery.neq("submission_type", "photo");
}
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;
// Log the actual data returned to verify sort order
if (submissions && submissions.length > 0) {
const sortField = sort.debouncedConfig.field;
const preview = submissions.slice(0, 3).map(s => ({
id: s.id.substring(0, 8),
[sortField]: s[sortField],
escalated: s.escalated
}));
console.log(`📋 [SORT RESULT] First 3 items by ${sortField}:`, preview);
}
// 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.debouncedEntityFilter,
filters.debouncedStatusFilter,
filters.activeTab,
pagination.setTotalCount,
pagination.startIndex,
pagination.endIndex,
sort.debouncedConfig.field,
sort.debouncedConfig.direction,
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 and sort changes trigger refetch
useEffect(() => {
if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return;
console.log('🔄 [SORT/FILTER CHANGE] Detected change:', {
entityFilter: filters.debouncedEntityFilter,
statusFilter: filters.debouncedStatusFilter,
sortField: sort.debouncedConfig.field,
sortDirection: sort.debouncedConfig.direction,
timestamp: new Date().toISOString()
});
pagination.reset();
fetchItemsRef.current?.(false);
}, [
filters.debouncedEntityFilter,
filters.debouncedStatusFilter,
sort.debouncedConfig.field,
sort.debouncedConfig.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,
};
}