diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 9be0ad63..552be225 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -494,14 +494,28 @@ export const ModerationQueue = forwardRef((props, ref) => { break; case 'replace': - // Full refresh: replace entire queue - setItems(moderationItems); + // Smart refresh: only update if data actually changed + 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):', { + added: mergeResult.changes.added.length, + removed: mergeResult.changes.removed.length, + updated: mergeResult.changes.updated.length, + }); + } else { + console.log('✅ Queue unchanged (replace mode) - no visual updates needed'); + } + if (!currentPreserveInteraction) { - // Reset interaction state if not preserving setPendingNewItems([]); setNewItemsCount(0); } - console.log('🔄 Queue replaced (replace mode) - full refresh with', moderationItems.length, 'items'); break; default: @@ -821,7 +835,15 @@ export const ModerationQueue = forwardRef((props, ref) => { if (wasInQueue && !shouldBeInQueue) { // Submission moved out of current filter (e.g., pending → approved) console.log('❌ Submission moved out of queue:', updatedSubmission.id); - setItems(prev => prev.filter(i => i.id !== updatedSubmission.id)); + setItems(prev => { + const exists = prev.some(i => i.id === updatedSubmission.id); + if (!exists) { + console.log('✅ Realtime: Item already removed', updatedSubmission.id); + return prev; // Keep existing array reference + } + console.log('❌ Realtime: Removing item from queue', updatedSubmission.id); + return prev.filter(i => i.id !== updatedSubmission.id); + }); } else if (shouldBeInQueue) { // Submission should be in queue - update it console.log('🔄 Submission updated in queue:', updatedSubmission.id); @@ -870,10 +892,32 @@ export const ModerationQueue = forwardRef((props, ref) => { // Update or add to queue setItems(prev => { const exists = prev.some(i => i.id === fullItem.id); + if (exists) { + // Check if item actually changed before updating + const currentItem = prev.find(i => i.id === fullItem.id); + if (!currentItem) return prev; + + // Deep comparison of critical fields + const hasChanged = + currentItem.status !== fullItem.status || + currentItem.reviewed_at !== fullItem.reviewed_at || + currentItem.reviewed_by !== fullItem.reviewed_by || + currentItem.reviewer_notes !== fullItem.reviewer_notes || + currentItem.assigned_to !== fullItem.assigned_to || + currentItem.locked_until !== fullItem.locked_until || + JSON.stringify(currentItem.content) !== JSON.stringify(fullItem.content); + + if (!hasChanged) { + console.log('✅ Realtime UPDATE: No changes detected for', fullItem.id); + return prev; // Keep existing array reference + } + + console.log('🔄 Realtime UPDATE: Changes detected for', fullItem.id); return prev.map(i => i.id === fullItem.id ? fullItem : i); } else { - return [fullItem, ...prev]; // Add at top + console.log('🆕 Realtime UPDATE: Adding new item', fullItem.id); + return [fullItem, ...prev]; } }); } catch (error) { diff --git a/src/components/moderation/QueueItem.tsx b/src/components/moderation/QueueItem.tsx index dadcd154..3e1dc5dd 100644 --- a/src/components/moderation/QueueItem.tsx +++ b/src/components/moderation/QueueItem.tsx @@ -643,16 +643,32 @@ export const QueueItem = memo(({ ); }, (prevProps, nextProps) => { - // Custom comparison to prevent re-renders - return ( - prevProps.item.id === nextProps.item.id && - prevProps.item.status === nextProps.item.status && - prevProps.actionLoading === nextProps.actionLoading && - prevProps.lockedSubmissions.has(prevProps.item.id) === nextProps.lockedSubmissions.has(nextProps.item.id) && - prevProps.currentLockSubmissionId === nextProps.currentLockSubmissionId && - prevProps.notes[prevProps.item.id] === nextProps.notes[nextProps.item.id] && - prevProps.notes[`reverse-${prevProps.item.id}`] === nextProps.notes[`reverse-${nextProps.item.id}`] - ); + // Quick checks first (cheapest) + if (prevProps.item.id !== nextProps.item.id) return false; + if (prevProps.item.status !== nextProps.item.status) return false; + if (prevProps.actionLoading !== nextProps.actionLoading) return false; + if (prevProps.currentLockSubmissionId !== nextProps.currentLockSubmissionId) return false; + if (prevProps.notes[prevProps.item.id] !== nextProps.notes[nextProps.item.id]) return false; + if (prevProps.notes[`reverse-${prevProps.item.id}`] !== nextProps.notes[`reverse-${nextProps.item.id}`]) return false; + + // Check lock status + const prevLocked = prevProps.lockedSubmissions.has(prevProps.item.id); + const nextLocked = nextProps.lockedSubmissions.has(nextProps.item.id); + if (prevLocked !== nextLocked) return false; + + // Deep comparison of content and other fields that affect rendering + if (prevProps.item.reviewed_at !== nextProps.item.reviewed_at) return false; + if (prevProps.item.reviewed_by !== nextProps.item.reviewed_by) 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; + + // All checks passed - items are identical + return true; }); QueueItem.displayName = 'QueueItem';