mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(replies): keep fresh replies scoped to thread (#1174)
This commit is contained in:
@@ -939,7 +939,7 @@ const PostDesktop = ({
|
|||||||
? fullReplies
|
? fullReplies
|
||||||
: previewReplies
|
: previewReplies
|
||||||
: getPreviewDisplayReplies(previewReplies, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT);
|
: getPreviewDisplayReplies(previewReplies, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT);
|
||||||
const freshRepliesForRender = useFreshReplies(repliesForRender);
|
const freshRepliesForRender = useFreshReplies(repliesForRender, { post: resolvedPost });
|
||||||
useRegisterFreshReplies(resolvedPost, freshRepliesForRender);
|
useRegisterFreshReplies(resolvedPost, freshRepliesForRender);
|
||||||
const setResetFunction = useFeedResetStore((s) => s.setResetFunction);
|
const setResetFunction = useFeedResetStore((s) => s.setResetFunction);
|
||||||
const repliesResetRequestId = useThreadLiveUpdatesStore((state) => state.repliesResetRequestId);
|
const repliesResetRequestId = useThreadLiveUpdatesStore((state) => state.repliesResetRequestId);
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ const PostMobile = ({
|
|||||||
const { replies, hasMore, loadMore } = repliesResult;
|
const { replies, hasMore, loadMore } = repliesResult;
|
||||||
const updatedReplies = repliesResult.updatedReplies;
|
const updatedReplies = repliesResult.updatedReplies;
|
||||||
const repliesForRender = updatedReplies?.length ? updatedReplies : replies || [];
|
const repliesForRender = updatedReplies?.length ? updatedReplies : replies || [];
|
||||||
const freshRepliesForRender = useFreshReplies(repliesForRender);
|
const freshRepliesForRender = useFreshReplies(repliesForRender, { post: resolvedPost });
|
||||||
useRegisterFreshReplies(resolvedPost, freshRepliesForRender);
|
useRegisterFreshReplies(resolvedPost, freshRepliesForRender);
|
||||||
const reset = (repliesResult as { reset?: () => Promise<void> }).reset;
|
const reset = (repliesResult as { reset?: () => Promise<void> }).reset;
|
||||||
const setResetFunction = useFeedResetStore((s) => s.setResetFunction);
|
const setResetFunction = useFeedResetStore((s) => s.setResetFunction);
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ type TestComment = {
|
|||||||
content?: string;
|
content?: string;
|
||||||
index?: number;
|
index?: number;
|
||||||
number?: number;
|
number?: number;
|
||||||
|
parentCid?: string;
|
||||||
pendingApproval?: boolean;
|
pendingApproval?: boolean;
|
||||||
|
postCid?: string;
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const testState = vi.hoisted(() => ({
|
const testState = vi.hoisted(() => ({
|
||||||
accountComments: [] as TestComment[],
|
accountComments: [] as TestComment[],
|
||||||
accountCommentsCalls: [] as Array<{ commentIndices?: number[]; filter?: (comment: TestComment) => boolean } | undefined>,
|
accountCommentsCalls: [] as Array<{ commentIndices?: number[]; filter?: (comment: TestComment) => boolean } | undefined>,
|
||||||
|
post: undefined as TestComment | undefined,
|
||||||
replies: [] as TestComment[],
|
replies: [] as TestComment[],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -46,7 +49,7 @@ let latestValue: ReturnType<typeof useFreshReplies>;
|
|||||||
let root: Root;
|
let root: Root;
|
||||||
|
|
||||||
const HookHarness = () => {
|
const HookHarness = () => {
|
||||||
latestValue = useFreshReplies(testState.replies as never);
|
latestValue = useFreshReplies(testState.replies as never, { post: testState.post as never });
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +63,7 @@ describe('useFreshReplies', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testState.accountComments = [];
|
testState.accountComments = [];
|
||||||
testState.accountCommentsCalls = [];
|
testState.accountCommentsCalls = [];
|
||||||
|
testState.post = undefined;
|
||||||
testState.replies = [];
|
testState.replies = [];
|
||||||
|
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
@@ -237,4 +241,68 @@ describe('useFreshReplies', () => {
|
|||||||
|
|
||||||
expect(latestValue.map((reply) => reply.cid)).toEqual(['reply-8', 'reply-11', 'reply-12']);
|
expect(latestValue.map((reply) => reply.cid)).toEqual(['reply-8', 'reply-11', 'reply-12']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not replace a failed reply placeholder with a same-index post from another thread', () => {
|
||||||
|
testState.post = {
|
||||||
|
cid: 'g-thread-cid',
|
||||||
|
communityAddress: 'technology.eth',
|
||||||
|
};
|
||||||
|
testState.replies = [
|
||||||
|
{
|
||||||
|
content: 'failed g reply',
|
||||||
|
index: 4,
|
||||||
|
parentCid: 'g-thread-cid',
|
||||||
|
postCid: 'g-thread-cid',
|
||||||
|
communityAddress: 'technology.eth',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testState.accountComments = [
|
||||||
|
{
|
||||||
|
cid: 'mu-thread-cid',
|
||||||
|
content: 'mu op',
|
||||||
|
index: 4,
|
||||||
|
postCid: 'mu-thread-cid',
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
renderHook();
|
||||||
|
|
||||||
|
expect(latestValue).toHaveLength(1);
|
||||||
|
expect(latestValue[0]).toBe(testState.replies[0] as never);
|
||||||
|
expect(latestValue[0]?.content).toBe('failed g reply');
|
||||||
|
expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [4] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces indexed replies when the account comment still belongs to the same thread', () => {
|
||||||
|
testState.post = {
|
||||||
|
cid: 'thread-cid',
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
};
|
||||||
|
testState.replies = [
|
||||||
|
{
|
||||||
|
content: 'pending reply',
|
||||||
|
index: 5,
|
||||||
|
parentCid: 'thread-cid',
|
||||||
|
postCid: 'thread-cid',
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testState.accountComments = [
|
||||||
|
{
|
||||||
|
cid: 'reply-cid',
|
||||||
|
content: 'fresh reply',
|
||||||
|
index: 5,
|
||||||
|
number: 9,
|
||||||
|
parentCid: 'thread-cid',
|
||||||
|
postCid: 'thread-cid',
|
||||||
|
communityAddress: 'music.bso',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
renderHook();
|
||||||
|
|
||||||
|
expect(latestValue[0]).toBe(testState.accountComments[0] as never);
|
||||||
|
expect(latestValue[0]?.number).toBe(9);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,55 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Comment, useAccountComments } from '@bitsocial/bitsocial-react-hooks';
|
import { Comment, useAccountComments } from '@bitsocial/bitsocial-react-hooks';
|
||||||
import { sortRepliesForDisplay } from '../lib/utils/replies-preview-utils';
|
import { sortRepliesForDisplay } from '../lib/utils/replies-preview-utils';
|
||||||
|
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
||||||
|
import { normalizeBoardAddress } from '../lib/utils/directory-list-lookup-utils';
|
||||||
|
|
||||||
// Keep the hook on its indexed fast path when there are no reply indices to resolve.
|
// Keep the hook on its indexed fast path when there are no reply indices to resolve.
|
||||||
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
||||||
|
|
||||||
const useFreshReplies = (replies: Comment[] = []) => {
|
type UseFreshRepliesOptions = {
|
||||||
|
post?: Comment;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getString = (value: unknown): string | undefined => (typeof value === 'string' && value.length > 0 ? value : undefined);
|
||||||
|
|
||||||
|
const getThreadPostCid = (post: Comment | undefined): string | undefined => getString(post?.postCid) ?? getString(post?.cid);
|
||||||
|
|
||||||
|
const hasSameCommunityAddress = (a: string | undefined, b: string | undefined): boolean => {
|
||||||
|
if (!a || !b) return a === b;
|
||||||
|
return normalizeBoardAddress(a) === normalizeBoardAddress(b);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFreshReplyForOriginalReply = (reply: Comment, freshReply: Comment, post: Comment | undefined): boolean => {
|
||||||
|
const replyCid = getString(reply?.cid);
|
||||||
|
const freshReplyCid = getString(freshReply?.cid);
|
||||||
|
if (replyCid && reply?.pendingApproval !== true && replyCid !== freshReplyCid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedPostCid = getString(reply?.postCid) ?? getThreadPostCid(post);
|
||||||
|
if (expectedPostCid) {
|
||||||
|
if (getString(freshReply?.postCid) !== expectedPostCid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getString(freshReply?.parentCid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedParentCid = getString(reply?.parentCid);
|
||||||
|
if (expectedParentCid && getString(freshReply?.parentCid) !== expectedParentCid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedCommunityAddress = getCommentCommunityAddress(reply) ?? getCommentCommunityAddress(post);
|
||||||
|
const freshCommunityAddress = getCommentCommunityAddress(freshReply);
|
||||||
|
return hasSameCommunityAddress(expectedCommunityAddress, freshCommunityAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFreshReplies = (replies: Comment[] = [], options: UseFreshRepliesOptions = {}) => {
|
||||||
|
const { post } = options;
|
||||||
const replyIndices = useMemo(
|
const replyIndices = useMemo(
|
||||||
() => Array.from(new Set(replies.map((reply) => reply?.index).filter((replyIndex): replyIndex is number => typeof replyIndex === 'number'))),
|
() => Array.from(new Set(replies.map((reply) => reply?.index).filter((replyIndex): replyIndex is number => typeof replyIndex === 'number'))),
|
||||||
[replies],
|
[replies],
|
||||||
@@ -74,6 +118,10 @@ const useFreshReplies = (replies: Comment[] = []) => {
|
|||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isFreshReplyForOriginalReply(reply, freshReply, post)) {
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
hasFreshReplies = true;
|
hasFreshReplies = true;
|
||||||
return freshReply;
|
return freshReply;
|
||||||
});
|
});
|
||||||
@@ -99,7 +147,7 @@ const useFreshReplies = (replies: Comment[] = []) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return sortRepliesForDisplay(hasDuplicateReplyIndices ? dedupedReplies : nextReplies);
|
return sortRepliesForDisplay(hasDuplicateReplyIndices ? dedupedReplies : nextReplies);
|
||||||
}, [accountCommentsByCidList, accountCommentsByIndexList, replies]);
|
}, [accountCommentsByCidList, accountCommentsByIndexList, post, replies]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useFreshReplies;
|
export default useFreshReplies;
|
||||||
|
|||||||
Reference in New Issue
Block a user