fix: include manual post-number quotes in reply backlinks

Backlink map generation now merges protocol-level quotedCids with parsed >>number references from reply content. Manual quotes are resolved through numberToCid from usePostNumberStore and deduplicated using Set. Centralized QUOTE_NUMBER_REGEX in url-utils to keep markdown conversion and backlink detection aligned.
This commit is contained in:
plebeius
2026-02-10 16:53:34 +08:00
parent 35783ff627
commit 702816f99b
3 changed files with 51 additions and 17 deletions
+24 -8
View File
@@ -8,7 +8,7 @@ import styles from '../../views/post/post.module.css';
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils';
import { isValidURL, QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils';
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
import useModQueueStore from '../../stores/use-mod-queue-store';
@@ -620,6 +620,7 @@ const PostDesktop = ({
const { replies, hasMore, loadMore } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
const registerComments = usePostNumberStore((s) => s.registerComments);
const numberToCid = usePostNumberStore((s) => s.numberToCid);
const prevCidsRef = useRef<string>('');
useEffect(() => {
const all = post ? [post, ...(replies || [])] : replies || [];
@@ -653,17 +654,32 @@ const PostDesktop = ({
const quotedByMap = useMemo(() => {
const map = new Map<string, Comment[]>();
for (const reply of filteredReplies) {
const quotedCids = reply.quotedCids;
if (quotedCids?.length) {
for (const quotedCid of quotedCids) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
const cidSet = new Set<string>();
if (reply.quotedCids?.length) {
for (const quotedCid of reply.quotedCids) {
cidSet.add(quotedCid);
}
}
if (reply.content) {
for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) {
const postNumber = parseInt(match[1], 10);
const quotedCid = numberToCid[postNumber];
if (quotedCid) {
cidSet.add(quotedCid);
}
}
}
for (const quotedCid of cidSet) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
}
}
return map;
}, [filteredReplies]);
}, [filteredReplies, numberToCid]);
// Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
+24 -7
View File
@@ -9,6 +9,7 @@ import { shouldShowSnow } from '../../lib/snow';
import { getHasThumbnail } from '../../lib/utils/media-utils';
import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils';
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
import useModQueueStore from '../../stores/use-mod-queue-store';
@@ -465,6 +466,7 @@ const PostMobile = ({
const linksCount = useCountLinksInReplies(post);
const { replies, hasMore, loadMore } = useReplies({ comment: post, accountComments: { newerThan: Infinity } });
const registerComments = usePostNumberStore((s) => s.registerComments);
const numberToCid = usePostNumberStore((s) => s.numberToCid);
const prevCidsRef = useRef<string>('');
useEffect(() => {
const all = post ? [post, ...(replies || [])] : replies || [];
@@ -491,17 +493,32 @@ const PostMobile = ({
const quotedByMap = useMemo(() => {
const map = new Map<string, Comment[]>();
for (const reply of filteredReplies) {
const quotedCids = reply.quotedCids;
if (quotedCids?.length) {
for (const quotedCid of quotedCids) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
const cidSet = new Set<string>();
if (reply.quotedCids?.length) {
for (const quotedCid of reply.quotedCids) {
cidSet.add(quotedCid);
}
}
if (reply.content) {
for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) {
const postNumber = parseInt(match[1], 10);
const quotedCid = numberToCid[postNumber];
if (quotedCid) {
cidSet.add(quotedCid);
}
}
}
for (const quotedCid of cidSet) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
}
}
return map;
}, [filteredReplies]);
}, [filteredReplies, numberToCid]);
// Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
+3 -2
View File
@@ -1,5 +1,7 @@
import { copyToClipboard } from './clipboard-utils';
export const QUOTE_NUMBER_REGEX = /(?<![>/\w])>>(\d+)/g;
export const getHostname = (url: string) => {
try {
return new URL(url).hostname.replace(/^www\./, '');
@@ -183,8 +185,7 @@ export const isValidCrossboardPattern = (pattern: string): boolean => {
// Transform >>{number} post number patterns to markdown links with special anchor
const preprocessPostNumberPatterns = (content: string): string => {
// Match >> followed by digits, avoid overlap with greentext (>>>), cross-board (>>>/), URLs, CID-like patterns
const pattern = /(?<![>/\w])>>(\d+)(?![\d/])/g;
return content.replace(pattern, (_, num) => `[>>${num}](#q-${num})`);
return content.replace(QUOTE_NUMBER_REGEX, (_, num) => `[>>${num}](#q-${num})`);
};
// Preprocess content to convert plain text 5chan cross-board patterns to markdown links