feat(mod-queue): add minutes support to alert threshold

This commit is contained in:
plebeius
2026-01-11 15:20:48 +01:00
parent 49004109f8
commit ab353b59f6
38 changed files with 191 additions and 96 deletions
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "نشر", "publishing": "نشر",
"approved": "موافق عليه", "approved": "موافق عليه",
"rejected": "مرفوض", "rejected": "مرفوض",
"submitted": "تم الإرسال" "submitted": "تم الإرسال",
"hours": "ساعات",
"minutes": "دقائق"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "প্রকাশনা", "publishing": "প্রকাশনা",
"approved": "অনুমোদিত", "approved": "অনুমোদিত",
"rejected": "প্রত্যাখ্যান", "rejected": "প্রত্যাখ্যান",
"submitted": "জমা দেওয়া হয়েছে" "submitted": "জমা দেওয়া হয়েছে",
"hours": "ঘণ্টা",
"minutes": "মিনিট"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publikování", "publishing": "Publikování",
"approved": "Schváleno", "approved": "Schváleno",
"rejected": "Zamítnuto", "rejected": "Zamítnuto",
"submitted": "Odesláno" "submitted": "Odesláno",
"hours": "hodin",
"minutes": "minut"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Udgivelse", "publishing": "Udgivelse",
"approved": "Godkendt", "approved": "Godkendt",
"rejected": "Afvist", "rejected": "Afvist",
"submitted": "Indsendt" "submitted": "Indsendt",
"hours": "timer",
"minutes": "minutter"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Veröffentlichung", "publishing": "Veröffentlichung",
"approved": "Genehmigt", "approved": "Genehmigt",
"rejected": "Abgelehnt", "rejected": "Abgelehnt",
"submitted": "Eingereicht" "submitted": "Eingereicht",
"hours": "Stunden",
"minutes": "Minuten"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Δημοσίευση", "publishing": "Δημοσίευση",
"approved": "Εγκεκριμένο", "approved": "Εγκεκριμένο",
"rejected": "Απορρίφθηκε", "rejected": "Απορρίφθηκε",
"submitted": "Υποβλήθηκε" "submitted": "Υποβλήθηκε",
"hours": "ώρες",
"minutes": "λεπτά"
} }
+2
View File
@@ -261,6 +261,8 @@
"reject": "Reject", "reject": "Reject",
"filter_by_board": "Filter by board", "filter_by_board": "Filter by board",
"mod_queue_alert_threshold": "Mod queue alert threshold", "mod_queue_alert_threshold": "Mod queue alert threshold",
"hours": "hours",
"minutes": "minutes",
"no_content": "No content", "no_content": "No content",
"excerpt": "Excerpt", "excerpt": "Excerpt",
"publishing": "Publishing", "publishing": "Publishing",
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publicando", "publishing": "Publicando",
"approved": "Aprobado", "approved": "Aprobado",
"rejected": "Rechazado", "rejected": "Rechazado",
"submitted": "Enviado" "submitted": "Enviado",
"hours": "horas",
"minutes": "minutos"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "در حال انتشار", "publishing": "در حال انتشار",
"approved": "تایید شده", "approved": "تایید شده",
"rejected": "رد شده", "rejected": "رد شده",
"submitted": "ارسال شده" "submitted": "ارسال شده",
"hours": "ساعت",
"minutes": "دقیقه"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Julkaiseminen", "publishing": "Julkaiseminen",
"approved": "Hyväksytty", "approved": "Hyväksytty",
"rejected": "Hylätty", "rejected": "Hylätty",
"submitted": "Lähetetty" "submitted": "Lähetetty",
"hours": "tuntia",
"minutes": "minuuttia"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Pag-publish", "publishing": "Pag-publish",
"approved": "Naaprubado", "approved": "Naaprubado",
"rejected": "Tinanggihan", "rejected": "Tinanggihan",
"submitted": "Isinumite" "submitted": "Isinumite",
"hours": "oras",
"minutes": "minuto"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publication", "publishing": "Publication",
"approved": "Approuvé", "approved": "Approuvé",
"rejected": "Rejeté", "rejected": "Rejeté",
"submitted": "Soumis" "submitted": "Soumis",
"hours": "heures",
"minutes": "minutes"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "מפרסם", "publishing": "מפרסם",
"approved": "אושר", "approved": "אושר",
"rejected": "נדחה", "rejected": "נדחה",
"submitted": "נשלח" "submitted": "נשלח",
"hours": "שעות",
"minutes": "דקות"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "प्रकाशन", "publishing": "प्रकाशन",
"approved": "अनुमोदित", "approved": "अनुमोदित",
"rejected": "अस्वीकृत", "rejected": "अस्वीकृत",
"submitted": "सबमिट किया गया" "submitted": "सबमिट किया गया",
"hours": "घंटे",
"minutes": "मिनट"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Közzététel", "publishing": "Közzététel",
"approved": "Jóváhagyva", "approved": "Jóváhagyva",
"rejected": "Elutasítva", "rejected": "Elutasítva",
"submitted": "Beküldve" "submitted": "Beküldve",
"hours": "óra",
"minutes": "perc"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Menerbitkan", "publishing": "Menerbitkan",
"approved": "Disetujui", "approved": "Disetujui",
"rejected": "Ditolak", "rejected": "Ditolak",
"submitted": "Dikirim" "submitted": "Dikirim",
"hours": "jam",
"minutes": "menit"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Pubblicazione", "publishing": "Pubblicazione",
"approved": "Approvato", "approved": "Approvato",
"rejected": "Rifiutato", "rejected": "Rifiutato",
"submitted": "Inviato" "submitted": "Inviato",
"hours": "ore",
"minutes": "minuti"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "公開中", "publishing": "公開中",
"approved": "承認済み", "approved": "承認済み",
"rejected": "却下", "rejected": "却下",
"submitted": "送信済み" "submitted": "送信済み",
"hours": "時間",
"minutes": "分"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "게시 중", "publishing": "게시 중",
"approved": "승인됨", "approved": "승인됨",
"rejected": "거부됨", "rejected": "거부됨",
"submitted": "제출됨" "submitted": "제출됨",
"hours": "시간",
"minutes": "분"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "प्रकाशन करत आहे", "publishing": "प्रकाशन करत आहे",
"approved": "मंजूर", "approved": "मंजूर",
"rejected": "नाकारले", "rejected": "नाकारले",
"submitted": "सबमिट केले" "submitted": "सबमिट केले",
"hours": "तास",
"minutes": "मिनिटे"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publiceren", "publishing": "Publiceren",
"approved": "Goedgekeurd", "approved": "Goedgekeurd",
"rejected": "Afgewezen", "rejected": "Afgewezen",
"submitted": "Ingediend" "submitted": "Ingediend",
"hours": "uur",
"minutes": "minuten"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publisering", "publishing": "Publisering",
"approved": "Godkjent", "approved": "Godkjent",
"rejected": "Avslått", "rejected": "Avslått",
"submitted": "Sendt inn" "submitted": "Sendt inn",
"hours": "timer",
"minutes": "minutter"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publikowanie", "publishing": "Publikowanie",
"approved": "Zatwierdzone", "approved": "Zatwierdzone",
"rejected": "Odrzucone", "rejected": "Odrzucone",
"submitted": "Przesłane" "submitted": "Przesłane",
"hours": "godzin",
"minutes": "minut"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publicando", "publishing": "Publicando",
"approved": "Aprovado", "approved": "Aprovado",
"rejected": "Rejeitado", "rejected": "Rejeitado",
"submitted": "Enviado" "submitted": "Enviado",
"hours": "horas",
"minutes": "minutos"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publicare", "publishing": "Publicare",
"approved": "Aprobat", "approved": "Aprobat",
"rejected": "Respins", "rejected": "Respins",
"submitted": "Trimis" "submitted": "Trimis",
"hours": "ore",
"minutes": "minute"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Публикация", "publishing": "Публикация",
"approved": "Одобрено", "approved": "Одобрено",
"rejected": "Отклонено", "rejected": "Отклонено",
"submitted": "Отправлено" "submitted": "Отправлено",
"hours": "часов",
"minutes": "минут"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publikimi", "publishing": "Publikimi",
"approved": "Aprovuar", "approved": "Aprovuar",
"rejected": "Refuzuar", "rejected": "Refuzuar",
"submitted": "Dërguar" "submitted": "Dërguar",
"hours": "orë",
"minutes": "minuta"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Publicerar", "publishing": "Publicerar",
"approved": "Godkänd", "approved": "Godkänd",
"rejected": "Avvisad", "rejected": "Avvisad",
"submitted": "Inskickat" "submitted": "Inskickat",
"hours": "timmar",
"minutes": "minuter"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "ప్రచురించడం", "publishing": "ప్రచురించడం",
"approved": "అనుమోదించబడింది", "approved": "అనుమోదించబడింది",
"rejected": "తిరస్కరించబడింది", "rejected": "తిరస్కరించబడింది",
"submitted": "సమర్పించబడింది" "submitted": "సమర్పించబడింది",
"hours": "గంటలు",
"minutes": "నిమిషాలు"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "กำลังเผยแพร่", "publishing": "กำลังเผยแพร่",
"approved": "อนุมัติแล้ว", "approved": "อนุมัติแล้ว",
"rejected": "ปฏิเสธ", "rejected": "ปฏิเสธ",
"submitted": "ส่งแล้ว" "submitted": "ส่งแล้ว",
"hours": "ชั่วโมง",
"minutes": "นาที"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Yayınlama", "publishing": "Yayınlama",
"approved": "Onaylandı", "approved": "Onaylandı",
"rejected": "Reddedildi", "rejected": "Reddedildi",
"submitted": "Gönderildi" "submitted": "Gönderildi",
"hours": "saat",
"minutes": "dakika"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Публікація", "publishing": "Публікація",
"approved": "Схвалено", "approved": "Схвалено",
"rejected": "Відхилено", "rejected": "Відхилено",
"submitted": "Відправлено" "submitted": "Відправлено",
"hours": "годин",
"minutes": "хвилин"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "اشاعت", "publishing": "اشاعت",
"approved": "منظور شدہ", "approved": "منظور شدہ",
"rejected": "مسترد", "rejected": "مسترد",
"submitted": "جمع کرایا گیا" "submitted": "جمع کرایا گیا",
"hours": "گھنٹے",
"minutes": "منٹ"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "Đang xuất bản", "publishing": "Đang xuất bản",
"approved": "Đã phê duyệt", "approved": "Đã phê duyệt",
"rejected": "Đã từ chối", "rejected": "Đã từ chối",
"submitted": "Đã gửi" "submitted": "Đã gửi",
"hours": "giờ",
"minutes": "phút"
} }
+3 -1
View File
@@ -267,5 +267,7 @@
"publishing": "发布中", "publishing": "发布中",
"approved": "已批准", "approved": "已批准",
"rejected": "已拒绝", "rejected": "已拒绝",
"submitted": "已提交" "submitted": "已提交",
"hours": "小时",
"minutes": "分钟"
} }
+44 -8
View File
@@ -1,21 +1,57 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
export type AlertThresholdUnit = 'hours' | 'minutes';
interface ModQueueState { interface ModQueueState {
alertThresholdHours: number; alertThresholdValue: number;
alertThresholdUnit: AlertThresholdUnit;
selectedBoardFilter: string | null; selectedBoardFilter: string | null;
setAlertThresholdHours: (hours: number) => void; setAlertThreshold: (value: number, unit: AlertThresholdUnit) => void;
setSelectedBoardFilter: (boardAddress: string | null) => void; setSelectedBoardFilter: (boardAddress: string | null) => void;
// Helper to get threshold in seconds for calculations
getAlertThresholdSeconds: () => number;
} }
const useModQueueStore = create<ModQueueState>()( const useModQueueStore = create<ModQueueState>()(
persist( persist(
(set) => ({ (set, get) => {
alertThresholdHours: 6, // Migration: Check for old format in localStorage
selectedBoardFilter: null, let initialState = { alertThresholdValue: 6, alertThresholdUnit: 'hours' as AlertThresholdUnit };
setAlertThresholdHours: (hours) => set({ alertThresholdHours: hours }), if (typeof window !== 'undefined') {
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }), try {
}), const stored = localStorage.getItem('mod-queue-storage');
if (stored) {
const parsed = JSON.parse(stored);
// If old format exists, migrate it
if (parsed.state?.alertThresholdHours !== undefined) {
initialState = {
alertThresholdValue: parsed.state.alertThresholdHours,
alertThresholdUnit: 'hours',
};
} else if (parsed.state?.alertThresholdValue !== undefined) {
initialState = {
alertThresholdValue: parsed.state.alertThresholdValue,
alertThresholdUnit: parsed.state.alertThresholdUnit || 'hours',
};
}
}
} catch (e) {
// Ignore parse errors, use defaults
}
}
return {
...initialState,
selectedBoardFilter: null,
setAlertThreshold: (value, unit) => set({ alertThresholdValue: value, alertThresholdUnit: unit }),
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }),
getAlertThresholdSeconds: () => {
const { alertThresholdValue, alertThresholdUnit } = get();
return alertThresholdUnit === 'hours' ? alertThresholdValue * 3600 : alertThresholdValue * 60;
},
};
},
{ {
name: 'mod-queue-storage', name: 'mod-queue-storage',
}, },
+8 -2
View File
@@ -39,6 +39,14 @@
color: var(--body-font-color); color: var(--body-font-color);
} }
.alertThresholdUnitSelect {
padding: 4px;
border: var(--mod-queue-table-border);
background: var(--mod-queue-header-background-color);
color: var(--body-font-color);
margin-left: 4px;
}
.tableWrapper { .tableWrapper {
border: var(--mod-queue-table-border); border: var(--mod-queue-table-border);
} }
@@ -147,8 +155,6 @@
color: var(--mod-queue-alert-color); color: var(--mod-queue-alert-color);
font-weight: bold; font-weight: bold;
animation: blink 2s infinite; animation: blink 2s infinite;
/* Synchronize all animations by starting from the same phase */
animation-delay: calc(var(--mod-queue-blink-phase, 0s) * -1);
} }
@keyframes blink { @keyframes blink {
+35 -52
View File
@@ -48,7 +48,7 @@ type ModerationAction = 'approve' | 'reject' | null;
const ModQueueRow = ({ comment, showBoardColumn = false }: ModQueueRowProps) => { const ModQueueRow = ({ comment, showBoardColumn = false }: ModQueueRowProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { alertThresholdHours } = useModQueueStore(); const { getAlertThresholdSeconds } = useModQueueStore();
const [initiatedAction, setInitiatedAction] = useState<ModerationAction>(null); const [initiatedAction, setInitiatedAction] = useState<ModerationAction>(null);
// handle pending mod or author edit // handle pending mod or author edit
@@ -65,7 +65,8 @@ const ModQueueRow = ({ comment, showBoardColumn = false }: ModQueueRowProps) =>
const boardPath = useBoardPath(subplebbitAddress); const boardPath = useBoardPath(subplebbitAddress);
const timeWaiting = Date.now() / 1000 - timestamp; const timeWaiting = Date.now() / 1000 - timestamp;
const isOverThreshold = timeWaiting > alertThresholdHours * 3600; const alertThresholdSeconds = getAlertThresholdSeconds();
const isOverThreshold = timeWaiting > alertThresholdSeconds;
// Only show alert animation for comments awaiting approval (not approved or rejected) // Only show alert animation for comments awaiting approval (not approved or rejected)
const isAwaitingApproval = !alreadyApproved && !alreadyRejected; const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
@@ -256,18 +257,18 @@ interface ModQueueButtonProps {
// and reports its status via callback // and reports its status via callback
interface ModQueueCountItemProps { interface ModQueueCountItemProps {
comment: Comment; comment: Comment;
alertThresholdHours: number; alertThresholdSeconds: number;
onStatusChange: (cid: string, status: { awaiting: boolean; urgent: boolean }) => void; onStatusChange: (cid: string, status: { awaiting: boolean; urgent: boolean }) => void;
} }
const ModQueueCountItem = ({ comment, alertThresholdHours, onStatusChange }: ModQueueCountItemProps) => { const ModQueueCountItem = ({ comment, alertThresholdSeconds, onStatusChange }: ModQueueCountItemProps) => {
const { editedComment } = useEditedComment({ comment }); const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment; const displayComment = editedComment || comment;
const { cid, approved, removed, timestamp } = displayComment; const { cid, approved, removed, timestamp } = displayComment;
const isAwaiting = approved !== true && removed !== true; const isAwaiting = approved !== true && removed !== true;
const timeWaiting = Date.now() / 1000 - timestamp; const timeWaiting = Date.now() / 1000 - timestamp;
const isUrgent = isAwaiting && timeWaiting > alertThresholdHours * 3600; const isUrgent = isAwaiting && timeWaiting > alertThresholdSeconds;
// Report status changes to parent - useEffect is appropriate here for syncing with parent state // Report status changes to parent - useEffect is appropriate here for syncing with parent state
useEffect(() => { useEffect(() => {
@@ -280,12 +281,12 @@ const ModQueueCountItem = ({ comment, alertThresholdHours, onStatusChange }: Mod
// Inner component that handles counting with useEditedComment for each item // Inner component that handles counting with useEditedComment for each item
interface ModQueueButtonContentProps { interface ModQueueButtonContentProps {
feed: Comment[]; feed: Comment[];
alertThresholdHours: number; alertThresholdSeconds: number;
boardIdentifier?: string; boardIdentifier?: string;
isMobile?: boolean; isMobile?: boolean;
} }
const ModQueueButtonContent = ({ feed, alertThresholdHours, boardIdentifier, isMobile }: ModQueueButtonContentProps) => { const ModQueueButtonContent = ({ feed, alertThresholdSeconds, boardIdentifier, isMobile }: ModQueueButtonContentProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [statusMap, setStatusMap] = useState<Map<string, { awaiting: boolean; urgent: boolean }>>(new Map()); const [statusMap, setStatusMap] = useState<Map<string, { awaiting: boolean; urgent: boolean }>>(new Map());
@@ -344,7 +345,7 @@ const ModQueueButtonContent = ({ feed, alertThresholdHours, boardIdentifier, isM
<> <>
{/* Render counter items to track real-time moderation status */} {/* Render counter items to track real-time moderation status */}
{feed.map((item) => ( {feed.map((item) => (
<ModQueueCountItem key={item.cid} comment={item} alertThresholdHours={alertThresholdHours} onStatusChange={handleStatusChange} /> <ModQueueCountItem key={item.cid} comment={item} alertThresholdSeconds={alertThresholdSeconds} onStatusChange={handleStatusChange} />
))} ))}
{isMobile ? buttonContent : <>[{buttonContent}]</>} {isMobile ? buttonContent : <>[{buttonContent}]</>}
</> </>
@@ -352,7 +353,7 @@ const ModQueueButtonContent = ({ feed, alertThresholdHours, boardIdentifier, isM
}; };
export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => { export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => {
const { alertThresholdHours } = useModQueueStore(); const { getAlertThresholdSeconds } = useModQueueStore();
const { accountSubplebbits } = useAccountSubplebbits(); const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = useMemo(() => Object.keys(accountSubplebbits || {}), [accountSubplebbits]); const accountSubplebbitAddresses = useMemo(() => Object.keys(accountSubplebbits || {}), [accountSubplebbits]);
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
@@ -388,13 +389,14 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
return null; return null;
} }
return <ModQueueButtonContent feed={feed} alertThresholdHours={alertThresholdHours} boardIdentifier={boardIdentifier} isMobile={isMobile} />; const alertThresholdSeconds = getAlertThresholdSeconds();
return <ModQueueButtonContent feed={feed} alertThresholdSeconds={alertThresholdSeconds} boardIdentifier={boardIdentifier} isMobile={isMobile} />;
}; };
export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => { export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
const { selectedBoardFilter, alertThresholdHours, setAlertThresholdHours } = useModQueueStore(); const { selectedBoardFilter, alertThresholdValue, alertThresholdUnit, setAlertThreshold } = useModQueueStore();
const { accountSubplebbits } = useAccountSubplebbits(); const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = Object.keys(accountSubplebbits); const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
@@ -436,43 +438,6 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
setResetFunction(reset); setResetFunction(reset);
}, [reset, setResetFunction]); }, [reset, setResetFunction]);
// Check if any feed items need the blinking animation (awaiting approval + over threshold)
// This is a heuristic based on raw feed data - real-time edits might change this,
// but it's good enough to avoid running the interval when nothing needs to blink
const hasBlinkingItems = useMemo(() => {
const thresholdSeconds = alertThresholdHours * 3600;
const now = Date.now() / 1000;
return feed.some((comment) => {
const isAwaiting = comment.approved !== true && comment.removed !== true;
const timeWaiting = now - comment.timestamp;
return isAwaiting && timeWaiting > thresholdSeconds;
});
}, [feed, alertThresholdHours]);
// Synchronize blinking animation across all rows
// Set a CSS variable with the current animation phase so all elements start from the same point
// Update frequently to ensure elements rendered at different times stay synchronized
// Only run when there are items that actually need the blinking animation
useEffect(() => {
if (!hasBlinkingItems) {
return;
}
const ANIMATION_DURATION = 2000; // 2 seconds
const UPDATE_INTERVAL = 100; // Update every 100ms for smooth synchronization
const updateAnimationPhase = () => {
// Calculate current phase in the animation cycle (0 to 2 seconds)
const phase = (Date.now() % ANIMATION_DURATION) / 1000;
document.documentElement.style.setProperty('--mod-queue-blink-phase', `${phase}s`);
};
// Update immediately and then at regular intervals
updateAnimationPhase();
const interval = setInterval(updateAnimationPhase, UPDATE_INTERVAL);
return () => clearInterval(interval);
}, [hasBlinkingItems]);
const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading'); const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading');
const showBoardColumn = !resolvedAddress && !selectedBoardFilter; const showBoardColumn = !resolvedAddress && !selectedBoardFilter;
@@ -494,11 +459,29 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
<input <input
type='number' type='number'
min='1' min='1'
value={alertThresholdHours} step={alertThresholdUnit === 'minutes' ? '1' : '1'}
onChange={(e) => setAlertThresholdHours(Number(e.target.value))} value={alertThresholdValue}
onChange={(e) => setAlertThreshold(Number(e.target.value), alertThresholdUnit)}
className={styles.alertThresholdInput} className={styles.alertThresholdInput}
/>{' '} />
{t('hours')} <select
value={alertThresholdUnit}
onChange={(e) => {
const newUnit = e.target.value as 'hours' | 'minutes';
// Convert value when switching units
const newValue =
alertThresholdUnit === 'hours' && newUnit === 'minutes'
? alertThresholdValue * 60
: alertThresholdUnit === 'minutes' && newUnit === 'hours'
? Math.round(alertThresholdValue / 60)
: alertThresholdValue;
setAlertThreshold(Math.max(1, newValue), newUnit);
}}
className={styles.alertThresholdUnitSelect}
>
<option value='minutes'>{t('minutes')}</option>
<option value='hours'>{t('hours')}</option>
</select>
</label> </label>
</div> </div>
</div> </div>