mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(quotes): handle cross-thread quotes (publish + hover preview) (#1153)
* fix(quotes): publish only same-thread quoted cids
Quoting a post from another thread failed to publish with "One or more
quoted CIDs are not under the same post". That message is the protocol
error ERR_QUOTED_CID_NOT_UNDER_POST from @pkcprotocol/pkc-js, not a custom
5chan error: a reply's quotedCids must all live under the reply's own
thread.
The reply publish path resolved every >>number quotelink to a CID through
the board-scoped number map and sent them all as quotedCids, so any
cross-thread quotelink made the protocol reject the whole post.
Track each comment's thread (postCid) in usePostNumberStore and filter the
merged quotedCids to same-thread entries before publishing. Cross-thread
quotelinks still render and navigate via their >>number text; only the
published metadata changes. Same-thread replies resolved on demand by the
external-quote resolver are preserved because the resolver registers what
it finds.
Regression introduced in 9f14a6326 (populate quotedCids when publishing
replies with quote references).
* fix(quotes): show hover preview for cross-thread quotelinks
Hovering a >>number quote whose thread was not loaded showed nothing: the
quotelink rendered as an inert span with no floating preview. NumberQuoteLink
only fell back to the lazy resolver when the number->cid mapping was unknown.
When the cid was known but the comment body was not cached, it rendered
ReplyQuotePreview with an undefined comment, which is treated as pending
resolution and rendered as inert text that never fetched the body (the lookup
used onlyIfCached).
Load the body lazily on hover instead: NumberQuoteLink keeps onlyIfCached until
the quotelink is hovered, and ReplyQuotePreview's pending quotelink is now
hoverable when its cid is known so it can request that fetch and prime the
floating-preview position. Once the body resolves the preview appears without
re-hovering. Same-thread quotes stay cached, so this never fetches for them.
* fix(quotes): filter the final quotedCids payload to same-thread
Apply the same-thread filter to the merged publish options rather than only the
derived subset, so any quotedCids carried on stored publishCommentOptions cannot
bypass the guard. Defense in depth for the protocol's same-thread requirement
(ERR_QUOTED_CID_NOT_UNDER_POST); addresses PR review feedback.
This commit is contained in:
@@ -80,7 +80,8 @@ describe('usePublishReply', () => {
|
||||
testState.lastPublishOptions = undefined;
|
||||
testState.publishAuthorBlockedReason = undefined;
|
||||
useChallengesStore.setState({ challenges: [] });
|
||||
usePostNumberStore.setState({ cidToNumber: {}, numberToCid: { 'music.eth': { 12: 'quoted-cid' } } });
|
||||
// 'quoted-cid' (post #12) lives under the reply's own thread (postCid falls back to 'parent-cid').
|
||||
usePostNumberStore.setState({ cidToNumber: {}, numberToCid: { 'music.eth': { 12: 'quoted-cid' } }, cidToPostCid: { 'quoted-cid': 'parent-cid' } });
|
||||
usePublishReplyStore.setState({
|
||||
author: {},
|
||||
challengeRequest: {},
|
||||
@@ -127,12 +128,49 @@ describe('usePublishReply', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('drops cross-thread quoted cids so the protocol does not reject the publish', async () => {
|
||||
// Post #12 resolves to a comment that lives under a different thread; quoting it must not be
|
||||
// published as a quotedCid (ERR_QUOTED_CID_NOT_UNDER_POST), even though the >>12 text stays.
|
||||
usePostNumberStore.setState({ cidToPostCid: { 'quoted-cid': 'another-thread-cid' } });
|
||||
|
||||
await act(async () => {
|
||||
latestValue.setPublishReplyOptions({
|
||||
content: 'Replying to >>12',
|
||||
} as never);
|
||||
});
|
||||
|
||||
expect(testState.lastPublishOptions).toMatchObject({
|
||||
content: 'Replying to >>12',
|
||||
parentCid: 'parent-cid',
|
||||
postCid: 'parent-cid',
|
||||
});
|
||||
expect(testState.lastPublishOptions?.quotedCids).toBeUndefined();
|
||||
});
|
||||
|
||||
it('drops cross-thread cids that arrive via stored publish options, keeping same-thread ones', async () => {
|
||||
// Defense in depth: even quotedCids carried on the stored publish options must be filtered to
|
||||
// the reply's own thread before publishing, never bypassing the same-thread guard.
|
||||
await act(async () => {
|
||||
usePostNumberStore.setState({ cidToPostCid: { 'quoted-cid': 'parent-cid', 'foreign-cid': 'other-thread-cid' } });
|
||||
usePublishReplyStore.setState({
|
||||
publishCommentOptions: {
|
||||
'parent-cid': { content: 'body', parentCid: 'parent-cid', postCid: 'parent-cid', quotedCids: ['quoted-cid', 'foreign-cid'] },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(testState.lastPublishOptions?.quotedCids).toEqual(['quoted-cid']);
|
||||
});
|
||||
|
||||
it('resolves same-board external quote references before triggering publish', async () => {
|
||||
testState.resolveExternalQuoteTargetMock.mockResolvedValue({
|
||||
cid: 'external-cid',
|
||||
route: '/music/thread/external-cid',
|
||||
communityAddress: 'music.eth',
|
||||
});
|
||||
// The resolver registers whatever it finds; here #44 turns out to be an unloaded reply under
|
||||
// this same thread, so its cid is allowed in the published quotedCids.
|
||||
usePostNumberStore.setState({ cidToPostCid: { 'quoted-cid': 'parent-cid', 'external-cid': 'parent-cid' } });
|
||||
|
||||
await act(async () => {
|
||||
latestValue.setPublishReplyOptions({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { useDirectories } from './use-directories';
|
||||
import usePublishReplyStore from '../stores/use-publish-reply-store';
|
||||
import usePostNumberStore, { getScopedNumberToCidMap } from '../stores/use-post-number-store';
|
||||
import { getQuotedCidsFromContent, mergeQuotedCids } from '../lib/utils/reply-quote-utils';
|
||||
import { filterSameThreadQuotedCids, getQuotedCidsFromContent, mergeQuotedCids } from '../lib/utils/reply-quote-utils';
|
||||
import { extractUnresolvedExternalQuoteReferences, getExternalQuoteStatusMessage } from '../lib/utils/external-quote-utils';
|
||||
import { resolveExternalQuoteTarget } from '../lib/utils/external-quote-resolver';
|
||||
import useChallengesStore from '../stores/use-challenges-store';
|
||||
@@ -97,6 +97,8 @@ const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOpti
|
||||
const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]);
|
||||
|
||||
const scopedNumberToCid = usePostNumberStore((state) => getScopedNumberToCidMap(state.numberToCid, communityAddress));
|
||||
const cidToPostCid = usePostNumberStore((state) => state.cidToPostCid);
|
||||
const threadPostCid = postCid ?? parentCid;
|
||||
const quotedCids = useMemo(() => getQuotedCidsFromContent(content, scopedNumberToCid), [content, scopedNumberToCid]);
|
||||
const unresolvedExternalQuoteReferences = useMemo(
|
||||
() =>
|
||||
@@ -126,7 +128,19 @@ const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOpti
|
||||
return merged.size > 0 ? [...merged] : undefined;
|
||||
}, [quotedCids, resolvedExternalQuotedCids]);
|
||||
|
||||
const mergedPublishOptions = useMemo(() => mergeQuotedCids(publishCommentOptions, mergedQuotedCids), [publishCommentOptions, mergedQuotedCids]);
|
||||
const mergedPublishOptions = useMemo(() => {
|
||||
// Filter the FINAL payload so a reply only ever publishes same-thread quotedCids, even for any
|
||||
// that arrived via stored publishCommentOptions. The protocol rejects cross-thread quotes
|
||||
// (ERR_QUOTED_CID_NOT_UNDER_POST); cross-thread quotelinks still render/navigate via their text.
|
||||
const options = mergeQuotedCids(publishCommentOptions, mergedQuotedCids);
|
||||
if (!options?.quotedCids) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const sameThreadQuotedCids = filterSameThreadQuotedCids(options.quotedCids, cidToPostCid, threadPostCid);
|
||||
const { quotedCids: _crossThreadQuotedCids, ...optionsWithoutQuotedCids } = options;
|
||||
return sameThreadQuotedCids ? { ...optionsWithoutQuotedCids, quotedCids: sameThreadQuotedCids } : optionsWithoutQuotedCids;
|
||||
}, [publishCommentOptions, mergedQuotedCids, cidToPostCid, threadPostCid]);
|
||||
const publishOptionsWithAbandon = useMemo(
|
||||
() => ({
|
||||
...mergedPublishOptions,
|
||||
|
||||
Reference in New Issue
Block a user