mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
update mod queue
This commit is contained in:
@@ -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}>
|
||||
{' '}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user