diff --git a/src/components/comment-content/__tests__/comment-content.test.tsx b/src/components/comment-content/__tests__/comment-content.test.tsx index 67723a62..be631c63 100644 --- a/src/components/comment-content/__tests__/comment-content.test.tsx +++ b/src/components/comment-content/__tests__/comment-content.test.tsx @@ -397,7 +397,8 @@ describe('CommentContent', () => { postCid: 'post-2', state: 'failed', }); - expect(container.textContent).toContain('Failed to publish'); + expect(container.textContent).not.toContain('Failed to publish'); + expect(container.textContent).toContain('still pending'); expect(container.querySelector('[data-testid="loading-ellipsis"]')).toBeNull(); testState.stateString = 'Publishing'; diff --git a/src/components/comment-content/comment-content.tsx b/src/components/comment-content/comment-content.tsx index ee7e9cf4..a34dbe57 100644 --- a/src/components/comment-content/comment-content.tsx +++ b/src/components/comment-content/comment-content.tsx @@ -131,6 +131,7 @@ const CommentContent = ({ comment: post, prependContent }: { comment: Comment; p const stateString = useStateString(resolvedPost); const hasFailedState = state === 'failed'; const failedError = getFailedCommentError(resolvedPost); + const shouldShowUnpublishedStateDetails = !cid && (!hasFailedState || Boolean(failedError)); const loadingString = (
@@ -298,7 +299,7 @@ const CommentContent = ({ comment: post, prependContent }: { comment: Comment; p )} - {!cid && ( + {shouldShowUnpublishedStateDetails && ( <>

diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index 7e381981..02ab961b 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -384,7 +384,7 @@ const PostInfo = ({ ) : purged ? ( t('purged') ) : !cid && pseudonymityMode ? ( - {hasFailedState ? capitalize(t('failed')) : capitalize(t('pending'))} + {hasFailedState ? '?' : capitalize(t('pending'))} ) : ( No. - {hasFailedState ? capitalize(t('failed')) : capitalize(t('pending'))} + {hasFailedState ? '?' : capitalize(t('pending'))} )} {pinned && ( @@ -855,6 +855,7 @@ const PostDesktop = ({ const isMultiboardView = isInAllView || isInSubscriptionsView || isInModView; const directories = useDirectories(); const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined; + const deleteFailedPostRedirectPath = isInPendingPostView ? (boardPath ? `/${boardPath}` : '/') : undefined; const displayBoardPath = boardPath && communityAddress ? boardPath !== communityAddress @@ -957,8 +958,10 @@ const PostDesktop = ({ const stateString = useStateString(resolvedPost) || t('downloading_board'); const hasFailedState = state === 'failed'; - const { canDeleteFailedPost, canRetryFailedPost, isDeletingFailedPost, isRetryingFailedPost, onDeleteFailedPost, onRetryFailedPost } = - useDeleteFailedPost(resolvedPost); + const { canDeleteFailedPost, canRetryFailedPost, isDeletingFailedPost, isRetryingFailedPost, onDeleteFailedPost, onRetryFailedPost } = useDeleteFailedPost( + resolvedPost, + deleteFailedPostRedirectPath, + ); const failedPublishNotice = canDeleteFailedPost ? ( {hasFailedState ? capitalize(t('failed')) : capitalize(t('pending'))} + {hasFailedState ? '?' : capitalize(t('pending'))} ) : ( No. - {hasFailedState ? capitalize(t('failed')) : capitalize(t('pending'))} + {hasFailedState ? '?' : capitalize(t('pending'))} )} {shouldShowPendingApprovalButtons && ( @@ -595,6 +595,7 @@ const PostMobile = ({ ) : null; const linksCount = useCountLinksInReplies(resolvedPost); + const deleteFailedPostRedirectPath = isInPendingPostView ? (boardPath ? `/${boardPath}` : '/') : undefined; const hasReplyPaginationOverride = !!replyPaginationOverride; const shouldFetchReplies = showReplies && !isModQueue && !hasReplyPaginationOverride; const shouldUsePreview = shouldFetchReplies && !showAllReplies; @@ -668,8 +669,10 @@ const PostMobile = ({ const stateString = useStateString(resolvedPost) || t('loading_post'); const hasFailedState = state === 'failed'; const isReply = !!parentCid; - const { canDeleteFailedPost, canRetryFailedPost, isDeletingFailedPost, isRetryingFailedPost, onDeleteFailedPost, onRetryFailedPost } = - useDeleteFailedPost(resolvedPost); + const { canDeleteFailedPost, canRetryFailedPost, isDeletingFailedPost, isRetryingFailedPost, onDeleteFailedPost, onRetryFailedPost } = useDeleteFailedPost( + resolvedPost, + deleteFailedPostRedirectPath, + ); const failedPublishNotice = canDeleteFailedPost ? ( ({ alertMock: vi.fn(), deleteCommentMock: vi.fn(async (_targetComment: string | number) => undefined), lastPublishOptions: undefined as Record | undefined, + navigateMock: vi.fn(), publishCommentMock: vi.fn(async () => undefined), })); +vi.mock('react-router-dom', () => ({ + useNavigate: () => testState.navigateMock, +})); + vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ deleteComment: (targetComment: string | number) => testState.deleteCommentMock(targetComment), usePublishComment: (options: Record | undefined) => { @@ -63,14 +68,14 @@ const failedPost = { title: 'Hello', }; -const HookHarness = ({ post }: { post: typeof failedPost }) => { - latestValue = useDeleteFailedPost(post); +const HookHarness = ({ deleteRedirectPath, post }: { deleteRedirectPath?: string; post: typeof failedPost }) => { + latestValue = useDeleteFailedPost(post, deleteRedirectPath); return null; }; -const renderHook = (post = failedPost) => { +const renderHook = (post = failedPost, deleteRedirectPath?: string) => { act(() => { - root.render(createElement(HookHarness, { post })); + root.render(createElement(HookHarness, { deleteRedirectPath, post })); }); }; @@ -137,6 +142,17 @@ describe('useDeleteFailedPost', () => { expect(testState.publishCommentMock).toHaveBeenCalledTimes(1); }); + it('redirects after deleting a failed post when a redirect path is provided', async () => { + renderHook(failedPost, '/mu'); + + await act(async () => { + await latestValue.onDeleteFailedPost(); + }); + + expect(testState.deleteCommentMock).toHaveBeenCalledWith('failed-cid'); + expect(testState.navigateMock).toHaveBeenCalledWith('/mu', { replace: true }); + }); + it('routes retry challenges through the challenge store and preserves abandon behavior', async () => { await act(async () => { await testState.lastPublishOptions?.onChallenge('captcha', 'nonce'); diff --git a/src/hooks/use-delete-failed-post.ts b/src/hooks/use-delete-failed-post.ts index 6ba62ff2..80b73d86 100644 --- a/src/hooks/use-delete-failed-post.ts +++ b/src/hooks/use-delete-failed-post.ts @@ -1,4 +1,5 @@ import { useCallback, useMemo, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { ChallengeVerification, Comment, PublishCommentOptions, deleteComment, usePublishComment } from '@bitsocial/bitsocial-react-hooks'; import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils'; import useChallengesStore from '../stores/use-challenges-store'; @@ -67,10 +68,11 @@ export const getFailedPostRetryPublishOptions = (post?: FailedPost): PublishComm return retryOptions; }; -const useDeleteFailedPost = (post?: FailedPost) => { +const useDeleteFailedPost = (post?: FailedPost, deleteRedirectPath?: string) => { const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false); const [isRetryingFailedPost, setIsRetryingFailedPost] = useState(false); const addChallenge = useChallengesStore((state) => state.addChallenge); + const navigate = useNavigate(); const abandonPublishRef = useRef<(() => Promise) | undefined>(); const abandonCurrentPublish = useCallback(async () => { await abandonPublishRef.current?.(); @@ -114,13 +116,16 @@ const useDeleteFailedPost = (post?: FailedPost) => { deleteComment(targetComment) .then(() => { setIsDeletingFailedPost(false); + if (deleteRedirectPath) { + navigate(deleteRedirectPath, { replace: true }); + } }) .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, isRetryingFailedPost, post]); + }, [canDeleteFailedPost, deleteRedirectPath, isDeletingFailedPost, isRetryingFailedPost, navigate, post]); const canRetryFailedPost = canDeleteFailedPost && Boolean(retryPublishOptions);