update mod queue

This commit is contained in:
plebeius
2026-01-08 16:43:14 +01:00
parent 4e13efca5f
commit e899ac7fb9
5 changed files with 272 additions and 73 deletions
@@ -17,6 +17,7 @@ import useTimeFilter from '../../hooks/use-time-filter';
import CatalogFilters from '../catalog-filters'; import CatalogFilters from '../catalog-filters';
import CatalogSearch from '../catalog-search'; import CatalogSearch from '../catalog-search';
import Tooltip from '../tooltip'; import Tooltip from '../tooltip';
import { ModQueueButton } from '../../views/mod-queue/mod-queue';
import styles from './board-buttons.module.css'; import styles from './board-buttons.module.css';
import _ from 'lodash'; import _ from 'lodash';
@@ -362,6 +363,7 @@ export const MobileBoardButtons = () => {
)} )}
{showVoteButton && <VoteButton />} {showVoteButton && <VoteButton />}
{!(isInAllView || isInSubscriptionsView || isInModView) && <SubscribeButton address={subplebbitAddress} />} {!(isInAllView || isInSubscriptionsView || isInModView) && <SubscribeButton address={subplebbitAddress} />}
{!(isInAllView || isInSubscriptionsView || isInModView) && <ModQueueButton boardIdentifier={boardIdentifier} isMobile={true} />}
<RefreshButton /> <RefreshButton />
{isInCatalogView && searchText ? ( {isInCatalogView && searchText ? (
<span className={styles.filteredThreadsCount}> <span className={styles.filteredThreadsCount}>
@@ -496,6 +498,12 @@ export const DesktopBoardButtons = () => {
[<VoteButton />] [<VoteButton />]
</> </>
)} )}
{!(isInAllView || isInSubscriptionsView || isInModView) && (
<>
{' '}
[<ModQueueButton boardIdentifier={boardIdentifier} isMobile={false} />]
</>
)}
{isInCatalogView && searchText ? ( {isInCatalogView && searchText ? (
<span className={styles.filteredThreadsCount}> <span className={styles.filteredThreadsCount}>
{' '} {' '}
+27
View File
@@ -0,0 +1,27 @@
import { useState, useEffect } from 'react';
/**
* Returns the current time in seconds, updating periodically.
* This prevents unnecessary rerenders by only updating every 60 seconds
* instead of on every render cycle.
*
* For visual updates like blinking animations, CSS handles that independently.
* This hook is for time-based calculations that don't need millisecond precision.
*/
export const useCurrentTime = (updateIntervalSeconds = 60) => {
const [currentTime, setCurrentTime] = useState(() => Date.now() / 1000);
useEffect(() => {
// Update immediately on mount
setCurrentTime(Date.now() / 1000);
// Then update periodically
const intervalId = setInterval(() => {
setCurrentTime(Date.now() / 1000);
}, updateIntervalSeconds * 1000);
return () => clearInterval(intervalId);
}, [updateIntervalSeconds]);
return currentTime;
};
+102
View File
@@ -220,6 +220,23 @@
--topbar-mobile-border-bottom: 2px solid #d9c5b7; --topbar-mobile-border-bottom: 2px solid #d9c5b7;
--topbar-mobile-font-size: 10px; --topbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: navy; --topbar-mobile-button-text-color: navy;
/* mod queue */
--mod-queue-table-border: 1px solid #d9bfb7;
--mod-queue-header-background-color: #f0e0d6;
--mod-queue-header-border-bottom: 2px solid #d9bfb7;
--mod-queue-row-background-color: #ffe;
--mod-queue-row-border-bottom: 1px solid #d9bfb7;
--mod-queue-row-hover-background-color: #f0e0d6;
--mod-queue-button-text-color: #00e;
--mod-queue-button-text-color-hover: red;
--mod-queue-button-text-decoration: underline;
--mod-queue-approve-color: #00e;
--mod-queue-approve-color-hover: green;
--mod-queue-reject-color: #00e;
--mod-queue-reject-color-hover: red;
--mod-queue-time-color: #707070;
--mod-queue-alert-color: red;
} }
:root .yotsuba-b { :root .yotsuba-b {
@@ -440,6 +457,23 @@
--topbar-mobile-border-bottom: 2px solid #b7c5d9; --topbar-mobile-border-bottom: 2px solid #b7c5d9;
--topbar-mobile-font-size: 10px; --topbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: #34345c; --topbar-mobile-button-text-color: #34345c;
/* mod queue */
--mod-queue-table-border: 1px solid #b7c5d9;
--mod-queue-header-background-color: #d6daf0;
--mod-queue-header-border-bottom: 2px solid #b7c5d9;
--mod-queue-row-background-color: #eef2ff;
--mod-queue-row-border-bottom: 1px solid #b7c5d9;
--mod-queue-row-hover-background-color: #d6daf0;
--mod-queue-button-text-color: #34345c;
--mod-queue-button-text-color-hover: #d00;
--mod-queue-button-text-decoration: underline;
--mod-queue-approve-color: #34345c;
--mod-queue-approve-color-hover: green;
--mod-queue-reject-color: #34345c;
--mod-queue-reject-color-hover: #d00;
--mod-queue-time-color: #707070;
--mod-queue-alert-color: #d00;
} }
:root .futaba { :root .futaba {
@@ -631,6 +665,23 @@
--topbar-mobile-border-bottom: 2px solid #d9c5b7; --topbar-mobile-border-bottom: 2px solid #d9c5b7;
--topbar-mobile-font-size: 9pt; --topbar-mobile-font-size: 9pt;
--topbar-mobile-button-text-color: #00f; --topbar-mobile-button-text-color: #00f;
/* mod queue */
--mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20);
--mod-queue-header-background-color: #f0e0d6;
--mod-queue-header-border-bottom: 1px solid rgba(0, 0, 0, 0.20);
--mod-queue-row-background-color: #ffe;
--mod-queue-row-border-bottom: 1px solid rgba(0, 0, 0, 0.20);
--mod-queue-row-hover-background-color: #f0e0d6;
--mod-queue-button-text-color: #00e;
--mod-queue-button-text-color-hover: red;
--mod-queue-button-text-decoration: underline;
--mod-queue-approve-color: #00e;
--mod-queue-approve-color-hover: green;
--mod-queue-reject-color: #00e;
--mod-queue-reject-color-hover: red;
--mod-queue-time-color: #707070;
--mod-queue-alert-color: red;
} }
:root .burichan { :root .burichan {
@@ -826,6 +877,23 @@
--topbar-mobile-border-bottom: 2px solid #b7c5d9; --topbar-mobile-border-bottom: 2px solid #b7c5d9;
--topbar-mobile-font-size: 9pt; --topbar-mobile-font-size: 9pt;
--topbar-mobile-button-text-color: #34345c; --topbar-mobile-button-text-color: #34345c;
/* mod queue */
--mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20);
--mod-queue-header-background-color: #d6daf0;
--mod-queue-header-border-bottom: 1px solid rgba(0, 0, 0, 0.20);
--mod-queue-row-background-color: #eef2ff;
--mod-queue-row-border-bottom: 1px solid rgba(0, 0, 0, 0.20);
--mod-queue-row-hover-background-color: #d6daf0;
--mod-queue-button-text-color: #34345c;
--mod-queue-button-text-color-hover: #d00;
--mod-queue-button-text-decoration: underline;
--mod-queue-approve-color: #34345c;
--mod-queue-approve-color-hover: green;
--mod-queue-reject-color: #34345c;
--mod-queue-reject-color-hover: #d00;
--mod-queue-time-color: #707070;
--mod-queue-alert-color: #d00;
} }
:root .tomorrow { :root .tomorrow {
@@ -1056,6 +1124,23 @@
--topbar-mobile-font-size: 10px; --topbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: #81a2be; --topbar-mobile-button-text-color: #81a2be;
--topbar-mobile-button-color: #81a2be; --topbar-mobile-button-color: #81a2be;
/* mod queue */
--mod-queue-table-border: 1px solid #111;
--mod-queue-header-background-color: #282a2e;
--mod-queue-header-border-bottom: 1px solid #111;
--mod-queue-row-background-color: #1d1f21;
--mod-queue-row-border-bottom: 1px solid #111;
--mod-queue-row-hover-background-color: #282a2e;
--mod-queue-button-text-color: #81a2be;
--mod-queue-button-text-color-hover: #5f89ac;
--mod-queue-button-text-decoration: underline;
--mod-queue-approve-color: #81a2be;
--mod-queue-approve-color-hover: #b5bd68;
--mod-queue-reject-color: #81a2be;
--mod-queue-reject-color-hover: #cc6666;
--mod-queue-time-color: #707070;
--mod-queue-alert-color: #cc6666;
} }
:root .photon { :root .photon {
@@ -1268,4 +1353,21 @@
--topbar-mobile-font-size: 10px; --topbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: #f60; --topbar-mobile-button-text-color: #f60;
--topbar-mobile-button-color: #f60; --topbar-mobile-button-color: #f60;
/* mod queue */
--mod-queue-table-border: 1px solid #ccc;
--mod-queue-header-background-color: #ddd;
--mod-queue-header-border-bottom: 1px solid #ccc;
--mod-queue-row-background-color: #eee;
--mod-queue-row-border-bottom: 1px solid #ccc;
--mod-queue-row-hover-background-color: #ddd;
--mod-queue-button-text-color: #f60;
--mod-queue-button-text-color-hover: #f30;
--mod-queue-button-text-decoration: underline;
--mod-queue-approve-color: #f60;
--mod-queue-approve-color-hover: green;
--mod-queue-reject-color: #f60;
--mod-queue-reject-color-hover: red;
--mod-queue-time-color: #707070;
--mod-queue-alert-color: red;
} }
+75 -50
View File
@@ -34,16 +34,20 @@
.alertThresholdInput { .alertThresholdInput {
width: 50px; width: 50px;
padding: 4px; padding: 4px;
border: 1px solid var(--border-color); border: var(--mod-queue-table-border);
background: var(--input-background-color); background: var(--mod-queue-header-background-color);
color: var(--text-color); color: var(--body-font-color);
}
.tableWrapper {
border: var(--mod-queue-table-border);
} }
.tableHeader { .tableHeader {
display: flex; display: flex;
padding: 8px; padding: 8px;
border-bottom: 2px solid var(--border-color); border-bottom: var(--mod-queue-header-border-bottom);
background: var(--header-background-color); background: var(--mod-queue-header-background-color);
font-weight: bold; font-weight: bold;
font-size: 13px; font-size: 13px;
} }
@@ -72,23 +76,26 @@
.empty { .empty {
padding: 20px; padding: 20px;
text-align: center; text-align: center;
color: var(--secondary-text-color); color: var(--mod-queue-time-color);
} }
/* ModQueueRow styles */ /* ModQueueRow styles */
.row { .row {
display: flex; display: flex;
padding: 4px 8px; padding: 4px 8px;
border-bottom: 1px solid var(--border-color); border-top: none;
border-left: none;
border-right: none;
border-bottom: var(--mod-queue-row-border-bottom);
align-items: center; align-items: center;
gap: 8px; gap: 8px;
background-color: var(--post-background-color); background-color: var(--mod-queue-row-background-color);
color: var(--text-color); color: var(--body-font-color);
font-size: 13px; font-size: 13px;
} }
.row:hover { .row:hover {
background-color: var(--post-highlight-color); background-color: var(--mod-queue-row-hover-background-color);
} }
.board { .board {
@@ -106,23 +113,24 @@
} }
.excerpt a { .excerpt a {
text-decoration: none; text-decoration: var(--post-link-text-decoration);
color: var(--link-color); color: var(--post-link-text-color);
} }
.excerpt a:hover { .excerpt a:hover {
text-decoration: underline; text-decoration: var(--post-link-text-decoration-hover);
color: var(--post-link-text-color-hover);
} }
.time { .time {
width: 80px; width: 80px;
flex-shrink: 0; flex-shrink: 0;
text-align: right; text-align: right;
color: var(--secondary-text-color); color: var(--mod-queue-time-color);
} }
.time.alert { .time.alert {
color: red; color: var(--mod-queue-alert-color);
font-weight: bold; font-weight: bold;
animation: blink 2s infinite; animation: blink 2s infinite;
} }
@@ -141,27 +149,39 @@
justify-content: flex-end; justify-content: flex-end;
} }
.button { /* Desktop: buttons styled as text links */
cursor: pointer; @media (min-width: 640px) {
padding: 2px 6px; .button {
border: 1px solid var(--border-color); all: unset;
background: var(--button-background); cursor: pointer;
color: var(--button-text); padding: 0;
font-size: 11px; color: var(--mod-queue-button-text-color);
text-decoration: none; font-size: 11px;
display: inline-block; text-decoration: var(--mod-queue-button-text-decoration);
} display: inline-block;
font-family: inherit;
}
.button:hover { .button:hover {
background: var(--button-hover-background); color: var(--mod-queue-button-text-color-hover);
text-decoration: var(--mod-queue-button-text-decoration);
}
} }
.approve { .approve {
color: green; color: var(--mod-queue-approve-color);
}
.approve:hover {
color: var(--mod-queue-approve-color-hover);
} }
.reject { .reject {
color: red; color: var(--mod-queue-reject-color);
}
.reject:hover {
color: var(--mod-queue-reject-color-hover);
} }
/* ModQueueBoardFilter styles */ /* ModQueueBoardFilter styles */
@@ -174,34 +194,31 @@
.filterSelect { .filterSelect {
padding: 4px; padding: 4px;
border: 1px solid var(--border-color); border: var(--mod-queue-table-border);
background: var(--input-background-color); background: var(--mod-queue-header-background-color);
color: var(--text-color); color: var(--body-font-color);
} }
/* ModQueueButton styles */ /* ModQueueButton styles */
.modQueueButton {
cursor: pointer;
display: inline-flex;
align-items: center;
margin-left: 8px;
text-decoration: none;
color: var(--text-color);
font-size: 13px;
}
.modQueueButton:hover {
text-decoration: underline;
}
.modQueueButtonCount { .modQueueButtonCount {
margin-left: 4px;
font-weight: bold; font-weight: bold;
} }
.modQueueButtonAlert { /* Number blinking, changing color to red */
color: red; .modQueueButtonCountAlert {
animation: blink 2s infinite; animation: blinkColor 2s infinite;
}
@keyframes blinkColor {
0% {
color: inherit;
}
50% {
color: var(--mod-queue-alert-color);
}
100% {
color: inherit;
}
} }
/* Mobile Layout */ /* Mobile Layout */
@@ -243,6 +260,14 @@
flex: 1; flex: 1;
text-align: center; text-align: center;
padding: 8px; padding: 8px;
border: var(--button-border-mobile);
background: var(--button-background-color-mobile);
color: var(--button-text-color-mobile);
text-decoration: none;
}
.button:hover {
background: var(--button-background-color-mobile);
} }
.header { .header {
+60 -23
View File
@@ -148,16 +148,25 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
const { alertThresholdHours } = useModQueueStore(); const { alertThresholdHours } = useModQueueStore();
const { accountSubplebbits } = useAccountSubplebbits(); const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = useMemo(() => Object.keys(accountSubplebbits || {}), [accountSubplebbits]); const accountSubplebbitAddresses = useMemo(() => Object.keys(accountSubplebbits || {}), [accountSubplebbits]);
const defaultSubplebbits = useDefaultSubplebbits();
// Resolve boardIdentifier to address if it exists
const resolvedAddress = useMemo(() => {
if (boardIdentifier) {
return getSubplebbitAddress(boardIdentifier, defaultSubplebbits);
}
return undefined;
}, [boardIdentifier, defaultSubplebbits]);
const subplebbitAddresses = useMemo(() => { const subplebbitAddresses = useMemo(() => {
if (boardIdentifier) { if (resolvedAddress) {
return [boardIdentifier]; return [resolvedAddress];
} }
return accountSubplebbitAddresses; return accountSubplebbitAddresses;
}, [boardIdentifier, accountSubplebbitAddresses]); }, [resolvedAddress, accountSubplebbitAddresses]);
// If specific board, check if user is mod // If specific board, check if user is mod using resolved address
const isModOfBoard = boardIdentifier ? accountSubplebbitAddresses.includes(boardIdentifier) : true; const isModOfBoard = resolvedAddress ? accountSubplebbitAddresses.includes(resolvedAddress) : true;
// Only fetch if we have addresses to check and permissions // Only fetch if we have addresses to check and permissions
const shouldFetch = subplebbitAddresses.length > 0 && isModOfBoard; const shouldFetch = subplebbitAddresses.length > 0 && isModOfBoard;
@@ -172,29 +181,57 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
return null; return null;
} }
const count = feed.length; // Separate comments into normal and urgent based on threshold
// Match ModQueueRow logic: use > (strictly greater) to be consistent
const { normalCount, urgentCount } = useMemo(() => {
const thresholdSeconds = alertThresholdHours * 3600;
const now = Date.now() / 1000;
const hasAlert = feed.some((item) => { let normal = 0;
const timeWaiting = Date.now() / 1000 - item.timestamp; let urgent = 0;
return timeWaiting > alertThresholdHours * 3600;
});
for (const item of feed) {
const timeWaiting = now - item.timestamp;
if (timeWaiting > thresholdSeconds) {
urgent++;
} else {
normal++;
}
}
return { normalCount: normal, urgentCount: urgent };
}, [feed, alertThresholdHours]);
const totalCount = normalCount + urgentCount;
// Use boardIdentifier for the route, but resolvedAddress for mod check
const to = boardIdentifier ? `/${boardIdentifier}/queue` : '/mod/queue'; const to = boardIdentifier ? `/${boardIdentifier}/queue` : '/mod/queue';
if (count === 0) {
// If we are on a specific board and the user is a mod, we still show the button
// If we are in global mod view, we show it too
return (
<Link to={to} className={styles.modQueueButton}>
[{t('mod_queue')}]
</Link>
);
}
return ( return (
<Link to={to} className={`${styles.modQueueButton} ${hasAlert ? styles.modQueueButtonAlert : ''}`}> <button className='button'>
[{t('mod_queue')} <span className={styles.modQueueButtonCount}>{count}</span>] <Link to={to}>
</Link> {t('mod_queue')}
{totalCount > 0 && (
<strong>
(
{urgentCount > 0 && normalCount > 0 ? (
<>
<span className={styles.modQueueButtonCount}>{normalCount}</span>
<span className={`${styles.modQueueButtonCount} ${styles.modQueueButtonCountAlert}`}>
{'+'}
{urgentCount}
</span>
</>
) : urgentCount > 0 ? (
<span className={`${styles.modQueueButtonCount} ${styles.modQueueButtonCountAlert}`}>{urgentCount}</span>
) : (
<span className={styles.modQueueButtonCount}>{totalCount}</span>
)}
)
</strong>
)}
</Link>
</button>
); );
}; };