feat: render >>{number} as interactive quote links with hover preview

Added bidirectional `usePostNumberStore` for post number ↔ CID mapping, populated from thread views. Preprocesses `>>{number}` into markdown links, renders them via `NumberQuoteLink` and `ReplyQuotePreview`, and deduplicates against `quotedCids` when both refer to the same post.
This commit is contained in:
plebeius
2026-02-10 16:23:06 +08:00
parent db70a9148b
commit c4b6c77d4c
4 changed files with 73 additions and 1 deletions
@@ -38,6 +38,7 @@ import { shouldShowSnow } from '../../lib/snow';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
import useChallengesStore from '../../stores/use-challenges-store';
import usePostNumberStore from '../../stores/use-post-number-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
@@ -618,6 +619,20 @@ const PostDesktop = ({
const isHidden = hidden && !isInPostPageView;
const { replies, hasMore, loadMore } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
const registerComments = usePostNumberStore((s) => s.registerComments);
const prevCidsRef = useRef<string>('');
useEffect(() => {
const all = post ? [post, ...(replies || [])] : replies || [];
if (!all.length) return;
const cidsKey = all
.map((c) => c?.cid)
.filter(Boolean)
.sort()
.join(',');
if (cidsKey === prevCidsRef.current) return;
prevCidsRef.current = cidsKey;
registerComments(all);
}, [post, replies, registerComments]);
const visiblelinksCount = useCountLinksInReplies(post, 5);
const totalLinksCount = useCountLinksInReplies(post);
const replyCount = replies?.length;
@@ -33,6 +33,7 @@ import _ from 'lodash';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
import useChallengesStore from '../../stores/use-challenges-store';
import usePostNumberStore from '../../stores/use-post-number-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
const { addChallenge } = useChallengesStore.getState();
@@ -463,6 +464,20 @@ const PostMobile = ({
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
const linksCount = useCountLinksInReplies(post);
const { replies, hasMore, loadMore } = useReplies({ comment: post, accountComments: { newerThan: Infinity } });
const registerComments = usePostNumberStore((s) => s.registerComments);
const prevCidsRef = useRef<string>('');
useEffect(() => {
const all = post ? [post, ...(replies || [])] : replies || [];
if (!all.length) return;
const cidsKey = all
.map((c) => c?.cid)
.filter(Boolean)
.sort()
.join(',');
if (cidsKey === prevCidsRef.current) return;
prevCidsRef.current = cidsKey;
registerComments(all);
}, [post, replies, registerComments]);
const isInPostPageView = isPostPageView(location.pathname, params);
const { hidden, unhide } = useHide({ cid });