From e899ac7fb95f96b41600e1283f34fdbe2cefb168 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 8 Jan 2026 16:43:14 +0100 Subject: [PATCH] update mod queue --- .../board-buttons/board-buttons.tsx | 8 ++ src/hooks/use-current-time.ts | 27 ++++ src/themes.css | 102 ++++++++++++++ src/views/mod-queue/mod-queue.module.css | 125 +++++++++++------- src/views/mod-queue/mod-queue.tsx | 83 ++++++++---- 5 files changed, 272 insertions(+), 73 deletions(-) create mode 100644 src/hooks/use-current-time.ts diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 52618310..6e5c2bae 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -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 && } {!(isInAllView || isInSubscriptionsView || isInModView) && } + {!(isInAllView || isInSubscriptionsView || isInModView) && } {isInCatalogView && searchText ? ( @@ -496,6 +498,12 @@ export const DesktopBoardButtons = () => { [] )} + {!(isInAllView || isInSubscriptionsView || isInModView) && ( + <> + {' '} + [] + + )} {isInCatalogView && searchText ? ( {' '} diff --git a/src/hooks/use-current-time.ts b/src/hooks/use-current-time.ts new file mode 100644 index 00000000..0e4d9c7b --- /dev/null +++ b/src/hooks/use-current-time.ts @@ -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; +}; diff --git a/src/themes.css b/src/themes.css index a8bed4a9..ef659eb5 100644 --- a/src/themes.css +++ b/src/themes.css @@ -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; } diff --git a/src/views/mod-queue/mod-queue.module.css b/src/views/mod-queue/mod-queue.module.css index 4c156806..7357b0a1 100644 --- a/src/views/mod-queue/mod-queue.module.css +++ b/src/views/mod-queue/mod-queue.module.css @@ -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 { diff --git a/src/views/mod-queue/mod-queue.tsx b/src/views/mod-queue/mod-queue.tsx index 5ec7e4be..9225123c 100644 --- a/src/views/mod-queue/mod-queue.tsx +++ b/src/views/mod-queue/mod-queue.tsx @@ -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 ( - - [{t('mod_queue')}] - - ); - } - return ( - - [{t('mod_queue')} {count}] - + ); };