mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post): move failed publish notice into comment body (#1066)
Align the failed notice and delete action with the existing post card and footer controls.
This commit is contained in:
@@ -169,6 +169,12 @@ const renderContent = async (comment: TestComment) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderContentWithProps = async (props: { comment: TestComment; prependContent?: React.ReactNode }) => {
|
||||||
|
await act(async () => {
|
||||||
|
root.render(createElement(CommentContent, props as any));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const queryMarkdownText = () => Array.from(container.querySelectorAll('[data-testid="markdown"]')).map((node) => node.textContent ?? '');
|
const queryMarkdownText = () => Array.from(container.querySelectorAll('[data-testid="markdown"]')).map((node) => node.textContent ?? '');
|
||||||
|
|
||||||
describe('CommentContent', () => {
|
describe('CommentContent', () => {
|
||||||
@@ -217,6 +223,35 @@ describe('CommentContent', () => {
|
|||||||
expect(previews[0]?.textContent).toBe('quoted-2');
|
expect(previews[0]?.textContent).toBe('quoted-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders prepended content before reply quote previews and markdown', async () => {
|
||||||
|
testState.commentsByCid = {
|
||||||
|
'quoted-1': { cid: 'quoted-1', number: 11 },
|
||||||
|
};
|
||||||
|
testState.postNumbers = {
|
||||||
|
'quoted-1': 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderContentWithProps({
|
||||||
|
comment: {
|
||||||
|
cid: 'reply-1',
|
||||||
|
content: 'body',
|
||||||
|
parentCid: 'quoted-1',
|
||||||
|
postCid: 'post-1',
|
||||||
|
},
|
||||||
|
prependContent: createElement('span', { 'data-testid': 'prepend' }, 'failed notice'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockquote = container.querySelector('blockquote');
|
||||||
|
const prepend = container.querySelector('[data-testid="prepend"]');
|
||||||
|
const preview = container.querySelector('[data-testid="reply-quote-preview"]');
|
||||||
|
const markdown = container.querySelector('[data-testid="markdown"]');
|
||||||
|
|
||||||
|
expect(blockquote?.firstChild).toBe(prepend);
|
||||||
|
expect(preview?.textContent).toBe('quoted-1');
|
||||||
|
expect(markdown?.textContent).toBe('body');
|
||||||
|
expect(blockquote?.querySelectorAll('br')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('truncates long comments outside the post view and expands them on demand', async () => {
|
it('truncates long comments outside the post view and expands them on demand', async () => {
|
||||||
const longComment = 'x'.repeat(1105);
|
const longComment = 'x'.repeat(1105);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment, useMemo, useState } from 'react';
|
import { Fragment, type ReactNode, useMemo, useState } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Comment, useComment } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment, useComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
@@ -62,7 +62,7 @@ const useScopedCidToNumber = (cids: string[]) => {
|
|||||||
return cidToNumber;
|
return cidToNumber;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
const CommentContent = ({ comment: post, prependContent }: { comment: Comment; prependContent?: ReactNode }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -126,6 +126,12 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines}`}>
|
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines}`}>
|
||||||
|
{prependContent && (
|
||||||
|
<>
|
||||||
|
{prependContent}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{isReply &&
|
{isReply &&
|
||||||
!hasFailedState &&
|
!hasFailedState &&
|
||||||
!(deleted || removed || purged) &&
|
!(deleted || removed || purged) &&
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import styles from '../views/post/post.module.css';
|
||||||
|
import useIsMobile from '../hooks/use-is-mobile';
|
||||||
|
|
||||||
|
interface FailedPublishNoticeProps {
|
||||||
|
isDeleting: boolean;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FailedPublishNotice = ({ isDeleting, onDelete }: FailedPublishNoticeProps) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.failedPublishNotice}>
|
||||||
|
This post failed to publish, it's not visible to other users.{' '}
|
||||||
|
{isMobile && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span className={styles.failedDeletePostAction}>
|
||||||
|
[
|
||||||
|
<button type='button' className={styles.failedDeletePostButton} disabled={isDeleting} onClick={onDelete}>
|
||||||
|
Delete Post
|
||||||
|
</button>
|
||||||
|
]
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FailedPublishNotice;
|
||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from 'react';
|
|||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||||
import { Comment, deleteComment, useEditedComment, useReplies, useAccount, useAccountComment } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment, useEditedComment, useReplies, useAccount, useAccountComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import getShortAddress from '../../lib/get-short-address';
|
import getShortAddress from '../../lib/get-short-address';
|
||||||
import styles from '../../views/post/post.module.css';
|
import styles from '../../views/post/post.module.css';
|
||||||
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
||||||
@@ -27,6 +27,7 @@ import { useBoardPseudonymityMode } from '../../hooks/use-board-pseudonymity-mod
|
|||||||
import CommentContent from '../comment-content';
|
import CommentContent from '../comment-content';
|
||||||
import CommentMedia from '../comment-media';
|
import CommentMedia from '../comment-media';
|
||||||
import EditMenu from '../edit-menu/edit-menu';
|
import EditMenu from '../edit-menu/edit-menu';
|
||||||
|
import FailedPublishNotice from '../failed-publish-notice';
|
||||||
import { canEmbed } from '../embed';
|
import { canEmbed } from '../embed';
|
||||||
import LoadingEllipsis from '../loading-ellipsis';
|
import LoadingEllipsis from '../loading-ellipsis';
|
||||||
import PostMenuDesktop from './post-menu-desktop';
|
import PostMenuDesktop from './post-menu-desktop';
|
||||||
@@ -50,6 +51,7 @@ import useFreshReplies from '../../hooks/use-fresh-replies';
|
|||||||
import { BOARD_REPLIES_PREVIEW_FETCH_SIZE, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, REPLIES_PER_PAGE } from '../../lib/constants';
|
import { BOARD_REPLIES_PREVIEW_FETCH_SIZE, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, REPLIES_PER_PAGE } from '../../lib/constants';
|
||||||
import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount } from '../../lib/utils/replies-preview-utils';
|
import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount } from '../../lib/utils/replies-preview-utils';
|
||||||
import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
|
import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
|
||||||
|
import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -102,7 +104,6 @@ const PostInfo = ({
|
|||||||
const displayName = author?.displayName?.trim();
|
const displayName = author?.displayName?.trim();
|
||||||
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
||||||
const hasFailedState = state === 'failed';
|
const hasFailedState = state === 'failed';
|
||||||
const canDeleteFailedPost = hasFailedState && typeof post?.index === 'number';
|
|
||||||
const isReply = parentCid;
|
const isReply = parentCid;
|
||||||
const { showOmittedReplies } = useShowOmittedReplies();
|
const { showOmittedReplies } = useShowOmittedReplies();
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
@@ -166,8 +167,6 @@ const PostInfo = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [initiatedPendingAction, setInitiatedPendingAction] = useState<'approve' | 'reject' | null>(null);
|
const [initiatedPendingAction, setInitiatedPendingAction] = useState<'approve' | 'reject' | null>(null);
|
||||||
const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false);
|
|
||||||
|
|
||||||
const handlePendingApprove = useCallback(async () => {
|
const handlePendingApprove = useCallback(async () => {
|
||||||
const confirm = window.confirm(t('double_confirm'));
|
const confirm = window.confirm(t('double_confirm'));
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
@@ -250,23 +249,6 @@ const PostInfo = ({
|
|||||||
: openReplyModal && openReplyModal(cid, post?.number, postCid, threadNumber, subplebbitAddress);
|
: openReplyModal && openReplyModal(cid, post?.number, postCid, threadNumber, subplebbitAddress);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteFailedPost = () => {
|
|
||||||
if (isDeletingFailedPost || !canDeleteFailedPost) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDeletingFailedPost(true);
|
|
||||||
deleteComment(post?.cid || post.index)
|
|
||||||
.then(() => {
|
|
||||||
setIsDeletingFailedPost(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to delete failed post:', error);
|
|
||||||
alert(`Failed to delete post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
||||||
setIsDeletingFailedPost(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const threadRoute = cid ? (boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`) : undefined;
|
const threadRoute = cid ? (boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`) : undefined;
|
||||||
const threadTopNavigationState = !isReply ? getThreadTopNavigationState(cid) : undefined;
|
const threadTopNavigationState = !isReply ? getThreadTopNavigationState(cid) : undefined;
|
||||||
|
|
||||||
@@ -501,15 +483,6 @@ const PostInfo = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{canDeleteFailedPost && (
|
|
||||||
<span className={styles.failedPublishNotice}>
|
|
||||||
this post failed to publish, it's not visible to other users [{' '}
|
|
||||||
<button type='button' className={styles.failedDeletePostButton} disabled={isDeletingFailedPost} onClick={onDeleteFailedPost}>
|
|
||||||
Delete Post
|
|
||||||
</button>{' '}
|
|
||||||
]
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
{!(removed || deleted || purged) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
|
{!(removed || deleted || purged) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
|
||||||
{cid && parentCid && <ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />}
|
{cid && parentCid && <ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />}
|
||||||
@@ -746,6 +719,8 @@ const Reply = ({
|
|||||||
|
|
||||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||||
|
const { canDeleteFailedPost, isDeletingFailedPost, onDeleteFailedPost } = useDeleteFailedPost(post);
|
||||||
|
const failedPublishNotice = canDeleteFailedPost ? <FailedPublishNotice isDeleting={isDeletingFailedPost} onDelete={onDeleteFailedPost} /> : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.replyDesktop}>
|
<div className={styles.replyDesktop}>
|
||||||
@@ -777,7 +752,9 @@ const Reply = ({
|
|||||||
isInModView={isInModView}
|
isInModView={isInModView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!hidden && (!(removed || deleted || purged) || ((removed || deleted) && reason) || purged) && <CommentContent comment={post} />}
|
{!hidden && (!(removed || deleted || purged) || ((removed || deleted) && reason) || purged) && (
|
||||||
|
<CommentContent comment={post} prependContent={failedPublishNotice} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -888,6 +865,8 @@ const PostDesktop = ({
|
|||||||
|
|
||||||
const stateString = useStateString(post) || t('downloading_board');
|
const stateString = useStateString(post) || t('downloading_board');
|
||||||
const hasFailedState = state === 'failed';
|
const hasFailedState = state === 'failed';
|
||||||
|
const { canDeleteFailedPost, isDeletingFailedPost, onDeleteFailedPost } = useDeleteFailedPost(post);
|
||||||
|
const failedPublishNotice = canDeleteFailedPost ? <FailedPublishNotice isDeleting={isDeletingFailedPost} onDelete={onDeleteFailedPost} /> : undefined;
|
||||||
|
|
||||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||||
@@ -1027,7 +1006,7 @@ const PostDesktop = ({
|
|||||||
directRepliesByParentCid={directRepliesByParentCid}
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
/>
|
/>
|
||||||
{!isHidden && !content && !(deleted || removed || purged) && <div className={styles.spacer} />}
|
{!isHidden && !content && !(deleted || removed || purged) && <div className={styles.spacer} />}
|
||||||
{!isHidden && <CommentContent comment={post} />}
|
{!isHidden && <CommentContent comment={post} prependContent={failedPublishNotice} />}
|
||||||
</div>
|
</div>
|
||||||
{!isHidden && !isInPendingPostView && showReplies && repliesCount > 0 && !isInPostPageView && (
|
{!isHidden && !isInPendingPostView && showReplies && repliesCount > 0 && !isInPostPageView && (
|
||||||
<span className={styles.summary}>
|
<span className={styles.summary}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||||
import { Comment, deleteComment, useEditedComment, useReplies, useAccount, usePublishCommentModeration, useAccountComment } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment, useEditedComment, useReplies, useAccount, usePublishCommentModeration, useAccountComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import getShortAddress from '../../lib/get-short-address';
|
import getShortAddress from '../../lib/get-short-address';
|
||||||
import styles from '../../views/post/post.module.css';
|
import styles from '../../views/post/post.module.css';
|
||||||
import { shouldShowSnow } from '../../lib/snow';
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
@@ -25,6 +25,7 @@ import { useCurrentTime } from '../../hooks/use-current-time';
|
|||||||
import { useBoardPseudonymityMode } from '../../hooks/use-board-pseudonymity-mode';
|
import { useBoardPseudonymityMode } from '../../hooks/use-board-pseudonymity-mode';
|
||||||
import CommentContent from '../comment-content';
|
import CommentContent from '../comment-content';
|
||||||
import CommentMedia from '../comment-media';
|
import CommentMedia from '../comment-media';
|
||||||
|
import FailedPublishNotice from '../failed-publish-notice';
|
||||||
import LoadingEllipsis from '../loading-ellipsis';
|
import LoadingEllipsis from '../loading-ellipsis';
|
||||||
import PostMenuMobile from './post-menu-mobile';
|
import PostMenuMobile from './post-menu-mobile';
|
||||||
import ReplyQuotePreview from '../reply-quote-preview';
|
import ReplyQuotePreview from '../reply-quote-preview';
|
||||||
@@ -45,6 +46,7 @@ import { BOARD_REPLIES_PREVIEW_FETCH_SIZE, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT,
|
|||||||
import { filterRepliesForDisplay, getPreviewDisplayReplies } from '../../lib/utils/replies-preview-utils';
|
import { filterRepliesForDisplay, getPreviewDisplayReplies } from '../../lib/utils/replies-preview-utils';
|
||||||
import { getRenderableMobileBacklinks } from '../../lib/utils/reply-backlink-utils';
|
import { getRenderableMobileBacklinks } from '../../lib/utils/reply-backlink-utils';
|
||||||
import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
|
import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
|
||||||
|
import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -193,7 +195,6 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
const hasFailedState = state === 'failed';
|
const hasFailedState = state === 'failed';
|
||||||
const canDeleteFailedPost = hasFailedState && typeof post?.index === 'number';
|
|
||||||
const postMenuProps = selectPostMenuProps(post);
|
const postMenuProps = selectPostMenuProps(post);
|
||||||
|
|
||||||
const pseudonymityMode = useBoardPseudonymityMode(subplebbitAddress);
|
const pseudonymityMode = useBoardPseudonymityMode(subplebbitAddress);
|
||||||
@@ -229,24 +230,6 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
: openReplyModal && openReplyModal(cid, post?.number, postCid, threadNumber, subplebbitAddress);
|
: openReplyModal && openReplyModal(cid, post?.number, postCid, threadNumber, subplebbitAddress);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false);
|
|
||||||
const onDeleteFailedPost = () => {
|
|
||||||
if (isDeletingFailedPost || !canDeleteFailedPost) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDeletingFailedPost(true);
|
|
||||||
deleteComment(post?.cid || post.index)
|
|
||||||
.then(() => {
|
|
||||||
setIsDeletingFailedPost(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to delete failed post:', error);
|
|
||||||
alert(`Failed to delete post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
||||||
setIsDeletingFailedPost(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const threadRoute = cid ? (boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`) : undefined;
|
const threadRoute = cid ? (boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`) : undefined;
|
||||||
const threadTopNavigationState = !isReply ? getThreadTopNavigationState(cid) : undefined;
|
const threadTopNavigationState = !isReply ? getThreadTopNavigationState(cid) : undefined;
|
||||||
|
|
||||||
@@ -431,15 +414,6 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{canDeleteFailedPost && (
|
|
||||||
<span className={styles.failedPublishNotice}>
|
|
||||||
this post failed to publish, it's not visible to other users [{' '}
|
|
||||||
<button type='button' className={styles.failedDeletePostButton} disabled={isDeletingFailedPost} onClick={onDeleteFailedPost}>
|
|
||||||
Delete Post
|
|
||||||
</button>{' '}
|
|
||||||
]
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{(hasThumbnail || link) && !(deleted || removed || purged) && <PostMediaContent key={cid} post={post} link={link} />}
|
{(hasThumbnail || link) && !(deleted || removed || purged) && <PostMediaContent key={cid} post={post} link={link} />}
|
||||||
@@ -525,6 +499,8 @@ const Reply = ({
|
|||||||
const route = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
const route = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
||||||
const isRouteLinkToReply = cid ? location.pathname.startsWith(route) : false;
|
const isRouteLinkToReply = cid ? location.pathname.startsWith(route) : false;
|
||||||
const { hidden } = useHide({ cid });
|
const { hidden } = useHide({ cid });
|
||||||
|
const { canDeleteFailedPost, isDeletingFailedPost, onDeleteFailedPost } = useDeleteFailedPost(post);
|
||||||
|
const failedPublishNotice = canDeleteFailedPost ? <FailedPublishNotice isDeleting={isDeletingFailedPost} onDelete={onDeleteFailedPost} /> : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.replyMobile}>
|
<div className={styles.replyMobile}>
|
||||||
@@ -536,7 +512,9 @@ const Reply = ({
|
|||||||
data-post-cid={postCid}
|
data-post-cid={postCid}
|
||||||
>
|
>
|
||||||
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
|
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
|
||||||
{!hidden && (!(removed || deleted || purged) || ((removed || deleted) && reason) || purged) && <CommentContent comment={post} />}
|
{!hidden && (!(removed || deleted || purged) || ((removed || deleted) && reason) || purged) && (
|
||||||
|
<CommentContent comment={post} prependContent={failedPublishNotice} />
|
||||||
|
)}
|
||||||
<ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />
|
<ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -606,6 +584,8 @@ const PostMobile = ({
|
|||||||
const stateString = useStateString(post) || t('loading_post');
|
const stateString = useStateString(post) || t('loading_post');
|
||||||
const hasFailedState = state === 'failed';
|
const hasFailedState = state === 'failed';
|
||||||
const isReply = !!parentCid;
|
const isReply = !!parentCid;
|
||||||
|
const { canDeleteFailedPost, isDeletingFailedPost, onDeleteFailedPost } = useDeleteFailedPost(post);
|
||||||
|
const failedPublishNotice = canDeleteFailedPost ? <FailedPublishNotice isDeleting={isDeletingFailedPost} onDelete={onDeleteFailedPost} /> : undefined;
|
||||||
|
|
||||||
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
|
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
|
||||||
const filteredReplies = filterRepliesForDisplay(freshRepliesForRender);
|
const filteredReplies = filterRepliesForDisplay(freshRepliesForRender);
|
||||||
@@ -708,7 +688,7 @@ const PostMobile = ({
|
|||||||
>
|
>
|
||||||
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||||
<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} prependContent={failedPublishNotice} />
|
||||||
<ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />
|
<ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />
|
||||||
</div>
|
</div>
|
||||||
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
|
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { deleteComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
|
|
||||||
|
const useDeleteFailedPost = (post?: { cid?: string; index?: number; state?: string }) => {
|
||||||
|
const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false);
|
||||||
|
|
||||||
|
const canDeleteFailedPost = post?.state === 'failed' && typeof post?.index === 'number';
|
||||||
|
|
||||||
|
const onDeleteFailedPost = useCallback(() => {
|
||||||
|
if (isDeletingFailedPost || !canDeleteFailedPost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetComment = post?.cid ?? post?.index;
|
||||||
|
if (typeof targetComment === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeletingFailedPost(true);
|
||||||
|
deleteComment(targetComment)
|
||||||
|
.then(() => {
|
||||||
|
setIsDeletingFailedPost(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to delete failed post:', error);
|
||||||
|
alert(`Failed to delete post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
setIsDeletingFailedPost(false);
|
||||||
|
});
|
||||||
|
}, [canDeleteFailedPost, isDeletingFailedPost, post?.cid, post?.index]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canDeleteFailedPost,
|
||||||
|
isDeletingFailedPost,
|
||||||
|
onDeleteFailedPost,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDeleteFailedPost;
|
||||||
@@ -595,28 +595,31 @@
|
|||||||
|
|
||||||
.failedPublishNotice {
|
.failedPublishNotice {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
|
||||||
color: red;
|
color: red;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.failedDeletePostAction {
|
||||||
|
color: var(--button-desktop-text-color);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.failedDeletePostButton {
|
.failedDeletePostButton {
|
||||||
all: unset;
|
all: unset;
|
||||||
color: red;
|
font: inherit;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: var(--button-desktop-text-color);
|
||||||
|
text-decoration: var(--button-text-decoration);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 700;
|
|
||||||
text-decoration: underline;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.failedDeletePostButton:hover:not(:disabled) {
|
.failedDeletePostButton:hover:not(:disabled) {
|
||||||
color: #b30000;
|
color: var(--button-desktop-text-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.failedDeletePostButton:disabled {
|
.failedDeletePostButton:disabled {
|
||||||
cursor: wait;
|
opacity: 0.5;
|
||||||
opacity: 0.7;
|
cursor: not-allowed;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grayEditMessage {
|
.grayEditMessage {
|
||||||
|
|||||||
Reference in New Issue
Block a user