Merge branch 'codex/fix/pending-escape-board-redirect'

# Conflicts:
#	src/components/post-form/post-form.tsx
This commit is contained in:
Tommaso Casaburi
2026-05-23 17:26:47 +07:00
5 changed files with 88 additions and 12 deletions
@@ -906,7 +906,7 @@ describe('PostForm', () => {
await flushEffects(); await flushEffects();
expect(testState.resetPublishPostOptionsMock).toHaveBeenCalledTimes(1); expect(testState.resetPublishPostOptionsMock).toHaveBeenCalledTimes(1);
expect(testState.navigateMock).toHaveBeenCalledWith('/pending/7'); expect(testState.navigateMock).toHaveBeenCalledWith('/pending/7', { state: { boardPath: 'mu' } });
}); });
it('redirects new posts to the board index when nonoko is used', async () => { it('redirects new posts to the board index when nonoko is used', async () => {
+3 -2
View File
@@ -431,6 +431,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const subscriptions = account?.subscriptions || []; const subscriptions = account?.subscriptions || [];
const directories = useDirectories(); const directories = useDirectories();
const directoryEntry = useDirectoryByAddress(effectiveBoardAddress); const directoryEntry = useDirectoryByAddress(effectiveBoardAddress);
const pendingPostBoardPath = effectiveBoardAddress ? getBoardPath(effectiveBoardAddress, directories) : undefined;
const rulesPath = effectiveBoardAddress ? `/rules/${getBoardPath(effectiveBoardAddress, directories)}` : '/rules'; const rulesPath = effectiveBoardAddress ? `/rules/${getBoardPath(effectiveBoardAddress, directories)}` : '/rules';
const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true; const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true;
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true; const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
@@ -556,10 +557,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
if (nonokoRedirectPath) { if (nonokoRedirectPath) {
navigate(nonokoRedirectPath, { state: getNonokoPendingRouteState(postIndex) }); navigate(nonokoRedirectPath, { state: getNonokoPendingRouteState(postIndex) });
} else { } else {
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 // in post page, publish a reply to the post
const isInPostView = isPostPageView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params);
@@ -65,6 +65,13 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
}, },
state: 'succeeded', state: 'succeeded',
}), }),
useCommunities: ({ communities }: { communities?: Array<{ name: string }> } = {}) => ({
communities: (communities ?? []).map(() => ({
roles: {
'0x123': { role: 'moderator' },
},
})),
}),
useEditedComment: ({ comment }: { comment?: TestComment }) => ({ useEditedComment: ({ comment }: { comment?: TestComment }) => ({
editedComment: comment, editedComment: comment,
failedEdits: {}, failedEdits: {},
@@ -16,8 +16,10 @@ type TestComment = {
const testState = vi.hoisted(() => ({ const testState = vi.hoisted(() => ({
accountCommentIndex: undefined as string | undefined, accountCommentIndex: undefined as string | undefined,
accountComments: [] as TestComment[], accountComments: [] as TestComment[],
challengeCount: 0,
directories: [] as Array<{ address: string; title?: string }>, directories: [] as Array<{ address: string; title?: string }>,
getBoardPathMock: vi.fn<(address: string) => string>(), getBoardPathMock: vi.fn<(address: string) => string>(),
locationState: null as { boardPath?: string } | null,
navigateMock: vi.fn(), navigateMock: vi.fn(),
post: undefined as TestComment | undefined, 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'); const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
return { return {
...actual, ...actual,
useLocation: () => ({
state: testState.locationState,
}),
useNavigate: () => testState.navigateMock, useNavigate: () => testState.navigateMock,
useParams: () => ({ useParams: () => ({
accountCommentIndex: testState.accountCommentIndex, accountCommentIndex: testState.accountCommentIndex,
@@ -49,6 +54,10 @@ vi.mock('../../../lib/utils/route-utils', () => ({
getBoardPath: (address: string) => testState.getBoardPathMock(address), 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', () => ({ vi.mock('../../post', () => ({
Post: ({ post }: { post?: TestComment }) => createElement('div', { 'data-testid': 'post-view' }, post?.cid ?? 'no-post'), Post: ({ post }: { post?: TestComment }) => createElement('div', { 'data-testid': 'post-view' }, post?.cid ?? 'no-post'),
})); }));
@@ -78,8 +87,10 @@ describe('PendingPost', () => {
vi.clearAllMocks(); vi.clearAllMocks();
testState.accountCommentIndex = undefined; testState.accountCommentIndex = undefined;
testState.accountComments = []; testState.accountComments = [];
testState.challengeCount = 0;
testState.directories = []; testState.directories = [];
testState.getBoardPathMock.mockReset(); testState.getBoardPathMock.mockReset();
testState.locationState = null;
testState.navigateMock.mockReset(); testState.navigateMock.mockReset();
testState.post = undefined; testState.post = undefined;
@@ -151,6 +162,25 @@ describe('PendingPost', () => {
expect(testState.navigateMock).not.toHaveBeenCalledWith('/not-found', { replace: true }); 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 () => { it('redirects missing sparse pending account comment indices to not found', async () => {
testState.accountCommentIndex = '0'; testState.accountCommentIndex = '0';
testState.accountComments = [{ index: 1 }]; testState.accountComments = [{ index: 1 }];
+47 -9
View File
@@ -1,10 +1,11 @@
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComments } from '@bitsocial/bitsocial-react-hooks'; import { useAccountComments } from '@bitsocial/bitsocial-react-hooks';
import { useDirectories } from '../../hooks/use-directories'; import { useDirectories } from '../../hooks/use-directories';
import useSafeAccountComment from '../../hooks/use-safe-account-comment'; import useSafeAccountComment from '../../hooks/use-safe-account-comment';
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils'; import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { getBoardPath } from '../../lib/utils/route-utils'; import { getBoardPath } from '../../lib/utils/route-utils';
import useChallengesStore from '../../stores/use-challenges-store';
import { Post } from '../post'; import { Post } from '../post';
type PendingAccountComment = { type PendingAccountComment = {
@@ -31,17 +32,40 @@ const hasPendingAccountCommentIndex = (accountComments: PendingAccountComment[]
return hasExplicitIndices ? false : accountCommentIndex < accountComments.length; 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 PendingPost = () => {
const { accountComments } = useAccountComments(); const { accountComments } = useAccountComments();
const { accountCommentIndex } = useParams<{ accountCommentIndex?: string }>(); const { accountCommentIndex } = useParams<{ accountCommentIndex?: string }>();
const location = useLocation();
const normalizedAccountCommentIndex = accountCommentIndex === undefined ? undefined : Number(accountCommentIndex); const normalizedAccountCommentIndex = accountCommentIndex === undefined ? undefined : Number(accountCommentIndex);
const hasNormalizedAccountCommentIndex = normalizedAccountCommentIndex !== undefined && !Number.isNaN(normalizedAccountCommentIndex); const hasNormalizedAccountCommentIndex = normalizedAccountCommentIndex !== undefined && !Number.isNaN(normalizedAccountCommentIndex);
const post = useSafeAccountComment({ commentIndex: accountCommentIndex }); const post = useSafeAccountComment({ commentIndex: accountCommentIndex });
const postCommunityAddress = getCommentCommunityAddress(post);
const hasAddressablePost = Boolean(post?.cid || postCommunityAddress);
const navigate = useNavigate(); const navigate = useNavigate();
const directories = useDirectories(); 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(() => window.scrollTo(0, 0), []);
useEffect(() => {
if (typeof normalizedAccountCommentIndex === 'number' && pendingBoardPath) {
lastPendingBoardRef.current = { accountCommentIndex: normalizedAccountCommentIndex, boardPath: pendingBoardPath };
}
}, [normalizedAccountCommentIndex, pendingBoardPath]);
const isValidAccountCommentIndex = const isValidAccountCommentIndex =
!accountCommentIndex || !accountCommentIndex ||
(hasNormalizedAccountCommentIndex && (hasNormalizedAccountCommentIndex &&
@@ -51,17 +75,31 @@ const PendingPost = () => {
useEffect(() => { useEffect(() => {
if (!isValidAccountCommentIndex) { 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(() => { useEffect(() => {
const postCommunityAddress = getCommentCommunityAddress(post); if (post?.cid && postBoardPath) {
if (post?.cid && postCommunityAddress) { navigate(`/${postBoardPath}/thread/${post.cid}`, { replace: true });
const boardPath = getBoardPath(postCommunityAddress, directories);
navigate(`/${boardPath}/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} />; return <Post post={post} />;
}; };