From e95dcb8af572e5af0d2a9fdcf96d9841479f3c4a Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sat, 23 May 2026 17:24:22 +0700 Subject: [PATCH] fix(pending-post): return to board after abandoned challenge --- .../post-form/__tests__/post-form.test.tsx | 2 +- src/components/post-form/post-form.tsx | 5 +- .../mod-queue/__tests__/mod-queue.test.tsx | 7 +++ .../__tests__/pending-post.test.tsx | 30 ++++++++++ src/views/pending-post/pending-post.tsx | 56 ++++++++++++++++--- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/components/post-form/__tests__/post-form.test.tsx b/src/components/post-form/__tests__/post-form.test.tsx index e1399981..3057185e 100644 --- a/src/components/post-form/__tests__/post-form.test.tsx +++ b/src/components/post-form/__tests__/post-form.test.tsx @@ -904,7 +904,7 @@ describe('PostForm', () => { await flushEffects(); expect(testState.resetPublishPostOptionsMock).toHaveBeenCalledTimes(1); - expect(testState.navigateMock).toHaveBeenCalledWith('/pending/7'); + expect(testState.navigateMock).toHaveBeenCalledWith('/pending/7', { state: { boardPath: 'mu' } }); }); it('resets the reply form after a completed reply publish', async () => { diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index b444b796..26fe5147 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -428,6 +428,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: const subscriptions = account?.subscriptions || []; const directories = useDirectories(); const directoryEntry = useDirectoryByAddress(effectiveBoardAddress); + const pendingPostBoardPath = effectiveBoardAddress ? getBoardPath(effectiveBoardAddress, directories) : undefined; const rulesPath = effectiveBoardAddress ? `/rules/${getBoardPath(effectiveBoardAddress, directories)}` : '/rules'; const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true; const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true; @@ -538,9 +539,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: if (typeof postIndex === 'number') { resetPublishPostOptions(); resetFields(); - navigate(`/pending/${postIndex}`); + navigate(`/pending/${postIndex}`, pendingPostBoardPath ? { state: { boardPath: pendingPostBoardPath } } : undefined); } - }, [postIndex, resetPublishPostOptions, navigate]); + }, [postIndex, pendingPostBoardPath, resetPublishPostOptions, navigate]); // in post page, publish a reply to the post const isInPostView = isPostPageView(location.pathname, params); diff --git a/src/views/mod-queue/__tests__/mod-queue.test.tsx b/src/views/mod-queue/__tests__/mod-queue.test.tsx index ccb56387..2c608e42 100644 --- a/src/views/mod-queue/__tests__/mod-queue.test.tsx +++ b/src/views/mod-queue/__tests__/mod-queue.test.tsx @@ -65,6 +65,13 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ }, state: 'succeeded', }), + useCommunities: ({ communities }: { communities?: Array<{ name: string }> } = {}) => ({ + communities: (communities ?? []).map(() => ({ + roles: { + '0x123': { role: 'moderator' }, + }, + })), + }), useEditedComment: ({ comment }: { comment?: TestComment }) => ({ editedComment: comment, failedEdits: {}, diff --git a/src/views/pending-post/__tests__/pending-post.test.tsx b/src/views/pending-post/__tests__/pending-post.test.tsx index 9751284d..61060473 100644 --- a/src/views/pending-post/__tests__/pending-post.test.tsx +++ b/src/views/pending-post/__tests__/pending-post.test.tsx @@ -16,8 +16,10 @@ type TestComment = { const testState = vi.hoisted(() => ({ accountCommentIndex: undefined as string | undefined, accountComments: [] as TestComment[], + challengeCount: 0, directories: [] as Array<{ address: string; title?: string }>, getBoardPathMock: vi.fn<(address: string) => string>(), + locationState: null as { boardPath?: string } | null, navigateMock: vi.fn(), post: undefined as TestComment | undefined, })); @@ -26,6 +28,9 @@ vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, + useLocation: () => ({ + state: testState.locationState, + }), useNavigate: () => testState.navigateMock, useParams: () => ({ accountCommentIndex: testState.accountCommentIndex, @@ -49,6 +54,10 @@ vi.mock('../../../lib/utils/route-utils', () => ({ getBoardPath: (address: string) => testState.getBoardPathMock(address), })); +vi.mock('../../../stores/use-challenges-store', () => ({ + default: (selector: (state: { challenges: unknown[] }) => unknown) => selector({ challenges: Array.from({ length: testState.challengeCount }) }), +})); + vi.mock('../../post', () => ({ Post: ({ post }: { post?: TestComment }) => createElement('div', { 'data-testid': 'post-view' }, post?.cid ?? 'no-post'), })); @@ -78,8 +87,10 @@ describe('PendingPost', () => { vi.clearAllMocks(); testState.accountCommentIndex = undefined; testState.accountComments = []; + testState.challengeCount = 0; testState.directories = []; testState.getBoardPathMock.mockReset(); + testState.locationState = null; testState.navigateMock.mockReset(); testState.post = undefined; @@ -151,6 +162,25 @@ describe('PendingPost', () => { expect(testState.navigateMock).not.toHaveBeenCalledWith('/not-found', { replace: true }); }); + it('redirects abandoned pending posts back to their board after the challenge closes', async () => { + testState.accountCommentIndex = '0'; + testState.accountComments = []; + testState.challengeCount = 1; + testState.locationState = { boardPath: 'mu' }; + testState.post = { index: 0 }; + + await renderPendingPost(); + + expect(testState.navigateMock).not.toHaveBeenCalled(); + + testState.challengeCount = 0; + testState.navigateMock.mockClear(); + + await renderPendingPost(); + + expect(testState.navigateMock).toHaveBeenCalledWith('/mu', { replace: true }); + }); + it('redirects missing sparse pending account comment indices to not found', async () => { testState.accountCommentIndex = '0'; testState.accountComments = [{ index: 1 }]; diff --git a/src/views/pending-post/pending-post.tsx b/src/views/pending-post/pending-post.tsx index d433ccf6..5826e18e 100644 --- a/src/views/pending-post/pending-post.tsx +++ b/src/views/pending-post/pending-post.tsx @@ -1,10 +1,11 @@ -import { useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useEffect, useRef } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useAccountComments } from '@bitsocial/bitsocial-react-hooks'; import { useDirectories } from '../../hooks/use-directories'; import useSafeAccountComment from '../../hooks/use-safe-account-comment'; import { getCommentCommunityAddress } from '../../lib/utils/comment-utils'; import { getBoardPath } from '../../lib/utils/route-utils'; +import useChallengesStore from '../../stores/use-challenges-store'; import { Post } from '../post'; type PendingAccountComment = { @@ -31,17 +32,40 @@ const hasPendingAccountCommentIndex = (accountComments: PendingAccountComment[] return hasExplicitIndices ? false : accountCommentIndex < accountComments.length; }; +const getPendingRouteBoardPath = (state: unknown): string | undefined => { + if (!state || typeof state !== 'object') { + return undefined; + } + + const boardPath = (state as { boardPath?: unknown }).boardPath; + return typeof boardPath === 'string' && boardPath ? boardPath : undefined; +}; + const PendingPost = () => { const { accountComments } = useAccountComments(); const { accountCommentIndex } = useParams<{ accountCommentIndex?: string }>(); + const location = useLocation(); const normalizedAccountCommentIndex = accountCommentIndex === undefined ? undefined : Number(accountCommentIndex); const hasNormalizedAccountCommentIndex = normalizedAccountCommentIndex !== undefined && !Number.isNaN(normalizedAccountCommentIndex); const post = useSafeAccountComment({ commentIndex: accountCommentIndex }); + const postCommunityAddress = getCommentCommunityAddress(post); + const hasAddressablePost = Boolean(post?.cid || postCommunityAddress); const navigate = useNavigate(); const directories = useDirectories(); + const routeBoardPath = getPendingRouteBoardPath(location.state); + const postBoardPath = postCommunityAddress ? getBoardPath(postCommunityAddress, directories) : undefined; + const pendingBoardPath = postBoardPath || routeBoardPath; + const hasActiveChallenge = useChallengesStore((state) => state.challenges.length > 0); + const lastPendingBoardRef = useRef<{ accountCommentIndex: number; boardPath: string } | null>(null); useEffect(() => window.scrollTo(0, 0), []); + useEffect(() => { + if (typeof normalizedAccountCommentIndex === 'number' && pendingBoardPath) { + lastPendingBoardRef.current = { accountCommentIndex: normalizedAccountCommentIndex, boardPath: pendingBoardPath }; + } + }, [normalizedAccountCommentIndex, pendingBoardPath]); + const isValidAccountCommentIndex = !accountCommentIndex || (hasNormalizedAccountCommentIndex && @@ -51,17 +75,31 @@ const PendingPost = () => { useEffect(() => { if (!isValidAccountCommentIndex) { - navigate('/not-found', { replace: true }); + const lastPendingBoard = lastPendingBoardRef.current; + const abandonedBoardPath = + !hasActiveChallenge && lastPendingBoard && lastPendingBoard.accountCommentIndex === normalizedAccountCommentIndex ? lastPendingBoard.boardPath : undefined; + navigate(abandonedBoardPath ? `/${abandonedBoardPath}` : '/not-found', { replace: true }); } - }, [isValidAccountCommentIndex, navigate]); + }, [hasActiveChallenge, isValidAccountCommentIndex, navigate, normalizedAccountCommentIndex]); useEffect(() => { - const postCommunityAddress = getCommentCommunityAddress(post); - if (post?.cid && postCommunityAddress) { - const boardPath = getBoardPath(postCommunityAddress, directories); - navigate(`/${boardPath}/thread/${post.cid}`, { replace: true }); + if (post?.cid && postBoardPath) { + navigate(`/${postBoardPath}/thread/${post.cid}`, { replace: true }); } - }, [post, navigate, directories]); + }, [post?.cid, postBoardPath, navigate]); + + useEffect(() => { + if (hasAddressablePost || !isValidAccountCommentIndex) { + return; + } + + const lastPendingBoard = lastPendingBoardRef.current; + const abandonedBoardPath = + !hasActiveChallenge && lastPendingBoard && lastPendingBoard.accountCommentIndex === normalizedAccountCommentIndex ? lastPendingBoard.boardPath : undefined; + if (abandonedBoardPath) { + navigate(`/${abandonedBoardPath}`, { replace: true }); + } + }, [hasActiveChallenge, hasAddressablePost, isValidAccountCommentIndex, navigate, normalizedAccountCommentIndex]); return ; };