{!(isInAllView || isInSubscriptionsView || isInModView) && showForm &&
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm &&
}
- {isThreadClosed ? (
+ {isInModQueueView ? (
+
{t('moderation_queue')}
+ ) : isThreadClosed ? (
{t('thread_closed')}
diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx
index c1664f1a..c67741a5 100644
--- a/src/components/post-mobile/post-mobile.tsx
+++ b/src/components/post-mobile/post-mobile.tsx
@@ -9,7 +9,8 @@ import { shouldShowSnow } from '../../lib/snow';
import { getHasThumbnail } from '../../lib/utils/media-utils';
import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-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 { getBoardPath } from '../../lib/utils/route-utils';
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 isInPostPageView = isPostPageView(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 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 postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
@@ -176,7 +188,14 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
Board: {boardPath}
)}
-
{getFormattedDate(timestamp)}} content={getFormattedTimeAgo(timestamp)} />{' '}
+ {isInModQueueView && isOverThreshold ? (
+ <>
+ {getFormattedDate(timestamp)}} content={getFormattedTimeAgo(timestamp)} /> (
+ {getFormattedTimeAgo(timestamp)})
+ >
+ ) : (
+ {getFormattedDate(timestamp)}} content={getFormattedTimeAgo(timestamp)} />
+ )}{' '}
{cid ? (
{
);
};
-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 { author, cid, pinned, postCid, replyCount, state, subplebbitAddress } = post || {};
const params = useParams();
@@ -356,12 +387,12 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, targetRep
>
) : (
- {showReplies && (
+ {(showReplies || isModQueue) && (
)}
-
+
- {!isInPostView && !isInPendingPostView && showReplies && (
+ {!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
{replyCount > 0 && `${replyCount} Replies`}
{linksCount > 0 && ` / ${linksCount} Links`}
-
- {t('view_thread')}
-
+ {isModQueue ? (
+
+ {modQueueStatus === 'approved' ? (
+ {t('approved')}
+ ) : modQueueStatus === 'rejected' ? (
+ {t('rejected')}
+ ) : modQueueStatus === 'failed' ? (
+
+ {t('failed')}
+ {modQueueError ? `: ${modQueueError}` : ''}
+
+ ) : isPublishing ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ ) : (
+
+ {t('view_thread')}
+
+ )}
)}
diff --git a/src/stores/use-mod-queue-store.ts b/src/stores/use-mod-queue-store.ts
index 25f2d7e1..5c588646 100644
--- a/src/stores/use-mod-queue-store.ts
+++ b/src/stores/use-mod-queue-store.ts
@@ -2,13 +2,16 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type AlertThresholdUnit = 'hours' | 'minutes';
+export type ModQueueViewMode = 'compact' | 'feed';
interface ModQueueState {
alertThresholdValue: number;
alertThresholdUnit: AlertThresholdUnit;
selectedBoardFilter: string | null;
+ viewMode: ModQueueViewMode;
setAlertThreshold: (value: number, unit: AlertThresholdUnit) => void;
setSelectedBoardFilter: (boardAddress: string | null) => void;
+ setViewMode: (viewMode: ModQueueViewMode) => void;
// Helper to get threshold in seconds for calculations
getAlertThresholdSeconds: () => number;
}
@@ -19,10 +22,11 @@ interface OldPersistedState {
alertThresholdValue?: number;
alertThresholdUnit?: AlertThresholdUnit;
selectedBoardFilter?: string | null;
+ viewMode?: ModQueueViewMode;
}
// Type for persisted data (without methods)
-type PersistedModQueueData = Pick
;
+type PersistedModQueueData = Pick;
const useModQueueStore = create()(
persist(
@@ -30,8 +34,10 @@ const useModQueueStore = create()(
alertThresholdValue: 6,
alertThresholdUnit: 'hours' as AlertThresholdUnit,
selectedBoardFilter: null,
+ viewMode: 'compact',
setAlertThreshold: (value, unit) => set({ alertThresholdValue: value, alertThresholdUnit: unit }),
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }),
+ setViewMode: (viewMode) => set({ viewMode }),
getAlertThresholdSeconds: () => {
const { alertThresholdValue, alertThresholdUnit } = get();
return alertThresholdUnit === 'hours' ? alertThresholdValue * 3600 : alertThresholdValue * 60;
@@ -39,7 +45,7 @@ const useModQueueStore = create()(
}),
{
name: 'mod-queue-storage',
- version: 1,
+ version: 2,
// Migrate old alertThresholdHours format to new alertThresholdValue/alertThresholdUnit format
migrate: (persistedState, version): ModQueueState => {
const state = persistedState as OldPersistedState;
@@ -48,6 +54,7 @@ const useModQueueStore = create()(
alertThresholdValue: state.alertThresholdHours,
alertThresholdUnit: 'hours' as AlertThresholdUnit,
selectedBoardFilter: state.selectedBoardFilter ?? null,
+ viewMode: state.viewMode ?? 'compact',
};
// Zustand will merge this with the store definition (which includes methods)
return migrated as ModQueueState;
@@ -57,6 +64,7 @@ const useModQueueStore = create()(
alertThresholdValue: state.alertThresholdValue ?? 6,
alertThresholdUnit: state.alertThresholdUnit ?? 'hours',
selectedBoardFilter: state.selectedBoardFilter ?? null,
+ viewMode: state.viewMode ?? 'compact',
};
return current as ModQueueState;
},
diff --git a/src/views/mod-queue/mod-queue.module.css b/src/views/mod-queue/mod-queue.module.css
index fef3d539..2fa9bb2c 100644
--- a/src/views/mod-queue/mod-queue.module.css
+++ b/src/views/mod-queue/mod-queue.module.css
@@ -233,6 +233,52 @@
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) */
@media (min-width: 640px) {
.actions {
diff --git a/src/views/mod-queue/mod-queue.tsx b/src/views/mod-queue/mod-queue.tsx
index bdee9f98..bc520b1d 100644
--- a/src/views/mod-queue/mod-queue.tsx
+++ b/src/views/mod-queue/mod-queue.tsx
@@ -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 { useParams, Link } from 'react-router-dom';
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 Tooltip from '../../components/tooltip';
import useIsMobile from '../../hooks/use-is-mobile';
+import { Post } from '../post/post';
const { addChallenge } = useChallengesStore.getState();
@@ -55,30 +56,22 @@ interface ModQueueRowProps {
// Track which action was initiated to show appropriate completion message
type ModerationAction = 'approve' | 'reject' | null;
-const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
+interface ModQueueActionState {
+ status: 'approved' | 'rejected' | 'failed' | null;
+ errorMessage?: string;
+ isPublishing: boolean;
+ handleApprove: () => Promise;
+ handleReject: () => Promise;
+}
+
+const useModQueueActions = (comment: Comment): ModQueueActionState => {
const { t } = useTranslation();
- const { getAlertThresholdSeconds } = useModQueueStore();
+ const { cid, subplebbitAddress, approved, removed } = comment || {};
const [initiatedAction, setInitiatedAction] = useState(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 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 {
publishCommentModeration: approve,
state: approveState,
@@ -117,8 +110,7 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
},
});
- const handleApprove = async () => {
- // Double confirmation for approve action
+ const handleApprove = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
@@ -130,10 +122,9 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
} catch (e) {
console.error(e);
}
- };
+ }, [approve, t]);
- const handleReject = async () => {
- // Double confirmation for reject action
+ const handleReject = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
@@ -145,9 +136,8 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
} catch (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 isRejecting = initiatedAction === 'reject' && rejectState !== 'initializing' && rejectState !== 'succeeded' && rejectState !== 'failed';
const isPublishing = isApproving || isRejecting;
@@ -158,6 +148,37 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
const approveFailed = initiatedAction === 'approve' && approveState === '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 hasTitle = title && title.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
const renderActions = () => {
// Check existing moderation state first (from API/previous sessions)
- if (alreadyApproved || approveSucceeded) {
+ if (status === 'approved') {
return {t('approved')};
}
- if (alreadyRejected || rejectSucceeded) {
+ if (status === 'rejected') {
return {t('rejected')};
}
- if (approveFailed) {
+ if (status === 'failed') {
return (
- {t('failed')}: {approveError?.message}
-
- );
- }
- if (rejectFailed) {
- return (
-
- {t('failed')}: {rejectError?.message}
+ {t('failed')}
+ {errorMessage ? `: ${errorMessage}` : ''}
);
}
@@ -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 (
+
+ {t('approved')}
+
+ );
+ }
+ if (status === 'rejected') {
+ return (
+
+ {t('rejected')}
+
+ );
+ }
+ if (status === 'failed') {
+ return (
+
+
+ {t('failed')}
+ {errorMessage ? `: ${errorMessage}` : ''}
+
+
+ );
+ }
+ if (isPublishing) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+ };
+
+ return (
+
+
+ No. {number ?? 'N/A'}
+
+ {isAwaitingApproval && isOverThreshold ? (
+ <>
+ {getFormattedDate(timestamp)} ({getFormattedTimeAgo(timestamp)})
+ >
+ ) : (
+ getFormattedDate(timestamp)
+ )}
+
+
+
+ {t('excerpt')}:{' '}
+ {postUrl ? (
+
+ {excerpt}
+
+ ) : (
+ {excerpt}
+ )}
+
+ {renderActions()}
+
+ );
+};
+
+const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
+ const { editedComment } = useEditedComment({ comment });
+ const displayComment = editedComment || comment;
+ const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
+
+ return (
+
+ );
+};
+
interface ModQueueBoardFilterProps {
subplebbits: MultisubSubplebbit[];
}
@@ -474,7 +620,8 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
const { t } = useTranslation();
const params = useParams();
- const { selectedBoardFilter, alertThresholdValue, alertThresholdUnit, setAlertThreshold } = useModQueueStore();
+ const { selectedBoardFilter, viewMode } = useModQueueStore();
+ const isMobile = useIsMobile();
const accountSubplebbitAddresses = useAccountsStore(
(state) => {
@@ -563,97 +710,108 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
[hasMore, subplebbitAddresses, subplebbitError, feed.length],
);
- const alertThresholdControl = (
-
-
-
- );
-
return (
{!resolvedAddress && (
-
-
{t('moderation_queue')}
+
)}
-
- {!resolvedAddress ? (
- <>
-
-
-
-
{alertThresholdControl}
- >
- ) : (
- <>
-
-
{t('moderation_queue')}
-
-
{alertThresholdControl}
- >
- )}
-
-
{feed.length === 0 && !hasMore ? (
{t('queue_is_empty')}
) : (
<>
-
-
No.
-
{t('excerpt')}
-
{t('submitted')}
-
{t('actions')}
-
-
- {/* Use Virtuoso for infinite scroll only when there's more content to paginate */}
- {hasMore ? (
-
}
- components={footerComponents}
- />
- ) : (
+ {viewMode === 'compact' && !isMobile && (
<>
- {feed.map((comment, index) => (
-
- ))}
- {subplebbitError?.message && feed.length === 0 && (
-
-
-
+
+
No.
+
{t('excerpt')}
+
{t('submitted')}
+
{t('actions')}
+
+
+ {hasMore ? (
+ }
+ components={footerComponents}
+ />
+ ) : (
+ <>
+ {feed.map((comment, index) => (
+
+ ))}
+ {subplebbitError?.message && feed.length === 0 && (
+
+
+
+ )}
+
+ >
+ )}
+ >
+ )}
+
+ {viewMode === 'compact' && isMobile && (
+ <>
+ {hasMore ? (
+ }
+ components={footerComponents}
+ />
+ ) : (
+ <>
+ {feed.map((comment) => (
+
+ ))}
+ {subplebbitError?.message && feed.length === 0 && (
+
+
+
+ )}
+
+ >
+ )}
+ >
+ )}
+
+ {viewMode === 'feed' && (
+ <>
+ {hasMore ? (
+ }
+ components={footerComponents}
+ />
+ ) : (
+ <>
+ {feed.map((comment) => (
+
+ ))}
+ {subplebbitError?.message && feed.length === 0 && (
+
+
+
+ )}
+
+ >
)}
-
>
)}
>
diff --git a/src/views/post/post.module.css b/src/views/post/post.module.css
index 10482541..fdd0d19d 100644
--- a/src/views/post/post.module.css
+++ b/src/views/post/post.module.css
@@ -189,6 +189,55 @@
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 {
width: 100%;
padding: 5px;
@@ -668,4 +717,16 @@
color: red;
font-weight: 700;
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; }
}
\ No newline at end of file
diff --git a/src/views/post/post.tsx b/src/views/post/post.tsx
index 71333b19..8f58a58a 100644
--- a/src/views/post/post.tsx
+++ b/src/views/post/post.tsx
@@ -25,9 +25,26 @@ export interface PostProps {
showReplies?: boolean;
targetReplyCid?: string;
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
const roles = useSubplebbitField(post?.subplebbitAddress, (subplebbit) => subplebbit?.roles);
const isMobile = useIsMobile();
@@ -44,9 +61,33 @@ export const Post = ({ post, showAllReplies = false, showReplies = true, targetR
{isMobile ? (
-
+
) : (
-
+
)}
diff --git a/yarn.lock b/yarn.lock
index 54a37f42..dd342a58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1037,20 +1037,20 @@
ipaddr.js "^2.2.0"
punycode "^2.3.1"
-"@capacitor/android@7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-7.4.4.tgz#a14a1e844bd5079982427e247fdd17555b5fbedd"
- integrity sha512-y8knfV1JXNrd6XZZLZireGT+EBCN0lvOo+HZ/s7L8LkrPBu4nY5UZn0Wxz4yOezItEII9rqYJSHsS5fMJG9gdw==
+"@capacitor/android@7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-7.4.5.tgz#3832b457e0a3a7079a645bf5be3cc23d406c2971"
+ integrity sha512-G76C+PGIFkG+hxj6JLlY003txRaGZwsXBPo4b4xiZMZaD6DG++sDCoumYl/5xL40dL2b8P5tb3CKf/khWyL2Gw==
"@capacitor/app@7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-7.0.1.tgz#0d0709fb4dde5046c24853f2d6b77a7ea411f748"
integrity sha512-ArlVZAAla4MwQoKh26x2AaTDOBh5Vhp1VhMKR3RwqZSsZnazKTFGNrPbr9Ez5r1knnEDfApyjwp1uZnXK1WTYQ==
-"@capacitor/cli@7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-7.4.4.tgz#bfce7db8b15d6f44c9d58dc338a5e3d662fecfbb"
- integrity sha512-J7ciBE7GlJ70sr2s8oz1+H4ZdNk4MGG41fsakUlDHWva5UWgFIZYMiEdDvGbYazAYTaxN3lVZpH9zil9FfZj+Q==
+"@capacitor/cli@7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-7.4.5.tgz#056d8acc0d27ec3f8f79a5588e6cd6539db2895d"
+ integrity sha512-ApKxpzcWTqmyTD+WxBvL7UJt+x0xr9Vqa+S4Iqbl/R2kC0tf1ay22wE9pOrdWXbTqFbNhBuUuRvTvZ6Edp7QzQ==
dependencies:
"@ionic/cli-framework-output" "^2.2.8"
"@ionic/utils-subprocess" "^3.0.1"
@@ -1060,7 +1060,7 @@
env-paths "^2.2.0"
fs-extra "^11.2.0"
kleur "^4.1.5"
- native-run "^2.0.1"
+ native-run "^2.0.3"
open "^8.4.0"
plist "^3.1.0"
prompts "^2.4.2"
@@ -1070,10 +1070,10 @@
tslib "^2.8.1"
xml2js "^0.6.2"
-"@capacitor/core@7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-7.4.4.tgz#b752dd7a12141e40d1c8ce2e37a79571d9f83518"
- integrity sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==
+"@capacitor/core@7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-7.4.5.tgz#334d77356fa473e95d51cf6375e9b36471faf2d6"
+ integrity sha512-JnUywi0WRbqm4QYoplNsMQ7KAVuyrQFha0dOH9YDVCyrVHUvPcvrnK+9j5elUMgy+pOh3ve+pk6BVoySr91c+A==
dependencies:
tslib "^2.1.0"
@@ -6263,6 +6263,11 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+chownr@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
+ integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+
chownr@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4"
@@ -8179,6 +8184,13 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
+fs-minipass@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
+ integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+ dependencies:
+ minipass "^3.0.0"
+
fs-minipass@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54"
@@ -11271,12 +11283,17 @@ minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3.
dependencies:
yallist "^4.0.0"
+minipass@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
+ integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
+
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.0.4, minipass@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
-minizlib@^2.0.0, minizlib@^2.1.2:
+minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
@@ -11296,6 +11313,11 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+mkdirp@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+ integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
modify-filename@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1"
@@ -11424,10 +11446,10 @@ napi-build-utils@^2.0.0:
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e"
integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==
-native-run@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/native-run/-/native-run-2.0.1.tgz#a9b213c32824b007cbdd0279e0edd3c24bcc2f7a"
- integrity sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==
+native-run@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/native-run/-/native-run-2.0.3.tgz#b1ceb39c8d24abf4916c07ae9aad5a9fbcd31a02"
+ integrity sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==
dependencies:
"@ionic/utils-fs" "^3.1.7"
"@ionic/utils-terminal" "^2.3.4"
@@ -13958,7 +13980,19 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
-tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2, tar@^6.2.1, tar@^7.4.3, tar@^7.5.3:
+tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2, tar@^6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
+ integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
+ dependencies:
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ minipass "^5.0.0"
+ minizlib "^2.1.1"
+ mkdirp "^1.0.3"
+ yallist "^4.0.0"
+
+tar@^7.4.3:
version "7.5.3"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.3.tgz#e1a41236e32446f75e63b720222112c4ffe5b3a1"
integrity sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==