mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(mod-queue): rework excerpt hover preview, scope pending-age alerts (#1152)
Render the compact mod queue excerpt hover preview like the quote-link previews (clean read-only Post via useFloating, anchored to the right of the excerpt on desktop / below on mobile), cap its content at 350 chars, and scope the red pending-age timestamp to posts actually awaiting approval. Adds keyboard handlers to the span fallback.
This commit is contained in:
@@ -25,8 +25,10 @@ type TestComment = {
|
|||||||
linkWidth?: number;
|
linkWidth?: number;
|
||||||
number?: number;
|
number?: number;
|
||||||
parentCid?: string;
|
parentCid?: string;
|
||||||
|
pendingApproval?: boolean;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
postCid?: string;
|
postCid?: string;
|
||||||
|
approved?: boolean;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
replyCount?: number;
|
replyCount?: number;
|
||||||
replies?: {
|
replies?: {
|
||||||
@@ -167,6 +169,8 @@ vi.mock('../../lib/utils/time-utils', () => ({
|
|||||||
|
|
||||||
vi.mock('../../lib/utils/pending-approval-moderation', () => ({
|
vi.mock('../../lib/utils/pending-approval-moderation', () => ({
|
||||||
approvePendingCommentModeration: {},
|
approvePendingCommentModeration: {},
|
||||||
|
isPendingApprovalAwaiting: (comment?: TestComment) =>
|
||||||
|
comment?.pendingApproval === true && comment?.approved !== true && comment?.approved !== false && !comment?.removed,
|
||||||
isPendingApprovalRejected: () => false,
|
isPendingApprovalRejected: () => false,
|
||||||
rejectPendingCommentModeration: {},
|
rejectPendingCommentModeration: {},
|
||||||
}));
|
}));
|
||||||
@@ -178,7 +182,7 @@ vi.mock('../../lib/utils/url-utils', () => ({
|
|||||||
|
|
||||||
vi.mock('../../lib/utils/view-utils', () => ({
|
vi.mock('../../lib/utils/view-utils', () => ({
|
||||||
isAllView: (pathname: string) => pathname === '/all',
|
isAllView: (pathname: string) => pathname === '/all',
|
||||||
isModQueueView: () => false,
|
isModQueueView: (pathname: string) => pathname === '/mod/queue' || pathname.endsWith('/mod/queue'),
|
||||||
isModView: () => false,
|
isModView: () => false,
|
||||||
isPendingPostView: () => false,
|
isPendingPostView: () => false,
|
||||||
isPostPageView: (pathname: string) => pathname.includes('/thread/'),
|
isPostPageView: (pathname: string) => pathname.includes('/thread/'),
|
||||||
@@ -535,4 +539,34 @@ describe('post community address compatibility', () => {
|
|||||||
await renderWithRoute(createElement(PostMobile, { post: makeLegacyThreadWithoutReplies() }));
|
await renderWithRoute(createElement(PostMobile, { post: makeLegacyThreadWithoutReplies() }));
|
||||||
expect(container.querySelector('.postMobile')?.getAttribute('data-pretext-height')).toBeTruthy();
|
expect(container.querySelector('.postMobile')?.getAttribute('data-pretext-height')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show a mod queue age alert for published preview posts on the mod queue route', async () => {
|
||||||
|
const publishedPost = {
|
||||||
|
...makeLegacyThread(),
|
||||||
|
pendingApproval: false,
|
||||||
|
timestamp: 1_700_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderWithRoute(createElement(PostDesktop, { post: publishedPost, showReplies: false }), '/mod/queue');
|
||||||
|
expect(container.textContent).toContain('2026-03-13');
|
||||||
|
expect(container.textContent).not.toContain('moments ago');
|
||||||
|
|
||||||
|
await renderWithRoute(createElement(PostMobile, { post: publishedPost, showReplies: false }), '/mod/queue');
|
||||||
|
expect(container.textContent).toContain('2026-03-13');
|
||||||
|
expect(container.textContent).not.toContain('moments ago');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the mod queue age alert for pending preview posts on the mod queue route', async () => {
|
||||||
|
const pendingPost = {
|
||||||
|
...makeLegacyThread(),
|
||||||
|
pendingApproval: true,
|
||||||
|
timestamp: 1_700_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderWithRoute(createElement(PostDesktop, { post: pendingPost, showReplies: false }), '/mod/queue');
|
||||||
|
expect(container.textContent).toContain('moments ago');
|
||||||
|
|
||||||
|
await renderWithRoute(createElement(PostMobile, { post: pendingPost, showReplies: false }), '/mod/queue');
|
||||||
|
expect(container.textContent).toContain('moments ago');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import styles from '../../views/post/post.module.css';
|
|||||||
import { CommentMediaInfo, getHasThumbnail, getMediaDimensions, getPostMediaTypeLabel, getYouTubeEmbedPostMediaFileLink } from '../../lib/utils/media-utils';
|
import { CommentMediaInfo, getHasThumbnail, getMediaDimensions, getPostMediaTypeLabel, getYouTubeEmbedPostMediaFileLink } from '../../lib/utils/media-utils';
|
||||||
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 { approvePendingCommentModeration, isPendingApprovalRejected, rejectPendingCommentModeration } from '../../lib/utils/pending-approval-moderation';
|
import { approvePendingCommentModeration, isPendingApprovalAwaiting, rejectPendingCommentModeration } from '../../lib/utils/pending-approval-moderation';
|
||||||
import { isValidURL, parseHttpUrl } from '../../lib/utils/url-utils';
|
import { isValidURL, parseHttpUrl } from '../../lib/utils/url-utils';
|
||||||
import { isAllView, isModQueueView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isModQueueView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
||||||
@@ -263,10 +263,7 @@ const PostInfo = ({
|
|||||||
const shouldShowPendingApprovalButtons = isInPostPageView && !isInModQueueView && pendingApproval && isAccountMod && communityAddress;
|
const shouldShowPendingApprovalButtons = isInPostPageView && !isInModQueueView && pendingApproval && isAccountMod && communityAddress;
|
||||||
|
|
||||||
// Check if post is awaiting approval and over threshold (for mod queue view)
|
// Check if post is awaiting approval and over threshold (for mod queue view)
|
||||||
const approved = post?.approved;
|
const isAwaitingApproval = isInModQueueView && isPendingApprovalAwaiting(post);
|
||||||
const alreadyApproved = approved === true;
|
|
||||||
const alreadyRejected = isPendingApprovalRejected(post);
|
|
||||||
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
|
||||||
const timeWaiting = timestamp ? currentTime - timestamp : 0;
|
const timeWaiting = timestamp ? currentTime - timestamp : 0;
|
||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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 { approvePendingCommentModeration, isPendingApprovalRejected, rejectPendingCommentModeration } from '../../lib/utils/pending-approval-moderation';
|
import { approvePendingCommentModeration, isPendingApprovalAwaiting, rejectPendingCommentModeration } from '../../lib/utils/pending-approval-moderation';
|
||||||
import { isAllView, isModQueueView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isModQueueView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
|
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
|
||||||
import useModQueueStore from '../../stores/use-mod-queue-store';
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
@@ -210,10 +210,7 @@ const PostInfoAndMedia = ({
|
|||||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||||
|
|
||||||
// Check if post is awaiting approval and over threshold (for mod queue view)
|
// Check if post is awaiting approval and over threshold (for mod queue view)
|
||||||
const approved = resolvedPost?.approved;
|
const isAwaitingApproval = isInModQueueView && isPendingApprovalAwaiting(resolvedPost);
|
||||||
const alreadyApproved = approved === true;
|
|
||||||
const alreadyRejected = isPendingApprovalRejected(post);
|
|
||||||
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
|
|
||||||
const timeWaiting = timestamp ? currentTime - timestamp : 0;
|
const timeWaiting = timestamp ? currentTime - timestamp : 0;
|
||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ const act = (React as { act?: (callback: () => void | Promise<void>) => void | P
|
|||||||
|
|
||||||
type TestComment = {
|
type TestComment = {
|
||||||
cid: string;
|
cid: string;
|
||||||
|
content?: string;
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
|
number?: number;
|
||||||
pendingApproval?: boolean;
|
pendingApproval?: boolean;
|
||||||
|
timestamp?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const testState = vi.hoisted(() => ({
|
const testState = vi.hoisted(() => ({
|
||||||
@@ -96,6 +99,22 @@ vi.mock('@bitsocial/bitsocial-react-hooks/dist/stores/accounts/index.js', () =>
|
|||||||
selector({ accountsEditsSummaries: { account: {} }, activeAccountId: 'account' }),
|
selector({ accountsEditsSummaries: { account: {} }, activeAccountId: 'account' }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@floating-ui/react', () => ({
|
||||||
|
autoUpdate: vi.fn(),
|
||||||
|
flip: vi.fn(),
|
||||||
|
offset: vi.fn(),
|
||||||
|
shift: vi.fn(),
|
||||||
|
size: vi.fn(),
|
||||||
|
useFloating: () => ({
|
||||||
|
floatingStyles: { position: 'fixed' },
|
||||||
|
refs: {
|
||||||
|
setFloating: () => undefined,
|
||||||
|
setReference: () => undefined,
|
||||||
|
},
|
||||||
|
update: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('react-virtuoso', () => ({
|
vi.mock('react-virtuoso', () => ({
|
||||||
Virtuoso: ({
|
Virtuoso: ({
|
||||||
components,
|
components,
|
||||||
@@ -172,7 +191,18 @@ vi.mock('../../../components/tooltip', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../post/post', () => ({
|
vi.mock('../../post/post', () => ({
|
||||||
Post: () => createElement('div', { 'data-testid': 'mod-queue-feed-post' }),
|
Post: ({ isModQueue, post, showReplies }: { isModQueue?: boolean; post?: TestComment; showReplies?: boolean }) =>
|
||||||
|
createElement(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
'data-cid': post?.cid,
|
||||||
|
'data-content': post?.content,
|
||||||
|
'data-is-mod-queue': String(Boolean(isModQueue)),
|
||||||
|
'data-show-replies': String(Boolean(showReplies)),
|
||||||
|
'data-testid': 'mod-queue-feed-post',
|
||||||
|
},
|
||||||
|
post?.cid ?? 'missing',
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
@@ -234,4 +264,62 @@ describe('ModQueueView', () => {
|
|||||||
expect(text).toContain('queue_is_empty');
|
expect(text).toContain('queue_is_empty');
|
||||||
expect(text.indexOf('No.')).toBeLessThan(text.indexOf('queue_is_empty'));
|
expect(text.indexOf('No.')).toBeLessThan(text.indexOf('queue_is_empty'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens a full floating post preview from a compact excerpt hover', async () => {
|
||||||
|
testState.feed = [
|
||||||
|
{
|
||||||
|
cid: 'pending-reply',
|
||||||
|
communityAddress: 'music-posting.eth',
|
||||||
|
content: 'pending reply body',
|
||||||
|
number: 7,
|
||||||
|
pendingApproval: true,
|
||||||
|
timestamp: 90_000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await renderModQueue();
|
||||||
|
|
||||||
|
const excerptLink = Array.from(container.querySelectorAll<HTMLAnchorElement>('a')).find((link) => link.textContent === 'pending reply body');
|
||||||
|
expect(excerptLink).toBeTruthy();
|
||||||
|
expect(document.body.querySelector('[data-mod-queue-excerpt-preview="true"]')).toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
excerptLink?.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewPost = document.body.querySelector('[data-mod-queue-excerpt-preview="true"] [data-testid="mod-queue-feed-post"]');
|
||||||
|
expect(previewPost?.getAttribute('data-cid')).toBe('pending-reply');
|
||||||
|
// Rendered like a quote-link hover preview: a clean read-only Post, not the
|
||||||
|
// mod-queue feed layout (no leading <hr>, no inline approve/reject buttons).
|
||||||
|
expect(previewPost?.getAttribute('data-is-mod-queue')).toBe('false');
|
||||||
|
expect(previewPost?.getAttribute('data-show-replies')).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caps long content in the floating preview so the hover card stays compact', async () => {
|
||||||
|
testState.feed = [
|
||||||
|
{
|
||||||
|
cid: 'long-post',
|
||||||
|
communityAddress: 'music-posting.eth',
|
||||||
|
content: 'x'.repeat(500),
|
||||||
|
number: 9,
|
||||||
|
pendingApproval: true,
|
||||||
|
timestamp: 90_000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await renderModQueue();
|
||||||
|
|
||||||
|
const excerptLink = Array.from(container.querySelectorAll<HTMLAnchorElement>('a')).find((link) => link.textContent?.startsWith('xxx'));
|
||||||
|
expect(excerptLink).toBeTruthy();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
excerptLink?.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewPost = document.body.querySelector('[data-mod-queue-excerpt-preview="true"] [data-testid="mod-queue-feed-post"]');
|
||||||
|
const previewContent = previewPost?.getAttribute('data-content') ?? '';
|
||||||
|
// 350-char cap + a single ellipsis character (shorter than the feed's 1000).
|
||||||
|
expect(previewContent.length).toBe(351);
|
||||||
|
expect(previewContent.endsWith('…')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React, { useMemo, useState, useEffect, useCallback, memo } from 'react';
|
import React, { useMemo, useState, useEffect, useCallback, memo } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
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, useCommunity, useAccount } from '@bitsocial/bitsocial-react-hooks';
|
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useCommunity, useAccount } from '@bitsocial/bitsocial-react-hooks';
|
||||||
import useAccountsStore from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts/index.js';
|
import useAccountsStore from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts/index.js';
|
||||||
|
import { useFloating, offset, shift, size, flip, autoUpdate } from '@floating-ui/react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import styles from './mod-queue.module.css';
|
import styles from './mod-queue.module.css';
|
||||||
|
import postStyles from '../post/post.module.css';
|
||||||
import useModQueueStore from '../../stores/use-mod-queue-store';
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||||
import ErrorDisplay from '../../components/error-display/error-display';
|
import ErrorDisplay from '../../components/error-display/error-display';
|
||||||
@@ -169,6 +172,114 @@ interface ModQueueActionsProps {
|
|||||||
variant: 'row' | 'card';
|
variant: 'row' | 'card';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ModQueueExcerptPreviewLinkProps {
|
||||||
|
comment: Comment;
|
||||||
|
excerpt: string;
|
||||||
|
postUrl: string | undefined;
|
||||||
|
postUrlState: ReturnType<typeof getQueuedCommentRouteState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExcerptPreviewClassName = (comment: Comment) =>
|
||||||
|
!comment.parentCid ? `${postStyles.replyQuotePreview} ${postStyles.replyQuotePreviewOp}` : postStyles.replyQuotePreview;
|
||||||
|
|
||||||
|
// The hover card is a compact preview, so cap its body shorter than the feed's
|
||||||
|
// 1000-char CommentContent limit and mark the cut with an ellipsis.
|
||||||
|
const PREVIEW_CONTENT_MAX_LENGTH = 350;
|
||||||
|
|
||||||
|
const truncatePreviewComment = (comment: Comment): Comment => {
|
||||||
|
const { content } = comment;
|
||||||
|
if (typeof content !== 'string' || content.length <= PREVIEW_CONTENT_MAX_LENGTH) {
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
return { ...comment, content: `${content.slice(0, PREVIEW_CONTENT_MAX_LENGTH).trimEnd()}…` };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Floating preview of the full pending post, anchored to the excerpt text.
|
||||||
|
// Mirrors the quote-link hover previews (reply-quote-preview): a clean read-only
|
||||||
|
// Post positioned with useFloating, not the feed/mod-queue layout. Rendering it
|
||||||
|
// without isModQueue keeps it consistent with quote previews (no leading <hr>,
|
||||||
|
// no thread container, no inline approve/reject buttons that hover can't reach).
|
||||||
|
//
|
||||||
|
// Placement mirrors the quote previews: to the right of the excerpt on desktop,
|
||||||
|
// below it on mobile. See setReferenceNode for why desktop anchors to the cell.
|
||||||
|
const ModQueueExcerptPreviewLink = ({ comment, excerpt, postUrl, postUrlState }: ModQueueExcerptPreviewLinkProps) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
|
const previewComment = truncatePreviewComment(comment);
|
||||||
|
|
||||||
|
const { refs, floatingStyles } = useFloating({
|
||||||
|
placement: isMobile ? 'bottom-start' : 'right-start',
|
||||||
|
middleware: [
|
||||||
|
offset(isMobile ? 4 : 8),
|
||||||
|
flip({ fallbackPlacements: isMobile ? ['top-start'] : ['left-start'] }),
|
||||||
|
shift({ padding: 10 }),
|
||||||
|
size({
|
||||||
|
padding: 10,
|
||||||
|
apply({ availableWidth, elements }) {
|
||||||
|
elements.floating.style.maxWidth = `${Math.max(0, availableWidth - 12)}px`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
});
|
||||||
|
// Stable callback refs from useFloating (not React refs read during render).
|
||||||
|
const { setReference, setFloating } = refs;
|
||||||
|
|
||||||
|
// Desktop anchors the card to the excerpt cell, not the inline <a>. The excerpt
|
||||||
|
// is a wide single-line link whose box overflows the clipped flex cell, so its
|
||||||
|
// right edge sits far past the visible text; anchoring there shoved the card off
|
||||||
|
// to the right. The parent cell's right edge tracks where the ellipsized excerpt
|
||||||
|
// visibly ends. Mobile keeps the <a> itself and opens below it.
|
||||||
|
const setReferenceNode = (node: HTMLElement | null) => {
|
||||||
|
setReference(!node || isMobile ? node : (node.parentElement ?? node));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPreview = () => setIsPreviewOpen(true);
|
||||||
|
const closePreview = () => setIsPreviewOpen(false);
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
isPreviewOpen && typeof document !== 'undefined'
|
||||||
|
? createPortal(
|
||||||
|
<div className={getExcerptPreviewClassName(comment)} data-mod-queue-excerpt-preview='true' ref={setFloating} style={floatingStyles}>
|
||||||
|
<Post post={previewComment} showReplies={false} />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{postUrl ? (
|
||||||
|
<Link
|
||||||
|
to={postUrl}
|
||||||
|
state={postUrlState}
|
||||||
|
title={excerpt}
|
||||||
|
ref={setReferenceNode}
|
||||||
|
onMouseEnter={openPreview}
|
||||||
|
onFocus={openPreview}
|
||||||
|
onMouseLeave={closePreview}
|
||||||
|
onBlur={closePreview}
|
||||||
|
>
|
||||||
|
{excerpt}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
title={excerpt}
|
||||||
|
ref={setReferenceNode}
|
||||||
|
tabIndex={0}
|
||||||
|
onMouseEnter={openPreview}
|
||||||
|
onFocus={openPreview}
|
||||||
|
onMouseLeave={closePreview}
|
||||||
|
onBlur={closePreview}
|
||||||
|
>
|
||||||
|
{excerpt}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{preview}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove, variant }: ModQueueActionsProps) => {
|
const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleApprove, handleReject, handleRemove, variant }: ModQueueActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const displayError = error || errorMessage;
|
const displayError = error || errorMessage;
|
||||||
@@ -419,13 +530,7 @@ const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath
|
|||||||
<div className={styles.board}>{modQueueUrl ? <Link to={modQueueUrl}>/{boardDisplayPath ?? '—'}/</Link> : <span>/{boardDisplayPath ?? '—'}/</span>}</div>
|
<div className={styles.board}>{modQueueUrl ? <Link to={modQueueUrl}>/{boardDisplayPath ?? '—'}/</Link> : <span>/{boardDisplayPath ?? '—'}/</span>}</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.excerpt}>
|
<div className={styles.excerpt}>
|
||||||
{postUrl ? (
|
<ModQueueExcerptPreviewLink comment={displayComment} excerpt={excerpt} postUrl={postUrl} postUrlState={postUrlState} />
|
||||||
<Link to={postUrl} state={postUrlState} title={excerpt}>
|
|
||||||
{excerpt}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span title={excerpt}>{excerpt}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.time}>
|
<div className={styles.time}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
@@ -539,15 +644,8 @@ const ModQueueCard = memo(({ comment, showBoard = false, boardPath, boardDisplay
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.cardContent}>
|
<div className={styles.cardContent}>
|
||||||
{t('excerpt')}:{' '}
|
{t('excerpt')}: <ModQueueExcerptPreviewLink comment={displayComment} excerpt={excerpt} postUrl={postUrl} postUrlState={postUrlState} /> / {t('type')}:{' '}
|
||||||
{postUrl ? (
|
{isReply ? t('reply') : t('post')} / {capitalize(t('image'))}: {hasThumbnail ? lowerCase(t('yes')) : lowerCase(t('no'))}
|
||||||
<Link to={postUrl} state={postUrlState} title={excerpt}>
|
|
||||||
{excerpt}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span title={excerpt}>{excerpt}</span>
|
|
||||||
)}{' '}
|
|
||||||
/ {t('type')}: {isReply ? t('reply') : t('post')} / {capitalize(t('image'))}: {hasThumbnail ? lowerCase(t('yes')) : lowerCase(t('no'))}
|
|
||||||
</div>
|
</div>
|
||||||
<ModQueueActions
|
<ModQueueActions
|
||||||
status={status}
|
status={status}
|
||||||
|
|||||||
Reference in New Issue
Block a user