mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(media): clarify failed external image embeds
This commit is contained in:
@@ -185,7 +185,7 @@ const renderContent = async (comment: TestComment) => {
|
||||
});
|
||||
};
|
||||
|
||||
const renderContentWithProps = async (props: { comment: TestComment; prependContent?: React.ReactNode }) => {
|
||||
const renderContentWithProps = async (props: { appendContent?: React.ReactNode; comment: TestComment; prependContent?: React.ReactNode }) => {
|
||||
await act(async () => {
|
||||
root.render(createElement(CommentContent, props as any));
|
||||
});
|
||||
@@ -268,6 +268,26 @@ describe('CommentContent', () => {
|
||||
expect(blockquote?.querySelectorAll('br')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders appended content after markdown with a two-line break separator', async () => {
|
||||
await renderContentWithProps({
|
||||
appendContent: createElement('span', { 'data-testid': 'append' }, 'media failed'),
|
||||
comment: {
|
||||
cid: 'post-1',
|
||||
content: 'body',
|
||||
postCid: 'post-1',
|
||||
},
|
||||
});
|
||||
|
||||
const blockquote = container.querySelector('blockquote');
|
||||
const markdown = container.querySelector('[data-testid="markdown"]');
|
||||
const append = container.querySelector('[data-testid="append"]');
|
||||
|
||||
expect(markdown?.textContent).toBe('body');
|
||||
expect(append?.textContent).toBe('media failed');
|
||||
expect(blockquote?.lastChild).toBe(append);
|
||||
expect(blockquote?.querySelectorAll('br')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('truncates long comments outside the post view and expands them on demand', async () => {
|
||||
const longComment = 'x'.repeat(1105);
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const getFailedCommentError = (comment: Comment | undefined): unknown => {
|
||||
return Array.isArray(comment.errors) ? comment.errors.find(Boolean) : undefined;
|
||||
};
|
||||
|
||||
const CommentContent = ({ comment: post, prependContent }: { comment: Comment; prependContent?: ReactNode }) => {
|
||||
const CommentContent = ({ appendContent, comment: post, prependContent }: { appendContent?: ReactNode; comment: Comment; prependContent?: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
@@ -306,6 +306,13 @@ const CommentContent = ({ comment: post, prependContent }: { comment: Comment; p
|
||||
{loadingString}
|
||||
</>
|
||||
)}
|
||||
{appendContent && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
{appendContent}
|
||||
</>
|
||||
)}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ const testState = vi.hoisted(() => ({
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
t: (key: string, options?: { host?: string }) => (options?.host ? `${key}:${options.host}` : key),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -151,24 +151,73 @@ describe('CommentMedia', () => {
|
||||
expect(container.querySelector('[data-expanded-media="true"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('falls back to the deleted-file placeholder when an image fails to load', async () => {
|
||||
it('shows a media-load warning when an image fails to load', async () => {
|
||||
testState.hostname = 'files.catbox.moe';
|
||||
const onMediaLoadFailureChange = vi.fn();
|
||||
|
||||
await renderMedia({
|
||||
commentMediaInfo: {
|
||||
type: 'image',
|
||||
url: 'https://cdn.example.com/missing.jpg',
|
||||
url: 'https://files.catbox.moe/missing.jpg',
|
||||
},
|
||||
onMediaLoadFailureChange,
|
||||
setShowThumbnail: setShowThumbnailMock,
|
||||
showThumbnail: true,
|
||||
});
|
||||
|
||||
const image = container.querySelector('img[src="https://cdn.example.com/missing.jpg"]');
|
||||
const image = container.querySelector('img[src="https://files.catbox.moe/missing.jpg"]');
|
||||
expect(image).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
image?.dispatchEvent(new Event('error', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(container.querySelector('img[alt="File deleted"]')).toBeTruthy();
|
||||
expect(container.querySelector('img[src="assets/filedeleted-res.gif"]')).toBeTruthy();
|
||||
expect(onMediaLoadFailureChange).toHaveBeenCalledWith('https://files.catbox.moe/missing.jpg');
|
||||
const status = container.querySelector('[role="status"]');
|
||||
expect(status?.textContent).toContain('media_failed_to_load_inline:files.catbox.moe');
|
||||
expect(status?.textContent).toContain('media_failed_to_load_open_source');
|
||||
expect(status?.textContent).not.toContain('media_failed_to_load_hint');
|
||||
expect(status?.getAttribute('aria-label')).toBe(
|
||||
'media_failed_to_load. media_failed_to_load_source:files.catbox.moe. media_failed_to_load_hint. media_failed_to_load_open_source.',
|
||||
);
|
||||
expect(status?.getAttribute('title')).toBe(
|
||||
'media_failed_to_load. media_failed_to_load_source:files.catbox.moe. media_failed_to_load_hint. media_failed_to_load_open_source.',
|
||||
);
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="https://files.catbox.moe/missing.jpg"]')?.textContent).toBe('media_failed_to_load_open_source');
|
||||
});
|
||||
|
||||
it('moves media-load guidance below failed image thumbnails on mobile', async () => {
|
||||
testState.hostname = 'files.catbox.moe';
|
||||
testState.isMobile = true;
|
||||
const onMediaLoadFailureChange = vi.fn();
|
||||
|
||||
await renderMedia({
|
||||
commentMediaInfo: {
|
||||
type: 'image',
|
||||
url: 'https://files.catbox.moe/missing.jpg',
|
||||
},
|
||||
onMediaLoadFailureChange,
|
||||
setShowThumbnail: setShowThumbnailMock,
|
||||
showThumbnail: true,
|
||||
});
|
||||
|
||||
const image = container.querySelector('img[src="https://files.catbox.moe/missing.jpg"]');
|
||||
expect(image).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
image?.dispatchEvent(new Event('error', { bubbles: true }));
|
||||
});
|
||||
|
||||
const status = container.querySelector('[role="status"]');
|
||||
expect(status?.textContent).toBe('');
|
||||
expect(status?.getAttribute('aria-label')).toBe(
|
||||
'media_failed_to_load. media_failed_to_load_source:files.catbox.moe. media_failed_to_load_hint. media_failed_to_load_open_source.',
|
||||
);
|
||||
expect(onMediaLoadFailureChange).toHaveBeenCalledWith('https://files.catbox.moe/missing.jpg');
|
||||
expect(container.textContent).not.toContain('media_failed_to_load_inline:files.catbox.moe');
|
||||
expect(container.textContent).not.toContain('media_failed_to_load_open_source');
|
||||
expect(container.textContent).toContain('image');
|
||||
});
|
||||
|
||||
it('renders GIF thumbnail states and toggles the media view from the placeholder', async () => {
|
||||
|
||||
@@ -169,6 +169,69 @@
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.mediaLoadFailure {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.2;
|
||||
font-size: 10pt;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mediaLoadFailureSource {
|
||||
max-width: 30ch;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.mediaLoadFailureStatus {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.mediaLoadFailureLink {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: var(--post-link-text-decoration);
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.mediaLoadFailureLink:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.mediaLoadFailureInfo {
|
||||
display: block;
|
||||
clear: both;
|
||||
color: var(--post-mobile-file-info-text-color);
|
||||
font-size: 9pt;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.mediaLoadFailureInfo a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: var(--post-link-text-decoration);
|
||||
}
|
||||
|
||||
.mediaLoadFailureInfo a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.thumbnail,
|
||||
.content {
|
||||
|
||||
@@ -25,10 +25,68 @@ interface MediaProps {
|
||||
spoiler?: boolean;
|
||||
showThumbnail?: boolean;
|
||||
setShowThumbnail: (showThumbnail: boolean) => void;
|
||||
onMediaLoadFailureChange?: (url: string | undefined) => void;
|
||||
}
|
||||
|
||||
type GifFrameState = ReturnType<typeof useFetchGifFirstFrame>;
|
||||
|
||||
const getMediaLoadFailureLabels = (url: string | undefined, t: ReturnType<typeof useTranslation>['t']) => {
|
||||
const hostname = url ? getHostname(url) : undefined;
|
||||
const label = t('media_failed_to_load');
|
||||
const source = hostname ? t('media_failed_to_load_source', { host: hostname }) : t('media_failed_to_load_source_unknown');
|
||||
const hint = t('media_failed_to_load_hint');
|
||||
const openSourceLabel = t('media_failed_to_load_open_source');
|
||||
const inline = hostname ? t('media_failed_to_load_inline', { host: hostname }) : t('media_failed_to_load_inline_unknown');
|
||||
const statusLabel = url ? `${label}. ${source}. ${hint}. ${openSourceLabel}.` : `${label}. ${source}. ${hint}.`;
|
||||
|
||||
return { inline, openSourceLabel, statusLabel };
|
||||
};
|
||||
|
||||
const MediaLoadFailure = ({ compact = false, url }: { compact?: boolean; url?: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { inline, openSourceLabel, statusLabel } = getMediaLoadFailureLabels(url, t);
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<>
|
||||
<img className={styles.fileDeleted} src='assets/filedeleted-res.gif' alt='' aria-hidden='true' />
|
||||
<span className={styles.mediaLoadFailureStatus} role='status' aria-label={statusLabel} title={statusLabel} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.mediaLoadFailure} role='status' aria-label={statusLabel} title={statusLabel}>
|
||||
<img className={styles.fileDeleted} src='assets/filedeleted-res.gif' alt='' aria-hidden='true' />
|
||||
<span className={styles.mediaLoadFailureSource}>{inline}</span>
|
||||
{url && (
|
||||
<a className={styles.mediaLoadFailureLink} href={url} target='_blank' rel='noopener noreferrer'>
|
||||
{openSourceLabel}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const MediaLoadFailureInfo = ({ url }: { url?: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { inline, openSourceLabel, statusLabel } = getMediaLoadFailureLabels(url, t);
|
||||
|
||||
return (
|
||||
<div className={styles.mediaLoadFailureInfo} title={statusLabel}>
|
||||
{inline}
|
||||
{url && (
|
||||
<>
|
||||
{' '}
|
||||
<a href={url} target='_blank' rel='noopener noreferrer'>
|
||||
{openSourceLabel}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Thumbnail = ({
|
||||
commentMediaInfo,
|
||||
deleted,
|
||||
@@ -290,11 +348,22 @@ interface ImageProps {
|
||||
displayWidth: string;
|
||||
initialExpanded?: boolean;
|
||||
isOutOfFeed: boolean;
|
||||
onMediaLoadFailureChange?: (url: string | undefined) => void;
|
||||
parentCid?: string;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
const Image = ({ commentMediaInfo, disableToggle = false, displayHeight, displayWidth, initialExpanded = false, isOutOfFeed, parentCid, spoiler }: ImageProps) => {
|
||||
const Image = ({
|
||||
commentMediaInfo,
|
||||
disableToggle = false,
|
||||
displayHeight,
|
||||
displayWidth,
|
||||
initialExpanded = false,
|
||||
isOutOfFeed,
|
||||
onMediaLoadFailureChange,
|
||||
parentCid,
|
||||
spoiler,
|
||||
}: ImageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { type, url } = commentMediaInfo || {};
|
||||
const isReply = parentCid;
|
||||
@@ -310,8 +379,16 @@ const Image = ({ commentMediaInfo, disableToggle = false, displayHeight, display
|
||||
const expandedMediaAttribute = isImageExpanded ? 'true' : undefined;
|
||||
const imageMediaStyle = isImageExpanded ? undefined : thumbnailDimensions;
|
||||
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const handleError = () => setHasError(true);
|
||||
const [failedUrl, setFailedUrl] = useState<string | undefined>();
|
||||
const hasError = failedUrl === url;
|
||||
const handleError = () => {
|
||||
setFailedUrl(url);
|
||||
onMediaLoadFailureChange?.(url);
|
||||
};
|
||||
const handleLoad = () => {
|
||||
setFailedUrl((currentFailedUrl) => (currentFailedUrl === url ? undefined : currentFailedUrl));
|
||||
onMediaLoadFailureChange?.(undefined);
|
||||
};
|
||||
|
||||
if (spoiler && !isImageExpanded) {
|
||||
const spoilerDimensions = { '--width': '150px', '--height': '150px' } as React.CSSProperties;
|
||||
@@ -346,11 +423,12 @@ const Image = ({ commentMediaInfo, disableToggle = false, displayHeight, display
|
||||
style={imageMediaStyle}
|
||||
>
|
||||
{hasError ? (
|
||||
<img src='assets/filedeleted-res.gif' alt='File deleted' />
|
||||
<MediaLoadFailure compact url={url} />
|
||||
) : (
|
||||
<img
|
||||
src={url}
|
||||
onError={handleError}
|
||||
onLoad={handleLoad}
|
||||
alt=''
|
||||
role={disableToggle ? undefined : 'button'}
|
||||
tabIndex={disableToggle ? undefined : 0}
|
||||
@@ -386,11 +464,12 @@ const Image = ({ commentMediaInfo, disableToggle = false, displayHeight, display
|
||||
style={imageMediaStyle}
|
||||
>
|
||||
{hasError ? (
|
||||
<img src='assets/filedeleted-res.gif' alt='File deleted' />
|
||||
<MediaLoadFailure url={url} />
|
||||
) : (
|
||||
<img
|
||||
src={url}
|
||||
onError={handleError}
|
||||
onLoad={handleLoad}
|
||||
alt=''
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
@@ -424,6 +503,7 @@ const CommentMedia = ({
|
||||
showThumbnail,
|
||||
setShowThumbnail,
|
||||
spoiler,
|
||||
onMediaLoadFailureChange,
|
||||
}: MediaProps) => {
|
||||
const isReply = parentCid;
|
||||
const { t } = useTranslation();
|
||||
@@ -476,6 +556,7 @@ const CommentMedia = ({
|
||||
displayWidth={displayWidth}
|
||||
initialExpanded={showThumbnail === false}
|
||||
isOutOfFeed={isOutOfFeed}
|
||||
onMediaLoadFailureChange={onMediaLoadFailureChange}
|
||||
parentCid={parentCid}
|
||||
spoiler={spoiler}
|
||||
/>
|
||||
@@ -516,6 +597,7 @@ export default memo(CommentMedia, (prev, next) => {
|
||||
prev.isReply === next.isReply &&
|
||||
prev.linkHeight === next.linkHeight &&
|
||||
prev.linkWidth === next.linkWidth &&
|
||||
prev.onMediaLoadFailureChange === next.onMediaLoadFailureChange &&
|
||||
prev.parentCid === next.parentCid &&
|
||||
prev.purged === next.purged &&
|
||||
prev.removed === next.removed &&
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default } from './comment-media';
|
||||
export { default, MediaLoadFailureInfo } from './comment-media';
|
||||
|
||||
@@ -25,7 +25,7 @@ import useSafeAccountComment from '../../hooks/use-safe-account-comment';
|
||||
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||
import { useBoardPseudonymityMode } from '../../hooks/use-board-pseudonymity-mode';
|
||||
import CommentContent from '../comment-content';
|
||||
import CommentMedia from '../comment-media';
|
||||
import CommentMedia, { MediaLoadFailureInfo } from '../comment-media';
|
||||
import FailedPublishNotice from '../failed-publish-notice';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import PostMenuMobile from './post-menu-mobile';
|
||||
@@ -70,7 +70,14 @@ const RepliesFooter = ({ hasMore, loadingString }: { hasMore: boolean; loadingSt
|
||||
// Store scroll position for replies virtuoso across navigations
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, postsByAuthorInThread }: PostProps & { postsByAuthorInThread?: Map<string, number> }) => {
|
||||
const PostInfoAndMedia = ({
|
||||
onMediaLoadFailureChange,
|
||||
post,
|
||||
postReplyCount = 0,
|
||||
roles,
|
||||
threadNumber,
|
||||
postsByAuthorInThread,
|
||||
}: PostProps & { onMediaLoadFailureChange?: (url: string | undefined) => void; postsByAuthorInThread?: Map<string, number> }) => {
|
||||
const { t } = useTranslation();
|
||||
const directories = useDirectories();
|
||||
const resolvedPost = withResolvedCommentCommunityAddress(post);
|
||||
@@ -441,12 +448,22 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, posts
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{(hasThumbnail || link) && !(deleted || removed || purged) && <PostMediaContent key={cid} post={resolvedPost} link={link} />}
|
||||
{(hasThumbnail || link) && !(deleted || removed || purged) && (
|
||||
<PostMediaContent key={cid} onMediaLoadFailureChange={onMediaLoadFailureChange} post={resolvedPost} link={link} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PostMediaContent = ({ post, link }: { post: Comment | undefined; link: string }) => {
|
||||
const PostMediaContent = ({
|
||||
onMediaLoadFailureChange,
|
||||
post,
|
||||
link,
|
||||
}: {
|
||||
onMediaLoadFailureChange?: (url: string | undefined) => void;
|
||||
post: Comment | undefined;
|
||||
link: string;
|
||||
}) => {
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
const { thumbnailUrl, linkWidth, linkHeight, spoiler, deleted, removed, parentCid } = post || {};
|
||||
const purged = post?.commentModeration?.purged;
|
||||
@@ -465,6 +482,7 @@ const PostMediaContent = ({ post, link }: { post: Comment | undefined; link: str
|
||||
setShowThumbnail={setShowThumbnail}
|
||||
parentCid={parentCid}
|
||||
spoiler={spoiler}
|
||||
onMediaLoadFailureChange={onMediaLoadFailureChange}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -536,6 +554,8 @@ const Reply = ({
|
||||
onRetry={canRetryFailedPost ? onRetryFailedPost : undefined}
|
||||
/>
|
||||
) : undefined;
|
||||
const [failedMediaUrl, setFailedMediaUrl] = useState<string | undefined>();
|
||||
const mediaLoadFailureInfo = failedMediaUrl && failedMediaUrl === post?.link ? <MediaLoadFailureInfo url={failedMediaUrl} /> : undefined;
|
||||
|
||||
return (
|
||||
<div className={`${styles.replyMobile} ${disableDeferredLayout ? styles.pretextVirtualizedReply : ''}`}>
|
||||
@@ -546,9 +566,16 @@ const Reply = ({
|
||||
data-author-address={author?.shortAddress}
|
||||
data-post-cid={postCid}
|
||||
>
|
||||
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} postsByAuthorInThread={postsByAuthorInThread} roles={roles} threadNumber={threadNumber} />
|
||||
<PostInfoAndMedia
|
||||
onMediaLoadFailureChange={setFailedMediaUrl}
|
||||
post={post}
|
||||
postReplyCount={postReplyCount}
|
||||
postsByAuthorInThread={postsByAuthorInThread}
|
||||
roles={roles}
|
||||
threadNumber={threadNumber}
|
||||
/>
|
||||
{post && !hidden && (!(removed || deleted || purged) || ((removed || deleted) && reason) || purged) && (
|
||||
<CommentContent comment={post} prependContent={failedPublishNotice} />
|
||||
<CommentContent appendContent={mediaLoadFailureInfo} comment={post} prependContent={failedPublishNotice} />
|
||||
)}
|
||||
{post && <ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />}
|
||||
</div>
|
||||
@@ -681,6 +708,8 @@ const PostMobile = ({
|
||||
onRetry={canRetryFailedPost ? onRetryFailedPost : undefined}
|
||||
/>
|
||||
) : undefined;
|
||||
const [failedMediaUrl, setFailedMediaUrl] = useState<string | undefined>();
|
||||
const mediaLoadFailureInfo = failedMediaUrl && failedMediaUrl === resolvedPost?.link ? <MediaLoadFailureInfo url={failedMediaUrl} /> : undefined;
|
||||
|
||||
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
|
||||
const filteredReplies = useMemo(() => filterRepliesForDisplay(freshRepliesForRender), [freshRepliesForRender]);
|
||||
@@ -837,13 +866,14 @@ const PostMobile = ({
|
||||
>
|
||||
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||
<PostInfoAndMedia
|
||||
onMediaLoadFailureChange={setFailedMediaUrl}
|
||||
post={resolvedPost}
|
||||
postReplyCount={replyCount}
|
||||
postsByAuthorInThread={postsByAuthorInThread}
|
||||
roles={roles}
|
||||
threadNumber={resolvedPost?.number}
|
||||
/>
|
||||
{resolvedPost && <CommentContent comment={resolvedPost} prependContent={failedPublishNotice} />}
|
||||
{resolvedPost && <CommentContent appendContent={mediaLoadFailureInfo} comment={resolvedPost} prependContent={failedPublishNotice} />}
|
||||
{resolvedPost && <ReplyBacklinks post={resolvedPost} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />}
|
||||
</div>
|
||||
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
|
||||
|
||||
Reference in New Issue
Block a user