fix(pending-post): return to board after abandoned challenge

This commit is contained in:
Tommaso Casaburi
2026-05-23 17:24:22 +07:00
parent f4965ee678
commit e95dcb8af5
5 changed files with 88 additions and 12 deletions
@@ -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: {},
@@ -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<typeof import('react-router-dom')>('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 }];
+47 -9
View File
@@ -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 <Post post={post} />;
};