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}]
-
+
);
};