From 36d8669b0a780ce00c75d61e3e3a9acbbf562ba4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:15:48 +0000 Subject: [PATCH] Fix: Address moderation queue reload issues --- src/components/moderation/ModerationQueue.tsx | 85 ++++++++++++------- src/components/moderation/QueueItem.tsx | 7 +- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 04cc947a..86472fcb 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -190,11 +190,16 @@ export const ModerationQueue = forwardRef((props, ref) => { localStorage.setItem('moderationQueue_sortConfig', JSON.stringify(sortConfig)); }, [sortConfig]); - // Only sync itemsRef (not loadedIdsRef) to avoid breaking silent polling logic + // Sync itemsRef with items state (after React commits) useEffect(() => { itemsRef.current = items; }, [items]); + // Sync loadedIdsRef with items state (after React commits) + useEffect(() => { + loadedIdsRef.current = new Set(items.map(item => item.id)); + }, [items]); + // Enable transitions after initial render useEffect(() => { if (loadingState === 'ready' && items.length > 0 && !hasRenderedOnce) { @@ -572,11 +577,8 @@ export const ModerationQueue = forwardRef((props, ref) => { const existingIds = new Set(prev.map(p => p.id)); const uniqueNew = newSubmissions.filter(item => !existingIds.has(item.id)); - // Track these IDs as loaded to prevent re-counting on next poll + // Track count increment (loadedIdsRef will sync automatically via useEffect) if (uniqueNew.length > 0) { - const newIds = uniqueNew.map(item => item.id); - const currentLoadedIds = loadedIdsRef.current; - loadedIdsRef.current = new Set([...currentLoadedIds, ...newIds]); setNewItemsCount(prev => prev + uniqueNew.length); } @@ -617,8 +619,6 @@ export const ModerationQueue = forwardRef((props, ref) => { }); if (mergeResult.hasChanges) { - // Update ref BEFORE setState to prevent race conditions - itemsRef.current = mergeResult.items; setItems(mergeResult.items); console.log('🔄 Queue updated (replace mode):', { added: mergeResult.changes.added.length, @@ -660,8 +660,6 @@ export const ModerationQueue = forwardRef((props, ref) => { }); if (mergeResult.hasChanges) { - // Update ref BEFORE setState to prevent race conditions - itemsRef.current = mergeResult.items; setItems(mergeResult.items); console.log('🔄 Queue updated (manual refresh):', { added: mergeResult.changes.added.length, @@ -1061,6 +1059,7 @@ export const ModerationQueue = forwardRef((props, ref) => { escalated: submission.escalated, assigned_to: submission.assigned_to || undefined, locked_until: submission.locked_until || undefined, + submission_items: submission.submission_items || undefined, }; // Update or add to queue @@ -1079,17 +1078,55 @@ export const ModerationQueue = forwardRef((props, ref) => { currentItem.reviewer_notes !== fullItem.reviewer_notes || currentItem.assigned_to !== fullItem.assigned_to || currentItem.locked_until !== fullItem.locked_until || - currentItem.escalated !== fullItem.escalated || - JSON.stringify(currentItem.content) !== JSON.stringify(fullItem.content); + currentItem.escalated !== fullItem.escalated; - if (!hasChanged) { + // Only check content if critical fields match (performance optimization) + let contentChanged = false; + if (!hasChanged && currentItem.content && fullItem.content) { + // Compare content reference first + if (currentItem.content !== fullItem.content) { + // Check each key for actual value changes (one level deep) + const currentKeys = Object.keys(currentItem.content).sort(); + const fullKeys = Object.keys(fullItem.content).sort(); + + if (currentKeys.length !== fullKeys.length || + !currentKeys.every((key, i) => key === fullKeys[i])) { + contentChanged = true; + } else { + for (const key of currentKeys) { + if (currentItem.content[key] !== fullItem.content[key]) { + contentChanged = true; + break; + } + } + } + } + } + + if (!hasChanged && !contentChanged) { console.log('✅ Realtime UPDATE: No changes detected for', fullItem.id); return prev; // Keep existing array reference - PREVENTS RE-RENDER } console.log('🔄 Realtime UPDATE: Changes detected for', fullItem.id); - // Shallow merge to preserve stable references - return prev.map(i => i.id === fullItem.id ? { ...i, ...fullItem } : i); + // Update ONLY changed fields to preserve object stability + return prev.map(i => { + if (i.id !== fullItem.id) return i; + + // Create minimal update object with only changed fields + const updates: Partial = {}; + if (i.status !== fullItem.status) updates.status = fullItem.status; + if (i.reviewed_at !== fullItem.reviewed_at) updates.reviewed_at = fullItem.reviewed_at; + if (i.reviewer_notes !== fullItem.reviewer_notes) updates.reviewer_notes = fullItem.reviewer_notes; + if (i.assigned_to !== fullItem.assigned_to) updates.assigned_to = fullItem.assigned_to; + if (i.locked_until !== fullItem.locked_until) updates.locked_until = fullItem.locked_until; + if (i.escalated !== fullItem.escalated) updates.escalated = fullItem.escalated; + if (contentChanged) updates.content = fullItem.content; + if (fullItem.submission_items) updates.submission_items = fullItem.submission_items; + + // Only create new object if there are actual updates + return Object.keys(updates).length > 0 ? { ...i, ...updates } : i; + }); } else { console.log('🆕 Realtime UPDATE: Adding new item', fullItem.id); return [fullItem, ...prev]; @@ -2226,23 +2263,13 @@ export const ModerationQueue = forwardRef((props, ref) => { variant="default" size="sm" onClick={() => { - // Smooth merge with loading state + // Instant merge without loading state if (pendingNewItems.length > 0) { - setLoadingState('loading'); - - // After 150ms, merge items - setTimeout(() => { - setItems(prev => [...pendingNewItems, ...prev]); - setPendingNewItems([]); - setNewItemsCount(0); - - // Show content again after brief pause - setTimeout(() => { - setLoadingState('ready'); - }, 100); - }, 150); + setItems(prev => [...pendingNewItems, ...prev]); + setPendingNewItems([]); + setNewItemsCount(0); + console.log('✅ New items merged into queue:', pendingNewItems.length); } - console.log('✅ New items merged into queue'); }} className="ml-4" > diff --git a/src/components/moderation/QueueItem.tsx b/src/components/moderation/QueueItem.tsx index fed84a54..07095b7b 100644 --- a/src/components/moderation/QueueItem.tsx +++ b/src/components/moderation/QueueItem.tsx @@ -668,15 +668,16 @@ export const QueueItem = memo(({ const nextLocked = nextProps.lockedSubmissions.has(nextProps.item.id); if (prevLocked !== nextLocked) return false; - // Deep comparison of content and other fields that affect rendering + // Deep comparison of critical fields (use strict equality for reference stability) + if (prevProps.item.status !== nextProps.item.status) return false; if (prevProps.item.reviewed_at !== nextProps.item.reviewed_at) return false; if (prevProps.item.reviewer_notes !== nextProps.item.reviewer_notes) return false; if (prevProps.item.assigned_to !== nextProps.item.assigned_to) return false; if (prevProps.item.locked_until !== nextProps.item.locked_until) return false; if (prevProps.item.escalated !== nextProps.item.escalated) return false; - // Content comparison (most expensive, do last) - if (JSON.stringify(prevProps.item.content) !== JSON.stringify(nextProps.item.content)) return false; + // Only check content reference, not deep equality (performance) + if (prevProps.item.content !== nextProps.item.content) return false; // All checks passed - items are identical return true;