From 702816f99b0e31d4d9b59e11831a519caf1c9e97 Mon Sep 17 00:00:00 2001 From: plebeius Date: Tue, 10 Feb 2026 16:53:34 +0800 Subject: [PATCH] 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. --- src/components/post-desktop/post-desktop.tsx | 32 +++++++++++++++----- src/components/post-mobile/post-mobile.tsx | 31 ++++++++++++++----- src/lib/utils/url-utils.ts | 5 +-- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index 38a6affb..17024134 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -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(''); useEffect(() => { const all = post ? [post, ...(replies || [])] : replies || []; @@ -653,17 +654,32 @@ const PostDesktop = ({ const quotedByMap = useMemo(() => { const map = new Map(); 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(); + + 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(null); diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index cf13b129..efc5c928 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -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(''); useEffect(() => { const all = post ? [post, ...(replies || [])] : replies || []; @@ -491,17 +493,32 @@ const PostMobile = ({ const quotedByMap = useMemo(() => { const map = new Map(); 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(); + + 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(null); diff --git a/src/lib/utils/url-utils.ts b/src/lib/utils/url-utils.ts index 606b342f..feda7ca1 100644 --- a/src/lib/utils/url-utils.ts +++ b/src/lib/utils/url-utils.ts @@ -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