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 CatalogSearch from '../catalog-search';
import Tooltip from '../tooltip';
import { ModQueueButton } from '../../views/mod-queue/mod-queue';
import styles from './board-buttons.module.css';
import _ from 'lodash';
@@ -362,6 +363,7 @@ export const MobileBoardButtons = () => {
)}
{showVoteButton && <VoteButton />}
{!(isInAllView || isInSubscriptionsView || isInModView) && <SubscribeButton address={subplebbitAddress} />}
{!(isInAllView || isInSubscriptionsView || isInModView) && <ModQueueButton boardIdentifier={boardIdentifier} isMobile={true} />}
<RefreshButton />
{isInCatalogView && searchText ? (
<span className={styles.filteredThreadsCount}>
@@ -496,6 +498,12 @@ export const DesktopBoardButtons = () => {
[<VoteButton />]
</>
)}
{!(isInAllView || isInSubscriptionsView || isInModView) && (
<>
{' '}
[<ModQueueButton boardIdentifier={boardIdentifier} isMobile={false} />]
</>
)}
{isInCatalogView && searchText ? (
<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-font-size: 10px;
--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 {
@@ -440,6 +457,23 @@
--topbar-mobile-border-bottom: 2px solid #b7c5d9;
--topbar-mobile-font-size: 10px;
--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 {
@@ -631,6 +665,23 @@
--topbar-mobile-border-bottom: 2px solid #d9c5b7;
--topbar-mobile-font-size: 9pt;
--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 {
@@ -826,6 +877,23 @@
--topbar-mobile-border-bottom: 2px solid #b7c5d9;
--topbar-mobile-font-size: 9pt;
--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 {
@@ -1056,6 +1124,23 @@
--topbar-mobile-font-size: 10px;
--topbar-mobile-button-text-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 {
@@ -1268,4 +1353,21 @@
--topbar-mobile-font-size: 10px;
--topbar-mobile-button-text-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 {
width: 50px;
padding: 4px;
border: 1px solid var(--border-color);
background: var(--input-background-color);
color: var(--text-color);
border: var(--mod-queue-table-border);
background: var(--mod-queue-header-background-color);
color: var(--body-font-color);
}
.tableWrapper {
border: var(--mod-queue-table-border);
}
.tableHeader {
display: flex;
padding: 8px;
border-bottom: 2px solid var(--border-color);
background: var(--header-background-color);
border-bottom: var(--mod-queue-header-border-bottom);
background: var(--mod-queue-header-background-color);
font-weight: bold;
font-size: 13px;
}
@@ -72,23 +76,26 @@
.empty {
padding: 20px;
text-align: center;
color: var(--secondary-text-color);
color: var(--mod-queue-time-color);
}
/* ModQueueRow styles */
.row {
display: flex;
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;
gap: 8px;
background-color: var(--post-background-color);
color: var(--text-color);
background-color: var(--mod-queue-row-background-color);
color: var(--body-font-color);
font-size: 13px;
}
.row:hover {
background-color: var(--post-highlight-color);
background-color: var(--mod-queue-row-hover-background-color);
}
.board {
@@ -106,23 +113,24 @@
}
.excerpt a {
text-decoration: none;
color: var(--link-color);
text-decoration: var(--post-link-text-decoration);
color: var(--post-link-text-color);
}
.excerpt a:hover {
text-decoration: underline;
text-decoration: var(--post-link-text-decoration-hover);
color: var(--post-link-text-color-hover);
}
.time {
width: 80px;
flex-shrink: 0;
text-align: right;
color: var(--secondary-text-color);
color: var(--mod-queue-time-color);
}
.time.alert {
color: red;
color: var(--mod-queue-alert-color);
font-weight: bold;
animation: blink 2s infinite;
}
@@ -141,27 +149,39 @@
justify-content: flex-end;
}
.button {
cursor: pointer;
padding: 2px 6px;
border: 1px solid var(--border-color);
background: var(--button-background);
color: var(--button-text);
font-size: 11px;
text-decoration: none;
display: inline-block;
}
/* Desktop: buttons styled as text links */
@media (min-width: 640px) {
.button {
all: unset;
cursor: pointer;
padding: 0;
color: var(--mod-queue-button-text-color);
font-size: 11px;
text-decoration: var(--mod-queue-button-text-decoration);
display: inline-block;
font-family: inherit;
}
.button:hover {
background: var(--button-hover-background);
.button:hover {
color: var(--mod-queue-button-text-color-hover);
text-decoration: var(--mod-queue-button-text-decoration);
}
}
.approve {
color: green;
color: var(--mod-queue-approve-color);
}
.approve:hover {
color: var(--mod-queue-approve-color-hover);
}
.reject {
color: red;
color: var(--mod-queue-reject-color);
}
.reject:hover {
color: var(--mod-queue-reject-color-hover);
}
/* ModQueueBoardFilter styles */
@@ -174,34 +194,31 @@
.filterSelect {
padding: 4px;
border: 1px solid var(--border-color);
background: var(--input-background-color);
color: var(--text-color);
border: var(--mod-queue-table-border);
background: var(--mod-queue-header-background-color);
color: var(--body-font-color);
}
/* 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 {
margin-left: 4px;
font-weight: bold;
}
.modQueueButtonAlert {
color: red;
animation: blink 2s infinite;
/* Number blinking, changing color to red */
.modQueueButtonCountAlert {
animation: blinkColor 2s infinite;
}
@keyframes blinkColor {
0% {
color: inherit;
}
50% {
color: var(--mod-queue-alert-color);
}
100% {
color: inherit;
}
}
/* Mobile Layout */
@@ -243,6 +260,14 @@
flex: 1;
text-align: center;
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 {
+60 -23
View File
@@ -148,16 +148,25 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
const { alertThresholdHours } = useModQueueStore();
const { accountSubplebbits } = useAccountSubplebbits();
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(() => {
if (boardIdentifier) {
return [boardIdentifier];
if (resolvedAddress) {
return [resolvedAddress];
}
return accountSubplebbitAddresses;
}, [boardIdentifier, accountSubplebbitAddresses]);
}, [resolvedAddress, accountSubplebbitAddresses]);
// If specific board, check if user is mod
const isModOfBoard = boardIdentifier ? accountSubplebbitAddresses.includes(boardIdentifier) : true;
// If specific board, check if user is mod using resolved address
const isModOfBoard = resolvedAddress ? accountSubplebbitAddresses.includes(resolvedAddress) : true;
// Only fetch if we have addresses to check and permissions
const shouldFetch = subplebbitAddresses.length > 0 && isModOfBoard;
@@ -172,29 +181,57 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
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) => {
const timeWaiting = Date.now() / 1000 - item.timestamp;
return timeWaiting > alertThresholdHours * 3600;
});
let normal = 0;
let urgent = 0;
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';
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 (
<Link to={to} className={`${styles.modQueueButton} ${hasAlert ? styles.modQueueButtonAlert : ''}`}>
[{t('mod_queue')} <span className={styles.modQueueButtonCount}>{count}</span>]
</Link>
<button className='button'>
<Link to={to}>
{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>
);
};