mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(mod-queue): add view mode selection and integrate with post components
This commit is contained in:
+2
-3
@@ -83,14 +83,13 @@ const BoardLayout = () => {
|
|||||||
<DisclaimerModal />
|
<DisclaimerModal />
|
||||||
<BoardHeader />
|
<BoardHeader />
|
||||||
{isMobile
|
{isMobile
|
||||||
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) &&
|
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress || isOnModQueueRoute) && (
|
||||||
!isOnModQueueRoute && (
|
|
||||||
<>
|
<>
|
||||||
<PostForm key={key} />
|
<PostForm key={key} />
|
||||||
<MobileBoardButtons />
|
<MobileBoardButtons />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
|
: (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress || isOnModQueueRoute) && (
|
||||||
<>
|
<>
|
||||||
<PostForm key={key} />
|
<PostForm key={key} />
|
||||||
{!(isInAllView || isInSubscriptionsView || isInModView) && !isOnModQueueRoute && <SubplebbitStats />}
|
{!(isInAllView || isInSubscriptionsView || isInModView) && !isOnModQueueRoute && <SubplebbitStats />}
|
||||||
|
|||||||
@@ -66,6 +66,30 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modQueueControls {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modQueueControls label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertThresholdInput {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileBoardButtons .modQueueControls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.desktopBoardButtons .rightSideButtons select {
|
.desktopBoardButtons .rightSideButtons select {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import useCatalogStyleStore from '../../stores/use-catalog-style-store';
|
|||||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||||
import useSortingStore from '../../stores/use-sorting-store';
|
import useSortingStore from '../../stores/use-sorting-store';
|
||||||
import useAllFeedFilterStore from '../../stores/use-all-feed-filter-store';
|
import useAllFeedFilterStore from '../../stores/use-all-feed-filter-store';
|
||||||
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
|
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import useTimeFilter from '../../hooks/use-time-filter';
|
import useTimeFilter from '../../hooks/use-time-filter';
|
||||||
@@ -229,6 +230,58 @@ const ImageSizeOptions = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ModQueueAlertThreshold = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { alertThresholdValue, alertThresholdUnit, setAlertThreshold } = useModQueueStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modQueueControls}>
|
||||||
|
<label>
|
||||||
|
{t('alert_threshold')}:
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
min='1'
|
||||||
|
step='1'
|
||||||
|
value={alertThresholdValue}
|
||||||
|
onChange={(e) => setAlertThreshold(Number(e.target.value), alertThresholdUnit)}
|
||||||
|
className={styles.alertThresholdInput}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={alertThresholdUnit}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newUnit = e.target.value as 'hours' | 'minutes';
|
||||||
|
const newValue =
|
||||||
|
alertThresholdUnit === 'hours' && newUnit === 'minutes'
|
||||||
|
? alertThresholdValue * 60
|
||||||
|
: alertThresholdUnit === 'minutes' && newUnit === 'hours'
|
||||||
|
? Math.round(alertThresholdValue / 60)
|
||||||
|
: alertThresholdValue;
|
||||||
|
setAlertThreshold(Math.max(1, newValue), newUnit);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value='minutes'>{t('minutes')}</option>
|
||||||
|
<option value='hours'>{t('hours')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModQueueViewSelector = () => {
|
||||||
|
const { viewMode, setViewMode } = useModQueueStore();
|
||||||
|
return (
|
||||||
|
<div className={styles.modQueueControls}>
|
||||||
|
<label>
|
||||||
|
View:
|
||||||
|
<select value={viewMode} onChange={(e) => setViewMode(e.target.value as 'compact' | 'feed')}>
|
||||||
|
<option value='compact'>Compact</option>
|
||||||
|
<option value='feed'>Feed</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ShowOPCommentOption = () => {
|
const ShowOPCommentOption = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showOPComment, setShowOPComment } = useCatalogStyleStore();
|
const { showOPComment, setShowOPComment } = useCatalogStyleStore();
|
||||||
@@ -353,6 +406,8 @@ export const MobileBoardButtons = () => {
|
|||||||
isInModQueueView={isInModQueueView}
|
isInModQueueView={isInModQueueView}
|
||||||
/>
|
/>
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
|
<ModQueueAlertThreshold />
|
||||||
|
<ModQueueViewSelector />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -476,6 +531,10 @@ export const DesktopBoardButtons = () => {
|
|||||||
/>
|
/>
|
||||||
] [
|
] [
|
||||||
<RefreshButton />]
|
<RefreshButton />]
|
||||||
|
<span className={styles.rightSideButtons}>
|
||||||
|
<ModQueueAlertThreshold />
|
||||||
|
<ModQueueViewSelector />
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDim
|
|||||||
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
|
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
|
||||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||||
import { isValidURL } from '../../lib/utils/url-utils';
|
import { isValidURL } from '../../lib/utils/url-utils';
|
||||||
import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||||
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
|
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
|
||||||
@@ -54,7 +55,19 @@ const useShowOmittedReplies = create<ShowOmittedRepliesState>((set) => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: PostProps) => {
|
const PostInfo = ({
|
||||||
|
post,
|
||||||
|
postReplyCount = 0,
|
||||||
|
roles,
|
||||||
|
isHidden,
|
||||||
|
threadNumber,
|
||||||
|
isModQueue,
|
||||||
|
modQueueStatus,
|
||||||
|
modQueueError,
|
||||||
|
isPublishing,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
}: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
|
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
|
||||||
const title = post?.title?.trim();
|
const title = post?.title?.trim();
|
||||||
@@ -74,6 +87,17 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||||
|
const isInModQueueView = isModQueueView(location.pathname);
|
||||||
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
|
||||||
|
// Check if post is awaiting approval and over threshold (for mod queue view)
|
||||||
|
const approved = post?.approved;
|
||||||
|
const alreadyApproved = approved === true;
|
||||||
|
const alreadyRejected = removed === true;
|
||||||
|
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
||||||
|
const timeWaiting = timestamp ? Date.now() / 1000 - timestamp : 0;
|
||||||
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
const userID = address && Plebbit.getShortAddress({ address }); // should not be shortened to less than 12 characters, because users can create unlimited addresses/IDs before authenticating or passing challenges, so if the ID is short enough they can spoof it to troll users with the same ID
|
const userID = address && Plebbit.getShortAddress({ address }); // should not be shortened to less than 12 characters, because users can create unlimited addresses/IDs before authenticating or passing challenges, so if the ID is short enough they can spoof it to troll users with the same ID
|
||||||
const userIDBackgroundColor = hashStringToColor(userID);
|
const userIDBackgroundColor = hashStringToColor(userID);
|
||||||
@@ -169,7 +193,14 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
|
|||||||
){' '}
|
){' '}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.dateTime}>
|
<span className={styles.dateTime}>
|
||||||
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />{' '}
|
{isInModQueueView && isOverThreshold ? (
|
||||||
|
<>
|
||||||
|
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} /> (
|
||||||
|
<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />
|
||||||
|
)}{' '}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.postNum}>
|
<span className={styles.postNum}>
|
||||||
{cid ? (
|
{cid ? (
|
||||||
@@ -204,7 +235,7 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
|
|||||||
<img src='assets/icons/closed.gif' alt='' className={styles.closedIcon} title={t('closed')} />
|
<img src='assets/icons/closed.gif' alt='' className={styles.closedIcon} title={t('closed')} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isInPostPageView && !isReply && !isHidden && (
|
{!isInPostPageView && !isReply && !isHidden && !isModQueue && (
|
||||||
<span className={styles.replyButton}>
|
<span className={styles.replyButton}>
|
||||||
[
|
[
|
||||||
<Link to={boardPath ? `/${boardPath}/thread/${postCid}` : `/thread/${postCid}`} onClick={(e) => !cid && e.preventDefault()}>
|
<Link to={boardPath ? `/${boardPath}/thread/${postCid}` : `/thread/${postCid}`} onClick={(e) => !cid && e.preventDefault()}>
|
||||||
@@ -213,8 +244,41 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
|
|||||||
]
|
]
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{isModQueue && (
|
||||||
|
<span className={styles.modQueueActions}>
|
||||||
|
{modQueueStatus === 'approved' ? (
|
||||||
|
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
|
||||||
|
) : modQueueStatus === 'rejected' ? (
|
||||||
|
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
|
||||||
|
) : modQueueStatus === 'failed' ? (
|
||||||
|
<span className={styles.modQueueStatusRejected}>
|
||||||
|
{t('failed')}
|
||||||
|
{modQueueError ? `: ${modQueueError}` : ''}
|
||||||
|
</span>
|
||||||
|
) : isPublishing ? (
|
||||||
|
<LoadingEllipsis string={t('publishing')} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className={styles.modQueueButtonWrapper}>
|
||||||
|
[
|
||||||
|
<button className={styles.modQueueActionButton} onClick={onApprove} disabled={isPublishing}>
|
||||||
|
{t('approve')}
|
||||||
|
</button>
|
||||||
|
]
|
||||||
|
</span>
|
||||||
|
<span className={styles.modQueueButtonWrapper}>
|
||||||
|
[
|
||||||
|
<button className={styles.modQueueActionButton} onClick={onReject} disabled={isPublishing}>
|
||||||
|
{t('reject')}
|
||||||
|
</button>
|
||||||
|
]
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{!(removed || deleted) && <PostMenuDesktop postMenu={postMenuProps} />}
|
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
|
||||||
{cid &&
|
{cid &&
|
||||||
parentCid &&
|
parentCid &&
|
||||||
replies &&
|
replies &&
|
||||||
@@ -376,7 +440,19 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, targetReplyCid }: PostProps) => {
|
const PostDesktop = ({
|
||||||
|
post,
|
||||||
|
roles,
|
||||||
|
showAllReplies,
|
||||||
|
showReplies = true,
|
||||||
|
targetReplyCid,
|
||||||
|
isModQueue,
|
||||||
|
modQueueStatus,
|
||||||
|
modQueueError,
|
||||||
|
isPublishing,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
}: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, content, deleted, link, linkHeight, linkWidth, pinned, postCid, removed, spoiler, state, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
|
const { author, cid, content, deleted, link, linkHeight, linkWidth, pinned, postCid, removed, spoiler, state, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -450,7 +526,7 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, targetRe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.postDesktop}>
|
<div className={styles.postDesktop}>
|
||||||
{showReplies ? (
|
{showReplies || isModQueue ? (
|
||||||
<div className={styles.hrWrapper}>
|
<div className={styles.hrWrapper}>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +561,19 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, targetRe
|
|||||||
isInSubscriptionsView={isInSubscriptionsView}
|
isInSubscriptionsView={isInSubscriptionsView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<PostInfo isHidden={hidden} post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
|
<PostInfo
|
||||||
|
isHidden={hidden}
|
||||||
|
post={post}
|
||||||
|
postReplyCount={replyCount}
|
||||||
|
roles={roles}
|
||||||
|
threadNumber={post?.number}
|
||||||
|
isModQueue={isModQueue}
|
||||||
|
modQueueStatus={modQueueStatus}
|
||||||
|
modQueueError={modQueueError}
|
||||||
|
isPublishing={isPublishing}
|
||||||
|
onApprove={onApprove}
|
||||||
|
onReject={onReject}
|
||||||
|
/>
|
||||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||||
{!isHidden && <CommentContent comment={post} />}
|
{!isHidden && <CommentContent comment={post} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,18 @@
|
|||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modQueueTitle {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.modQueueTitle {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.closed {
|
.closed {
|
||||||
font-size: x-large;
|
font-size: x-large;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/s
|
|||||||
import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils';
|
import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils';
|
||||||
import { formatMarkdown } from '../../lib/utils/post-utils';
|
import { formatMarkdown } from '../../lib/utils/post-utils';
|
||||||
import { isValidURL } from '../../lib/utils/url-utils';
|
import { isValidURL } from '../../lib/utils/url-utils';
|
||||||
import { isAllView, isCatalogView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||||
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
||||||
@@ -373,6 +373,7 @@ const PostForm = () => {
|
|||||||
const isInPostView = isPostPageView(location.pathname, params);
|
const isInPostView = isPostPageView(location.pathname, params);
|
||||||
const isInAllView = isAllView(location.pathname);
|
const isInAllView = isAllView(location.pathname);
|
||||||
const isInModView = isModView(location.pathname);
|
const isInModView = isModView(location.pathname);
|
||||||
|
const isInModQueueView = isModQueueView(location.pathname);
|
||||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||||
const isInCatalogView = isCatalogView(location.pathname, params);
|
const isInCatalogView = isCatalogView(location.pathname, params);
|
||||||
|
|
||||||
@@ -398,7 +399,9 @@ const PostForm = () => {
|
|||||||
<>
|
<>
|
||||||
<div className={styles.postFormDesktop}>
|
<div className={styles.postFormDesktop}>
|
||||||
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
|
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
|
||||||
{isThreadClosed ? (
|
{isInModQueueView ? (
|
||||||
|
<div className={styles.modQueueTitle}>{t('moderation_queue')}</div>
|
||||||
|
) : isThreadClosed ? (
|
||||||
<div className={styles.closed}>
|
<div className={styles.closed}>
|
||||||
{t('thread_closed')}
|
{t('thread_closed')}
|
||||||
<br />
|
<br />
|
||||||
@@ -418,7 +421,9 @@ const PostForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.postFormMobile}>
|
<div className={styles.postFormMobile}>
|
||||||
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
|
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
|
||||||
{isThreadClosed ? (
|
{isInModQueueView ? (
|
||||||
|
<div className={styles.modQueueTitle}>{t('moderation_queue')}</div>
|
||||||
|
) : isThreadClosed ? (
|
||||||
<div className={styles.closed}>
|
<div className={styles.closed}>
|
||||||
{t('thread_closed')}
|
{t('thread_closed')}
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { shouldShowSnow } from '../../lib/snow';
|
|||||||
import { getHasThumbnail } from '../../lib/utils/media-utils';
|
import { getHasThumbnail } from '../../lib/utils/media-utils';
|
||||||
import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils';
|
import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils';
|
||||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||||
import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||||
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
|
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
|
||||||
@@ -52,10 +53,21 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
const isInAllView = isAllView(location.pathname);
|
const isInAllView = isAllView(location.pathname);
|
||||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||||
|
const isInModQueueView = isModQueueView(location.pathname);
|
||||||
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
|
||||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||||
|
|
||||||
|
// Check if post is awaiting approval and over threshold (for mod queue view)
|
||||||
|
const approved = post?.approved;
|
||||||
|
const alreadyApproved = approved === true;
|
||||||
|
const alreadyRejected = removed === true;
|
||||||
|
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
||||||
|
const timeWaiting = timestamp ? Date.now() / 1000 - timestamp : 0;
|
||||||
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
const stateString = useStateString(post);
|
const stateString = useStateString(post);
|
||||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||||
|
|
||||||
@@ -176,7 +188,14 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
<Link to={`/${boardPath}`}>Board: {boardPath}</Link>
|
<Link to={`/${boardPath}`}>Board: {boardPath}</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />{' '}
|
{isInModQueueView && isOverThreshold ? (
|
||||||
|
<>
|
||||||
|
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} /> (
|
||||||
|
<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />
|
||||||
|
)}{' '}
|
||||||
{cid ? (
|
{cid ? (
|
||||||
<span className={styles.postNumLink}>
|
<span className={styles.postNumLink}>
|
||||||
<Link
|
<Link
|
||||||
@@ -283,7 +302,19 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostMobile = ({ post, roles, showAllReplies, showReplies = true, targetReplyCid }: PostProps) => {
|
const PostMobile = ({
|
||||||
|
post,
|
||||||
|
roles,
|
||||||
|
showAllReplies,
|
||||||
|
showReplies = true,
|
||||||
|
targetReplyCid,
|
||||||
|
isModQueue,
|
||||||
|
modQueueStatus,
|
||||||
|
modQueueError,
|
||||||
|
isPublishing,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
}: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, pinned, postCid, replyCount, state, subplebbitAddress } = post || {};
|
const { author, cid, pinned, postCid, replyCount, state, subplebbitAddress } = post || {};
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -356,12 +387,12 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, targetRep
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.postMobile}>
|
<div className={styles.postMobile}>
|
||||||
{showReplies && (
|
{(showReplies || isModQueue) && (
|
||||||
<div className={styles.hrWrapper}>
|
<div className={styles.hrWrapper}>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={showReplies ? styles.thread : styles.quotePreview}>
|
<div className={showReplies || isModQueue ? styles.thread : styles.quotePreview}>
|
||||||
<div className={styles.postContainer}>
|
<div className={styles.postContainer}>
|
||||||
<div
|
<div
|
||||||
className={`${styles.postOp} ${shouldShowSnow() ? styles.xmasHatWrapper : ''}`}
|
className={`${styles.postOp} ${shouldShowSnow() ? styles.xmasHatWrapper : ''}`}
|
||||||
@@ -373,15 +404,41 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, targetRep
|
|||||||
<PostInfoAndMedia post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
|
<PostInfoAndMedia post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
|
||||||
<CommentContent comment={post} />
|
<CommentContent comment={post} />
|
||||||
</div>
|
</div>
|
||||||
{!isInPostView && !isInPendingPostView && showReplies && (
|
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
|
||||||
<div className={styles.postLink}>
|
<div className={styles.postLink}>
|
||||||
<span className={styles.info}>
|
<span className={styles.info}>
|
||||||
{replyCount > 0 && `${replyCount} Replies`}
|
{replyCount > 0 && `${replyCount} Replies`}
|
||||||
{linksCount > 0 && ` / ${linksCount} Links`}
|
{linksCount > 0 && ` / ${linksCount} Links`}
|
||||||
</span>
|
</span>
|
||||||
<Link to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`} className='button'>
|
{isModQueue ? (
|
||||||
{t('view_thread')}
|
<div className={styles.modQueueActions}>
|
||||||
</Link>
|
{modQueueStatus === 'approved' ? (
|
||||||
|
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
|
||||||
|
) : modQueueStatus === 'rejected' ? (
|
||||||
|
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
|
||||||
|
) : modQueueStatus === 'failed' ? (
|
||||||
|
<span className={styles.modQueueStatusRejected}>
|
||||||
|
{t('failed')}
|
||||||
|
{modQueueError ? `: ${modQueueError}` : ''}
|
||||||
|
</span>
|
||||||
|
) : isPublishing ? (
|
||||||
|
<LoadingEllipsis string={t('publishing')} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button className={`button ${styles.approveButton}`} onClick={onApprove} disabled={isPublishing}>
|
||||||
|
{t('approve')}
|
||||||
|
</button>
|
||||||
|
<button className={`button ${styles.rejectButton}`} onClick={onReject} disabled={isPublishing}>
|
||||||
|
{t('reject')}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`} className='button'>
|
||||||
|
{t('view_thread')}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { create } from 'zustand';
|
|||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
export type AlertThresholdUnit = 'hours' | 'minutes';
|
export type AlertThresholdUnit = 'hours' | 'minutes';
|
||||||
|
export type ModQueueViewMode = 'compact' | 'feed';
|
||||||
|
|
||||||
interface ModQueueState {
|
interface ModQueueState {
|
||||||
alertThresholdValue: number;
|
alertThresholdValue: number;
|
||||||
alertThresholdUnit: AlertThresholdUnit;
|
alertThresholdUnit: AlertThresholdUnit;
|
||||||
selectedBoardFilter: string | null;
|
selectedBoardFilter: string | null;
|
||||||
|
viewMode: ModQueueViewMode;
|
||||||
setAlertThreshold: (value: number, unit: AlertThresholdUnit) => void;
|
setAlertThreshold: (value: number, unit: AlertThresholdUnit) => void;
|
||||||
setSelectedBoardFilter: (boardAddress: string | null) => void;
|
setSelectedBoardFilter: (boardAddress: string | null) => void;
|
||||||
|
setViewMode: (viewMode: ModQueueViewMode) => void;
|
||||||
// Helper to get threshold in seconds for calculations
|
// Helper to get threshold in seconds for calculations
|
||||||
getAlertThresholdSeconds: () => number;
|
getAlertThresholdSeconds: () => number;
|
||||||
}
|
}
|
||||||
@@ -19,10 +22,11 @@ interface OldPersistedState {
|
|||||||
alertThresholdValue?: number;
|
alertThresholdValue?: number;
|
||||||
alertThresholdUnit?: AlertThresholdUnit;
|
alertThresholdUnit?: AlertThresholdUnit;
|
||||||
selectedBoardFilter?: string | null;
|
selectedBoardFilter?: string | null;
|
||||||
|
viewMode?: ModQueueViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type for persisted data (without methods)
|
// Type for persisted data (without methods)
|
||||||
type PersistedModQueueData = Pick<ModQueueState, 'alertThresholdValue' | 'alertThresholdUnit' | 'selectedBoardFilter'>;
|
type PersistedModQueueData = Pick<ModQueueState, 'alertThresholdValue' | 'alertThresholdUnit' | 'selectedBoardFilter' | 'viewMode'>;
|
||||||
|
|
||||||
const useModQueueStore = create<ModQueueState>()(
|
const useModQueueStore = create<ModQueueState>()(
|
||||||
persist(
|
persist(
|
||||||
@@ -30,8 +34,10 @@ const useModQueueStore = create<ModQueueState>()(
|
|||||||
alertThresholdValue: 6,
|
alertThresholdValue: 6,
|
||||||
alertThresholdUnit: 'hours' as AlertThresholdUnit,
|
alertThresholdUnit: 'hours' as AlertThresholdUnit,
|
||||||
selectedBoardFilter: null,
|
selectedBoardFilter: null,
|
||||||
|
viewMode: 'compact',
|
||||||
setAlertThreshold: (value, unit) => set({ alertThresholdValue: value, alertThresholdUnit: unit }),
|
setAlertThreshold: (value, unit) => set({ alertThresholdValue: value, alertThresholdUnit: unit }),
|
||||||
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }),
|
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }),
|
||||||
|
setViewMode: (viewMode) => set({ viewMode }),
|
||||||
getAlertThresholdSeconds: () => {
|
getAlertThresholdSeconds: () => {
|
||||||
const { alertThresholdValue, alertThresholdUnit } = get();
|
const { alertThresholdValue, alertThresholdUnit } = get();
|
||||||
return alertThresholdUnit === 'hours' ? alertThresholdValue * 3600 : alertThresholdValue * 60;
|
return alertThresholdUnit === 'hours' ? alertThresholdValue * 3600 : alertThresholdValue * 60;
|
||||||
@@ -39,7 +45,7 @@ const useModQueueStore = create<ModQueueState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'mod-queue-storage',
|
name: 'mod-queue-storage',
|
||||||
version: 1,
|
version: 2,
|
||||||
// Migrate old alertThresholdHours format to new alertThresholdValue/alertThresholdUnit format
|
// Migrate old alertThresholdHours format to new alertThresholdValue/alertThresholdUnit format
|
||||||
migrate: (persistedState, version): ModQueueState => {
|
migrate: (persistedState, version): ModQueueState => {
|
||||||
const state = persistedState as OldPersistedState;
|
const state = persistedState as OldPersistedState;
|
||||||
@@ -48,6 +54,7 @@ const useModQueueStore = create<ModQueueState>()(
|
|||||||
alertThresholdValue: state.alertThresholdHours,
|
alertThresholdValue: state.alertThresholdHours,
|
||||||
alertThresholdUnit: 'hours' as AlertThresholdUnit,
|
alertThresholdUnit: 'hours' as AlertThresholdUnit,
|
||||||
selectedBoardFilter: state.selectedBoardFilter ?? null,
|
selectedBoardFilter: state.selectedBoardFilter ?? null,
|
||||||
|
viewMode: state.viewMode ?? 'compact',
|
||||||
};
|
};
|
||||||
// Zustand will merge this with the store definition (which includes methods)
|
// Zustand will merge this with the store definition (which includes methods)
|
||||||
return migrated as ModQueueState;
|
return migrated as ModQueueState;
|
||||||
@@ -57,6 +64,7 @@ const useModQueueStore = create<ModQueueState>()(
|
|||||||
alertThresholdValue: state.alertThresholdValue ?? 6,
|
alertThresholdValue: state.alertThresholdValue ?? 6,
|
||||||
alertThresholdUnit: state.alertThresholdUnit ?? 'hours',
|
alertThresholdUnit: state.alertThresholdUnit ?? 'hours',
|
||||||
selectedBoardFilter: state.selectedBoardFilter ?? null,
|
selectedBoardFilter: state.selectedBoardFilter ?? null,
|
||||||
|
viewMode: state.viewMode ?? 'compact',
|
||||||
};
|
};
|
||||||
return current as ModQueueState;
|
return current as ModQueueState;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -233,6 +233,52 @@
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobileCard {
|
||||||
|
border: var(--mod-queue-table-border);
|
||||||
|
background: var(--post-mobile-background-color, transparent);
|
||||||
|
padding: 8px;
|
||||||
|
margin: 6px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardNumber {
|
||||||
|
color: var(--body-font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTime {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
font-size: 9pt;
|
||||||
|
color: var(--body-font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent a {
|
||||||
|
text-decoration: var(--post-link-text-decoration);
|
||||||
|
color: var(--post-link-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent a:hover {
|
||||||
|
text-decoration: var(--post-link-text-decoration-hover);
|
||||||
|
color: var(--post-link-text-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardActions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Desktop: buttons styled as text links (same as board buttons) */
|
/* Desktop: buttons styled as text links (same as board buttons) */
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.actions {
|
.actions {
|
||||||
|
|||||||
+275
-117
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useState, useEffect } from 'react';
|
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||||
@@ -21,6 +21,7 @@ import useChallengesStore from '../../stores/use-challenges-store';
|
|||||||
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||||
import Tooltip from '../../components/tooltip';
|
import Tooltip from '../../components/tooltip';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
|
import { Post } from '../post/post';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -55,30 +56,22 @@ interface ModQueueRowProps {
|
|||||||
// Track which action was initiated to show appropriate completion message
|
// Track which action was initiated to show appropriate completion message
|
||||||
type ModerationAction = 'approve' | 'reject' | null;
|
type ModerationAction = 'approve' | 'reject' | null;
|
||||||
|
|
||||||
const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
interface ModQueueActionState {
|
||||||
|
status: 'approved' | 'rejected' | 'failed' | null;
|
||||||
|
errorMessage?: string;
|
||||||
|
isPublishing: boolean;
|
||||||
|
handleApprove: () => Promise<void>;
|
||||||
|
handleReject: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useModQueueActions = (comment: Comment): ModQueueActionState => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getAlertThresholdSeconds } = useModQueueStore();
|
const { cid, subplebbitAddress, approved, removed } = comment || {};
|
||||||
const [initiatedAction, setInitiatedAction] = useState<ModerationAction>(null);
|
const [initiatedAction, setInitiatedAction] = useState<ModerationAction>(null);
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const { editedComment } = useEditedComment({ comment });
|
|
||||||
const displayComment = editedComment || comment;
|
|
||||||
|
|
||||||
const { content, title, timestamp, subplebbitAddress, cid, shortCid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment;
|
|
||||||
|
|
||||||
// Check if already moderated (from previous session or API update)
|
|
||||||
// Note: `approved` and `removed` are direct fields on the comment from CommentUpdate,
|
|
||||||
// not nested under commentModeration (which is the options object for publishing moderation actions)
|
|
||||||
const alreadyApproved = approved === true;
|
const alreadyApproved = approved === true;
|
||||||
const alreadyRejected = removed === true;
|
const alreadyRejected = removed === true;
|
||||||
|
|
||||||
const timeWaiting = Date.now() / 1000 - timestamp;
|
|
||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
|
||||||
const isOverThreshold = timeWaiting > alertThresholdSeconds;
|
|
||||||
|
|
||||||
// Only show alert animation for comments awaiting approval (not approved or rejected)
|
|
||||||
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
publishCommentModeration: approve,
|
publishCommentModeration: approve,
|
||||||
state: approveState,
|
state: approveState,
|
||||||
@@ -117,8 +110,7 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleApprove = async () => {
|
const handleApprove = useCallback(async () => {
|
||||||
// Double confirmation for approve action
|
|
||||||
const confirm = window.confirm(t('double_confirm'));
|
const confirm = window.confirm(t('double_confirm'));
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
return;
|
return;
|
||||||
@@ -130,10 +122,9 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
}, [approve, t]);
|
||||||
|
|
||||||
const handleReject = async () => {
|
const handleReject = useCallback(async () => {
|
||||||
// Double confirmation for reject action
|
|
||||||
const confirm = window.confirm(t('double_confirm'));
|
const confirm = window.confirm(t('double_confirm'));
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
return;
|
return;
|
||||||
@@ -145,9 +136,8 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
}, [reject, t]);
|
||||||
|
|
||||||
// Determine the current moderation state based on which action was initiated
|
|
||||||
const isApproving = initiatedAction === 'approve' && approveState !== 'initializing' && approveState !== 'succeeded' && approveState !== 'failed';
|
const isApproving = initiatedAction === 'approve' && approveState !== 'initializing' && approveState !== 'succeeded' && approveState !== 'failed';
|
||||||
const isRejecting = initiatedAction === 'reject' && rejectState !== 'initializing' && rejectState !== 'succeeded' && rejectState !== 'failed';
|
const isRejecting = initiatedAction === 'reject' && rejectState !== 'initializing' && rejectState !== 'succeeded' && rejectState !== 'failed';
|
||||||
const isPublishing = isApproving || isRejecting;
|
const isPublishing = isApproving || isRejecting;
|
||||||
@@ -158,6 +148,37 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
const approveFailed = initiatedAction === 'approve' && approveState === 'failed';
|
const approveFailed = initiatedAction === 'approve' && approveState === 'failed';
|
||||||
const rejectFailed = initiatedAction === 'reject' && rejectState === 'failed';
|
const rejectFailed = initiatedAction === 'reject' && rejectState === 'failed';
|
||||||
|
|
||||||
|
const status = alreadyApproved || approveSucceeded ? 'approved' : alreadyRejected || rejectSucceeded ? 'rejected' : approveFailed || rejectFailed ? 'failed' : null;
|
||||||
|
const errorMessage = approveFailed ? approveError?.message : rejectFailed ? rejectError?.message : undefined;
|
||||||
|
|
||||||
|
return { status, errorMessage, isPublishing, handleApprove, handleReject };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const { editedComment } = useEditedComment({ comment });
|
||||||
|
const displayComment = editedComment || comment;
|
||||||
|
|
||||||
|
const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment;
|
||||||
|
|
||||||
|
// Check if already moderated (from previous session or API update)
|
||||||
|
// Note: `approved` and `removed` are direct fields on the comment from CommentUpdate,
|
||||||
|
// not nested under commentModeration (which is the options object for publishing moderation actions)
|
||||||
|
const alreadyApproved = approved === true;
|
||||||
|
const alreadyRejected = removed === true;
|
||||||
|
|
||||||
|
const timeWaiting = Date.now() / 1000 - timestamp;
|
||||||
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
|
const isOverThreshold = timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
|
// Only show alert animation for comments awaiting approval (not approved or rejected)
|
||||||
|
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
|
||||||
|
|
||||||
|
const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
|
||||||
|
|
||||||
const boardPath = useBoardPath(subplebbitAddress);
|
const boardPath = useBoardPath(subplebbitAddress);
|
||||||
const hasTitle = title && title.trim().length > 0;
|
const hasTitle = title && title.trim().length > 0;
|
||||||
const hasContent = content && content.trim().length > 0;
|
const hasContent = content && content.trim().length > 0;
|
||||||
@@ -176,23 +197,17 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
// Render the status or action buttons
|
// Render the status or action buttons
|
||||||
const renderActions = () => {
|
const renderActions = () => {
|
||||||
// Check existing moderation state first (from API/previous sessions)
|
// Check existing moderation state first (from API/previous sessions)
|
||||||
if (alreadyApproved || approveSucceeded) {
|
if (status === 'approved') {
|
||||||
return <span className={`${styles.button} ${styles.approve}`}>{t('approved')}</span>;
|
return <span className={`${styles.button} ${styles.approve}`}>{t('approved')}</span>;
|
||||||
}
|
}
|
||||||
if (alreadyRejected || rejectSucceeded) {
|
if (status === 'rejected') {
|
||||||
return <span className={`${styles.button} ${styles.reject}`}>{t('rejected')}</span>;
|
return <span className={`${styles.button} ${styles.reject}`}>{t('rejected')}</span>;
|
||||||
}
|
}
|
||||||
if (approveFailed) {
|
if (status === 'failed') {
|
||||||
return (
|
return (
|
||||||
<span className={`${styles.button} ${styles.reject}`}>
|
<span className={`${styles.button} ${styles.reject}`}>
|
||||||
{t('failed')}: {approveError?.message}
|
{t('failed')}
|
||||||
</span>
|
{errorMessage ? `: ${errorMessage}` : ''}
|
||||||
);
|
|
||||||
}
|
|
||||||
if (rejectFailed) {
|
|
||||||
return (
|
|
||||||
<span className={`${styles.button} ${styles.reject}`}>
|
|
||||||
{t('failed')}: {rejectError?.message}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -258,6 +273,137 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ModQueueCardProps {
|
||||||
|
comment: Comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModQueueCard = ({ comment }: ModQueueCardProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
|
||||||
|
const { editedComment } = useEditedComment({ comment });
|
||||||
|
const displayComment = editedComment || comment;
|
||||||
|
|
||||||
|
const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment;
|
||||||
|
|
||||||
|
const alreadyApproved = approved === true;
|
||||||
|
const alreadyRejected = removed === true;
|
||||||
|
|
||||||
|
const timeWaiting = Date.now() / 1000 - timestamp;
|
||||||
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
|
const isOverThreshold = timeWaiting > alertThresholdSeconds;
|
||||||
|
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
|
||||||
|
|
||||||
|
const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
|
||||||
|
|
||||||
|
const boardPath = useBoardPath(subplebbitAddress);
|
||||||
|
const hasTitle = title && title.trim().length > 0;
|
||||||
|
const hasContent = content && content.trim().length > 0;
|
||||||
|
const hasLink = link && link.length > 0;
|
||||||
|
const rawExcerpt =
|
||||||
|
(hasTitle ? title : null) ||
|
||||||
|
(hasContent ? content : null) ||
|
||||||
|
(hasLink ? link : null) ||
|
||||||
|
(getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : null) ||
|
||||||
|
t('no_content');
|
||||||
|
const excerpt = rawExcerpt.length > 140 ? rawExcerpt.slice(0, 137) + '...' : rawExcerpt;
|
||||||
|
const threadTargetCid = threadCid || cid;
|
||||||
|
const postUrl = boardPath && threadTargetCid ? `/${boardPath}/thread/${threadTargetCid}` : undefined;
|
||||||
|
|
||||||
|
const renderActions = () => {
|
||||||
|
if (status === 'approved') {
|
||||||
|
return (
|
||||||
|
<div className={styles.cardActions}>
|
||||||
|
<span className={`${styles.button} ${styles.approve}`}>{t('approved')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'rejected') {
|
||||||
|
return (
|
||||||
|
<div className={styles.cardActions}>
|
||||||
|
<span className={`${styles.button} ${styles.reject}`}>{t('rejected')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return (
|
||||||
|
<div className={styles.cardActions}>
|
||||||
|
<span className={`${styles.button} ${styles.reject}`}>
|
||||||
|
{t('failed')}
|
||||||
|
{errorMessage ? `: ${errorMessage}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isPublishing) {
|
||||||
|
return (
|
||||||
|
<div className={styles.cardActions}>
|
||||||
|
<LoadingEllipsis string={t('publishing')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.cardActions}>
|
||||||
|
<button className={styles.button} onClick={handleApprove} disabled={isPublishing}>
|
||||||
|
{t('approve')}
|
||||||
|
</button>
|
||||||
|
<button className={styles.button} onClick={handleReject} disabled={isPublishing}>
|
||||||
|
{t('reject')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.mobileCard}>
|
||||||
|
<div className={styles.cardHeader}>
|
||||||
|
<span className={styles.cardNumber}>No. {number ?? 'N/A'}</span>
|
||||||
|
<span className={styles.cardTime}>
|
||||||
|
{isAwaitingApproval && isOverThreshold ? (
|
||||||
|
<>
|
||||||
|
{getFormattedDate(timestamp)} (<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
getFormattedDate(timestamp)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.cardContent}>
|
||||||
|
{t('excerpt')}:{' '}
|
||||||
|
{postUrl ? (
|
||||||
|
<Link to={postUrl} title={excerpt}>
|
||||||
|
{excerpt}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span title={excerpt}>{excerpt}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{renderActions()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
|
||||||
|
const { editedComment } = useEditedComment({ comment });
|
||||||
|
const displayComment = editedComment || comment;
|
||||||
|
const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Post
|
||||||
|
post={displayComment}
|
||||||
|
showAllReplies={false}
|
||||||
|
showReplies={false}
|
||||||
|
isModQueue={true}
|
||||||
|
modQueueStatus={status}
|
||||||
|
modQueueError={errorMessage}
|
||||||
|
isPublishing={isPublishing}
|
||||||
|
onApprove={handleApprove}
|
||||||
|
onReject={handleReject}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ModQueueBoardFilterProps {
|
interface ModQueueBoardFilterProps {
|
||||||
subplebbits: MultisubSubplebbit[];
|
subplebbits: MultisubSubplebbit[];
|
||||||
}
|
}
|
||||||
@@ -474,7 +620,8 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
|
|||||||
export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
|
export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { selectedBoardFilter, alertThresholdValue, alertThresholdUnit, setAlertThreshold } = useModQueueStore();
|
const { selectedBoardFilter, viewMode } = useModQueueStore();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const accountSubplebbitAddresses = useAccountsStore(
|
const accountSubplebbitAddresses = useAccountsStore(
|
||||||
(state) => {
|
(state) => {
|
||||||
@@ -563,97 +710,108 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
|
|||||||
[hasMore, subplebbitAddresses, subplebbitError, feed.length],
|
[hasMore, subplebbitAddresses, subplebbitError, feed.length],
|
||||||
);
|
);
|
||||||
|
|
||||||
const alertThresholdControl = (
|
|
||||||
<div className={styles.alertThresholdSetting}>
|
|
||||||
<label>
|
|
||||||
{t('alert_threshold')}:
|
|
||||||
<input
|
|
||||||
type='number'
|
|
||||||
min='1'
|
|
||||||
step={alertThresholdUnit === 'minutes' ? '1' : '1'}
|
|
||||||
value={alertThresholdValue}
|
|
||||||
onChange={(e) => setAlertThreshold(Number(e.target.value), alertThresholdUnit)}
|
|
||||||
className={styles.alertThresholdInput}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={alertThresholdUnit}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newUnit = e.target.value as 'hours' | 'minutes';
|
|
||||||
const newValue =
|
|
||||||
alertThresholdUnit === 'hours' && newUnit === 'minutes'
|
|
||||||
? alertThresholdValue * 60
|
|
||||||
: alertThresholdUnit === 'minutes' && newUnit === 'hours'
|
|
||||||
? Math.round(alertThresholdValue / 60)
|
|
||||||
: alertThresholdValue;
|
|
||||||
setAlertThreshold(Math.max(1, newValue), newUnit);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value='minutes'>{t('minutes')}</option>
|
|
||||||
<option value='hours'>{t('hours')}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{!resolvedAddress && (
|
{!resolvedAddress && (
|
||||||
<div className={styles.header}>
|
<div className={styles.controls}>
|
||||||
<div className={styles.title}>{t('moderation_queue')}</div>
|
<div className={styles.controlsLeft}>
|
||||||
|
<ModQueueBoardFilter subplebbits={subplebbitsWithMetadata} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.controls}>
|
|
||||||
{!resolvedAddress ? (
|
|
||||||
<>
|
|
||||||
<div className={styles.controlsLeft}>
|
|
||||||
<ModQueueBoardFilter subplebbits={subplebbitsWithMetadata} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.controlsRight}>{alertThresholdControl}</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className={styles.controlsLeft}>
|
|
||||||
<div className={styles.title}>{t('moderation_queue')}</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.controlsRight}>{alertThresholdControl}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{feed.length === 0 && !hasMore ? (
|
{feed.length === 0 && !hasMore ? (
|
||||||
<div className={styles.empty}>{t('queue_is_empty')}</div>
|
<div className={styles.empty}>{t('queue_is_empty')}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={styles.tableHeader}>
|
{viewMode === 'compact' && !isMobile && (
|
||||||
<div className={styles.numberHeader}>No.</div>
|
|
||||||
<div className={styles.excerptHeader}>{t('excerpt')}</div>
|
|
||||||
<div className={styles.timeHeader}>{t('submitted')}</div>
|
|
||||||
<div className={styles.actionsHeader}>{t('actions')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Use Virtuoso for infinite scroll only when there's more content to paginate */}
|
|
||||||
{hasMore ? (
|
|
||||||
<Virtuoso
|
|
||||||
useWindowScroll
|
|
||||||
data={feed}
|
|
||||||
totalCount={feed.length}
|
|
||||||
endReached={loadMore}
|
|
||||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
|
||||||
itemContent={(index, comment) => <ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />}
|
|
||||||
components={footerComponents}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
{feed.map((comment, index) => (
|
<div className={styles.tableHeader}>
|
||||||
<ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />
|
<div className={styles.numberHeader}>No.</div>
|
||||||
))}
|
<div className={styles.excerptHeader}>{t('excerpt')}</div>
|
||||||
{subplebbitError?.message && feed.length === 0 && (
|
<div className={styles.timeHeader}>{t('submitted')}</div>
|
||||||
<div className={styles.error}>
|
<div className={styles.actionsHeader}>{t('actions')}</div>
|
||||||
<ErrorDisplay error={subplebbitError} />
|
</div>
|
||||||
</div>
|
|
||||||
|
{hasMore ? (
|
||||||
|
<Virtuoso
|
||||||
|
useWindowScroll
|
||||||
|
data={feed}
|
||||||
|
totalCount={feed.length}
|
||||||
|
endReached={loadMore}
|
||||||
|
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||||
|
itemContent={(index, comment) => <ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />}
|
||||||
|
components={footerComponents}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{feed.map((comment, index) => (
|
||||||
|
<ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />
|
||||||
|
))}
|
||||||
|
{subplebbitError?.message && feed.length === 0 && (
|
||||||
|
<div className={styles.error}>
|
||||||
|
<ErrorDisplay error={subplebbitError} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewMode === 'compact' && isMobile && (
|
||||||
|
<>
|
||||||
|
{hasMore ? (
|
||||||
|
<Virtuoso
|
||||||
|
useWindowScroll
|
||||||
|
data={feed}
|
||||||
|
totalCount={feed.length}
|
||||||
|
endReached={loadMore}
|
||||||
|
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||||
|
itemContent={(_index, comment) => <ModQueueCard key={comment.cid} comment={comment} />}
|
||||||
|
components={footerComponents}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{feed.map((comment) => (
|
||||||
|
<ModQueueCard key={comment.cid} comment={comment} />
|
||||||
|
))}
|
||||||
|
{subplebbitError?.message && feed.length === 0 && (
|
||||||
|
<div className={styles.error}>
|
||||||
|
<ErrorDisplay error={subplebbitError} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewMode === 'feed' && (
|
||||||
|
<>
|
||||||
|
{hasMore ? (
|
||||||
|
<Virtuoso
|
||||||
|
useWindowScroll
|
||||||
|
data={feed}
|
||||||
|
totalCount={feed.length}
|
||||||
|
endReached={loadMore}
|
||||||
|
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||||
|
itemContent={(_index, comment) => <ModQueueFeedPost key={comment.cid} comment={comment} />}
|
||||||
|
components={footerComponents}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{feed.map((comment) => (
|
||||||
|
<ModQueueFeedPost key={comment.cid} comment={comment} />
|
||||||
|
))}
|
||||||
|
{subplebbitError?.message && feed.length === 0 && (
|
||||||
|
<div className={styles.error}>
|
||||||
|
<ErrorDisplay error={subplebbitError} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -189,6 +189,55 @@
|
|||||||
color: var(--button-desktop-text-color-hover);
|
color: var(--button-desktop-text-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modQueueActions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modQueueButtonWrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modQueueActionButton {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: var(--button-desktop-text-color);
|
||||||
|
text-decoration: var(--button-text-decoration);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modQueueActionButton:hover {
|
||||||
|
color: var(--button-desktop-text-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modQueueActionButton:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modQueueStatusApproved {
|
||||||
|
color: green;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modQueueStatusRejected {
|
||||||
|
color: red;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approveButton {
|
||||||
|
background-color: #3fa96b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rejectButton {
|
||||||
|
background-color: #d9534f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.postDesktop .spacer {
|
.postDesktop .spacer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@@ -668,4 +717,16 @@
|
|||||||
color: red;
|
color: red;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
color: var(--mod-queue-alert-color, red);
|
||||||
|
font-weight: bold;
|
||||||
|
animation: blink 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
}
|
}
|
||||||
+44
-3
@@ -25,9 +25,26 @@ export interface PostProps {
|
|||||||
showReplies?: boolean;
|
showReplies?: boolean;
|
||||||
targetReplyCid?: string;
|
targetReplyCid?: string;
|
||||||
threadNumber?: number;
|
threadNumber?: number;
|
||||||
|
isModQueue?: boolean;
|
||||||
|
modQueueStatus?: 'approved' | 'rejected' | 'failed' | null;
|
||||||
|
modQueueError?: string;
|
||||||
|
isPublishing?: boolean;
|
||||||
|
onApprove?: () => void;
|
||||||
|
onReject?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Post = ({ post, showAllReplies = false, showReplies = true, targetReplyCid }: PostProps) => {
|
export const Post = ({
|
||||||
|
post,
|
||||||
|
showAllReplies = false,
|
||||||
|
showReplies = true,
|
||||||
|
targetReplyCid,
|
||||||
|
isModQueue,
|
||||||
|
modQueueStatus,
|
||||||
|
modQueueError,
|
||||||
|
isPublishing,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
}: PostProps) => {
|
||||||
// Only subscribe to roles field to avoid rerenders from updatingState changes
|
// Only subscribe to roles field to avoid rerenders from updatingState changes
|
||||||
const roles = useSubplebbitField(post?.subplebbitAddress, (subplebbit) => subplebbit?.roles);
|
const roles = useSubplebbitField(post?.subplebbitAddress, (subplebbit) => subplebbit?.roles);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@@ -44,9 +61,33 @@ export const Post = ({ post, showAllReplies = false, showReplies = true, targetR
|
|||||||
<div className={styles.thread}>
|
<div className={styles.thread}>
|
||||||
<div className={styles.postContainer}>
|
<div className={styles.postContainer}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<PostMobile post={comment} roles={roles} showAllReplies={showAllReplies} showReplies={showReplies} targetReplyCid={targetReplyCid} />
|
<PostMobile
|
||||||
|
post={comment}
|
||||||
|
roles={roles}
|
||||||
|
showAllReplies={showAllReplies}
|
||||||
|
showReplies={showReplies}
|
||||||
|
targetReplyCid={targetReplyCid}
|
||||||
|
isModQueue={isModQueue}
|
||||||
|
modQueueStatus={modQueueStatus}
|
||||||
|
modQueueError={modQueueError}
|
||||||
|
isPublishing={isPublishing}
|
||||||
|
onApprove={onApprove}
|
||||||
|
onReject={onReject}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PostDesktop post={comment} roles={roles} showAllReplies={showAllReplies} showReplies={showReplies} targetReplyCid={targetReplyCid} />
|
<PostDesktop
|
||||||
|
post={comment}
|
||||||
|
roles={roles}
|
||||||
|
showAllReplies={showAllReplies}
|
||||||
|
showReplies={showReplies}
|
||||||
|
targetReplyCid={targetReplyCid}
|
||||||
|
isModQueue={isModQueue}
|
||||||
|
modQueueStatus={modQueueStatus}
|
||||||
|
modQueueError={modQueueError}
|
||||||
|
isPublishing={isPublishing}
|
||||||
|
onApprove={onApprove}
|
||||||
|
onReject={onReject}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user