mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 04:51:22 -05:00
Refactor: Implement debouncing and anti-flashing fixes
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user