fix(quotes): scope post-number lookup by subplebbit, OP quote always navigates to thread

- numberToCid scoped by subplebbit address to fix wrong preview in /all/
- OP quote click navigates to thread page instead of highlighting card
This commit is contained in:
plebeius
2026-02-26 15:08:33 +08:00
parent c8824eb5d5
commit 216073aee3
9 changed files with 55 additions and 29 deletions
@@ -147,7 +147,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
) )
) : ( ) : (
<> <>
{!showOriginal && <Markdown content={displayContent} postCid={postCid} />} {!showOriginal && <Markdown content={displayContent} postCid={postCid} subplebbitAddress={subplebbitAddress} />}
{pendingApproval && ( {pendingApproval && (
<> <>
<br /> <br />
@@ -182,7 +182,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
)} )}
{edit && original?.content !== content && ( {edit && original?.content !== content && (
<span className={styles.editedInfo}> <span className={styles.editedInfo}>
{showOriginal && <Markdown content={original?.content} postCid={postCid} />} {showOriginal && <Markdown content={original?.content} postCid={postCid} subplebbitAddress={subplebbitAddress} />}
<br /> <br />
<br /> <br />
<Trans <Trans
+9 -8
View File
@@ -186,12 +186,13 @@ interface MarkdownProps {
content: string; content: string;
title?: string; title?: string;
postCid?: string; postCid?: string;
subplebbitAddress?: string;
} }
const NUMBER_QUOTE_HREF_REGEX = /^#q-(\d+)$/; const NUMBER_QUOTE_HREF_REGEX = /^#q-(\d+)$/;
const NumberQuoteLink = ({ number, threadPostCid }: { number: number; threadPostCid?: string }) => { const NumberQuoteLink = ({ number, threadPostCid, subplebbitAddress }: { number: number; threadPostCid?: string; subplebbitAddress?: string }) => {
const cid = usePostNumberStore((state) => state.numberToCid[number]); const cid = usePostNumberStore((state) => (subplebbitAddress ? state.numberToCid[subplebbitAddress]?.[number] : undefined));
const commentFromStore = useSubplebbitsPagesStore((state) => (cid ? state.comments[cid] : undefined)); const commentFromStore = useSubplebbitsPagesStore((state) => (cid ? state.comments[cid] : undefined));
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true }); const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
const comment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore; const comment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
@@ -204,7 +205,7 @@ const NumberQuoteLink = ({ number, threadPostCid }: { number: number; threadPost
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} isOP={isOP} showTrailingBreak={false} />; return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} isOP={isOP} showTrailingBreak={false} />;
}; };
const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string) => { const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string, subplebbitAddress?: string) => {
if (!href) { if (!href) {
return <span>{children}</span>; return <span>{children}</span>;
} }
@@ -214,7 +215,7 @@ const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid
const number = parseInt(numberQuoteMatch[1], 10); const number = parseInt(numberQuoteMatch[1], 10);
return ( return (
<span className={styles.inlineQuoteLink}> <span className={styles.inlineQuoteLink}>
<NumberQuoteLink number={number} threadPostCid={threadPostCid} /> <NumberQuoteLink number={number} threadPostCid={threadPostCid} subplebbitAddress={subplebbitAddress} />
</span> </span>
); );
} }
@@ -268,7 +269,7 @@ const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid
); );
}; };
const Markdown = ({ content, title, postCid }: MarkdownProps) => { const Markdown = ({ content, title, postCid, subplebbitAddress }: MarkdownProps) => {
const remarkPlugins = useMemo(() => { const remarkPlugins = useMemo(() => {
const plugins: any[] = [[supersub]]; const plugins: any[] = [[supersub]];
@@ -338,13 +339,13 @@ const Markdown = ({ content, title, postCid }: MarkdownProps) => {
console.debug('Invalid URL:', href); console.debug('Invalid URL:', href);
} }
return renderAnchorLink(children, href, postCid); return renderAnchorLink(children, href, postCid, subplebbitAddress);
} }
return renderAnchorLink(children, href || '', postCid); return renderAnchorLink(children, href || '', postCid, subplebbitAddress);
}, },
}) as ExtendedComponents, }) as ExtendedComponents,
[isInCatalogView, postCid], [isInCatalogView, postCid, subplebbitAddress],
); );
return ( return (
+1 -1
View File
@@ -849,7 +849,7 @@ const PostDesktop = ({
return map; return map;
})(); })();
const quotedByMap = useQuotedByMap(filteredReplies); const quotedByMap = useQuotedByMap(filteredReplies, subplebbitAddress);
const visibleReplies = useProgressiveRender(filteredReplies, { const visibleReplies = useProgressiveRender(filteredReplies, {
batchSize: 50, batchSize: 50,
+1 -1
View File
@@ -606,7 +606,7 @@ const PostMobile = ({
return map; return map;
})(); })();
const quotedByMap = useQuotedByMap(filteredReplies); const quotedByMap = useQuotedByMap(filteredReplies, subplebbitAddress);
const visibleReplies = useProgressiveRender(filteredReplies, { const visibleReplies = useProgressiveRender(filteredReplies, {
batchSize: 50, batchSize: 50,
@@ -131,8 +131,12 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
if (cid && subplebbitAddress) { if (cid && subplebbitAddress) {
const boardPath = getBoardPath(subplebbitAddress, directories); const boardPath = getBoardPath(subplebbitAddress, directories);
const threadRoute = `/${boardPath}/thread/${cid}`; const threadRoute = `/${boardPath}/thread/${cid}`;
if (isOpQuote && location.pathname === threadRoute) { if (isOpQuote) {
scrollToThreadCardTop(cid); if (location.pathname === threadRoute) {
scrollToThreadCardTop(cid);
} else {
navigate(threadRoute);
}
return; return;
} }
if (scrollToReplyOnPage(cid)) return; if (scrollToReplyOnPage(cid)) return;
@@ -248,8 +252,12 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
if (cid && subplebbitAddress) { if (cid && subplebbitAddress) {
const boardPath = getBoardPath(subplebbitAddress, directories); const boardPath = getBoardPath(subplebbitAddress, directories);
const threadRoute = `/${boardPath}/thread/${cid}`; const threadRoute = `/${boardPath}/thread/${cid}`;
if (isOpQuote && location.pathname === threadRoute) { if (isOpQuote) {
scrollToThreadCardTop(cid); if (location.pathname === threadRoute) {
scrollToThreadCardTop(cid);
} else {
navigate(threadRoute);
}
return; return;
} }
if (scrollToReplyOnPage(cid)) return; if (scrollToReplyOnPage(cid)) return;
+2 -2
View File
@@ -56,8 +56,8 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]); const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]);
const numberToCid = usePostNumberStore((state) => state.numberToCid); const scopedNumberToCid = usePostNumberStore((state) => (subplebbitAddress ? state.numberToCid[subplebbitAddress] : undefined));
const quotedCids = useMemo(() => getQuotedCidsFromContent(content, numberToCid), [content, numberToCid]); const quotedCids = useMemo(() => getQuotedCidsFromContent(content, scopedNumberToCid), [content, scopedNumberToCid]);
const publishOptions = useMemo(() => mergeQuotedCids(publishCommentOptions, quotedCids), [publishCommentOptions, quotedCids]); const publishOptions = useMemo(() => mergeQuotedCids(publishCommentOptions, quotedCids), [publishCommentOptions, quotedCids]);
+14 -6
View File
@@ -61,32 +61,40 @@ const extractReplyQuoteTargets = (replies: Comment[]) => {
}; };
}; };
const useQuotedByMap = (replies: Comment[] = []) => { const useQuotedByMap = (replies: Comment[] = [], subplebbitAddress?: string) => {
const stableQuotedByMapRef = useRef<Map<string, Comment[]>>(new Map()); const stableQuotedByMapRef = useRef<Map<string, Comment[]>>(new Map());
const { replyQuoteTargets, quotedPostNumbers } = useMemo(() => extractReplyQuoteTargets(replies), [replies]); const { replyQuoteTargets, quotedPostNumbers } = useMemo(() => extractReplyQuoteTargets(replies), [replies]);
// Subscribe only to post numbers referenced in this thread to avoid unrelated global store churn.
const quotedNumbersSignature = usePostNumberStore( const quotedNumbersSignature = usePostNumberStore(
useCallback((state) => quotedPostNumbers.map((postNumber) => `${postNumber}:${state.numberToCid[postNumber] ?? ''}`).join('|'), [quotedPostNumbers]), useCallback(
(state) => {
const scoped = subplebbitAddress ? state.numberToCid[subplebbitAddress] : undefined;
return quotedPostNumbers.map((postNumber) => `${postNumber}:${scoped?.[postNumber] ?? ''}`).join('|');
},
[quotedPostNumbers, subplebbitAddress],
),
); );
const quotedNumberToCid = useMemo(() => { const quotedNumberToCid = useMemo(() => {
if (quotedPostNumbers.length === 0) { if (quotedPostNumbers.length === 0 || !subplebbitAddress) {
return {} as Record<number, string>; return {} as Record<number, string>;
} }
const { numberToCid } = usePostNumberStore.getState(); const { numberToCid } = usePostNumberStore.getState();
const scoped = numberToCid[subplebbitAddress];
if (!scoped) return {} as Record<number, string>;
const nextQuotedNumberToCid: Record<number, string> = {}; const nextQuotedNumberToCid: Record<number, string> = {};
for (const postNumber of quotedPostNumbers) { for (const postNumber of quotedPostNumbers) {
const quotedCid = numberToCid[postNumber]; const quotedCid = scoped[postNumber];
if (quotedCid) { if (quotedCid) {
nextQuotedNumberToCid[postNumber] = quotedCid; nextQuotedNumberToCid[postNumber] = quotedCid;
} }
} }
return nextQuotedNumberToCid; return nextQuotedNumberToCid;
}, [quotedPostNumbers, quotedNumbersSignature]); }, [quotedPostNumbers, subplebbitAddress, quotedNumbersSignature]);
return useMemo(() => { return useMemo(() => {
const map = new Map<string, Comment[]>(); const map = new Map<string, Comment[]>();
+2 -2
View File
@@ -1,7 +1,7 @@
import { QUOTE_NUMBER_REGEX } from './url-utils'; import { QUOTE_NUMBER_REGEX } from './url-utils';
export const getQuotedCidsFromContent = (content: string | undefined, numberToCid: Record<number, string>) => { export const getQuotedCidsFromContent = (content: string | undefined, numberToCid: Record<number, string> | undefined) => {
if (!content) return undefined; if (!content || !numberToCid) return undefined;
const cids = new Set<string>(); const cids = new Set<string>();
for (const match of content.matchAll(new RegExp(QUOTE_NUMBER_REGEX.source, 'g'))) { for (const match of content.matchAll(new RegExp(QUOTE_NUMBER_REGEX.source, 'g'))) {
const num = parseInt(match[1], 10); const num = parseInt(match[1], 10);
+12 -3
View File
@@ -2,7 +2,9 @@ import { create } from 'zustand';
import type { Comment } from '@plebbit/plebbit-react-hooks'; import type { Comment } from '@plebbit/plebbit-react-hooks';
interface PostNumberState { interface PostNumberState {
numberToCid: Record<number, string>; // Post numbers are only unique within a subplebbit, so scope by address
// to avoid collisions in /all/ where multiple boards are shown together.
numberToCid: Record<string, Record<number, string>>;
cidToNumber: Record<string, number>; cidToNumber: Record<string, number>;
registerComments: (comments: Comment[]) => void; registerComments: (comments: Comment[]) => void;
} }
@@ -21,13 +23,20 @@ const usePostNumberStore = create<PostNumberState>((set) => ({
for (const c of comments) { for (const c of comments) {
const num = c?.number; const num = c?.number;
const cid = c?.cid; const cid = c?.cid;
if (typeof num === 'number' && cid && (nextNumberToCid[num] !== cid || nextCidToNumber[cid] !== num)) { const addr = c?.subplebbitAddress;
if (typeof num !== 'number' || !cid || !addr) continue;
const existingCid = nextNumberToCid[addr]?.[num];
if (existingCid !== cid || nextCidToNumber[cid] !== num) {
if (!hasUpdates) { if (!hasUpdates) {
nextNumberToCid = { ...state.numberToCid }; nextNumberToCid = { ...state.numberToCid };
nextCidToNumber = { ...state.cidToNumber }; nextCidToNumber = { ...state.cidToNumber };
hasUpdates = true; hasUpdates = true;
} }
nextNumberToCid[num] = cid; if (!nextNumberToCid[addr] || nextNumberToCid[addr] === state.numberToCid[addr]) {
nextNumberToCid[addr] = { ...nextNumberToCid[addr] };
}
nextNumberToCid[addr][num] = cid;
nextCidToNumber[cid] = num; nextCidToNumber[cid] = num;
} }
} }