Refactor: Implement debouncing and anti-flashing fixes

This commit is contained in:
gpt-engineer-app[bot]
2025-10-09 15:47:09 +00:00
parent f01c58a056
commit 039fe46e55
2 changed files with 68 additions and 38 deletions

View File

@@ -121,6 +121,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const preserveInteraction = getPreserveInteractionState();
const useRealtimeQueue = getUseRealtimeQueue();
// Track recently removed items to prevent realtime override of optimistic updates
const recentlyRemovedRef = useRef<Set<string>>(new Set());
const prevLocksRef = useRef<Map<string, boolean>>(new Map());
// Store admin settings and stable refs to avoid triggering fetchItems recreation
const refreshStrategyRef = useRef(refreshStrategy);
const preserveInteractionRef = useRef(preserveInteraction);
@@ -574,7 +578,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, refreshMode, pollInterval, isInitialLoad, useRealtimeQueue]);
// Real-time subscription for lock status
// Real-time subscription for lock status (optimized to prevent unnecessary updates)
useEffect(() => {
if (!user) return;
@@ -589,24 +593,21 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
},
(payload) => {
const newData = payload.new as any;
const isLocked = newData.assigned_to && newData.assigned_to !== user.id &&
newData.locked_until && new Date(newData.locked_until) > new Date();
const wasLocked = prevLocksRef.current.get(newData.id) || false;
// Track submissions locked by others
if (newData.assigned_to && newData.assigned_to !== user.id && newData.locked_until) {
const lockExpiry = new Date(newData.locked_until);
if (lockExpiry > new Date()) {
setLockedSubmissions((prev) => new Set(prev).add(newData.id));
} else {
setLockedSubmissions((prev) => {
const next = new Set(prev);
next.delete(newData.id);
return next;
});
}
} else {
// Lock released
// Only update if lock state actually changed
if (isLocked !== wasLocked) {
prevLocksRef.current.set(newData.id, isLocked);
setLockedSubmissions((prev) => {
const next = new Set(prev);
next.delete(newData.id);
if (isLocked) {
next.add(newData.id);
} else {
next.delete(newData.id);
}
return next;
});
}
@@ -635,6 +636,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
async (payload) => {
const newSubmission = payload.new as any;
// Ignore if recently removed (optimistic update)
if (recentlyRemovedRef.current.has(newSubmission.id)) {
return;
}
// Only process pending/partially_approved submissions
if (!['pending', 'partially_approved'].includes(newSubmission.status)) {
return;
@@ -857,14 +863,22 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const shouldRemove = (activeStatusFilter === 'pending' || activeStatusFilter === 'flagged') &&
(action === 'approved' || action === 'rejected');
// Optimistic update
if (shouldRemove) {
setItems(prev => prev.filter(i => i.id !== item.id));
} else {
setItems(prev => prev.map(i =>
i.id === item.id ? { ...i, status: action } : i
));
}
// Optimistic UI update - batch with requestAnimationFrame for smoother rendering
requestAnimationFrame(() => {
if (shouldRemove) {
setItems(prev => prev.filter(i => i.id !== item.id));
// Mark as recently removed - ignore realtime updates for 3 seconds
recentlyRemovedRef.current.add(item.id);
setTimeout(() => {
recentlyRemovedRef.current.delete(item.id);
}, 3000);
} else {
setItems(prev => prev.map(i =>
i.id === item.id ? { ...i, status: action } : i
));
}
});
// Release lock if this submission is claimed by current user
if (queue.currentLock?.submissionId === item.id) {
@@ -1431,13 +1445,20 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
return (
<div className="space-y-6">
{items.map((item) => (
<Card key={item.id} className={`border-l-4 ${
item.status === 'flagged' ? 'border-l-red-500' :
item.status === 'approved' ? 'border-l-green-500' :
<Card
key={item.id}
className={`border-l-4 transition-opacity duration-200 ${
item.status === 'flagged' ? 'border-l-red-500' :
item.status === 'approved' ? 'border-l-green-500' :
item.status === 'rejected' ? 'border-l-red-400' :
item.status === 'partially_approved' ? 'border-l-yellow-500' :
'border-l-amber-500'
}`}>
}`}
style={{
opacity: actionLoading === item.id ? 0.5 : 1,
pointerEvents: actionLoading === item.id ? 'none' : 'auto'
}}
>
<CardHeader className={isMobile ? "pb-3 p-4" : "pb-4"}>
<div className={`flex gap-3 ${isMobile ? 'flex-col' : 'items-center justify-between'}`}>
<div className="flex items-center gap-2 flex-wrap">