From ec404569e78622d4f1b8500dd1ffcc94eb398a85 Mon Sep 17 00:00:00 2001 From: plebeius Date: Fri, 6 Mar 2026 19:24:49 +0800 Subject: [PATCH] fix(quotes): avoid striking unresolved quote links --- .../comment-content/comment-content.tsx | 5 +++-- src/components/markdown/markdown.tsx | 4 ++-- .../reply-quote-preview.tsx | 21 ++++++++++++------- .../utils/__tests__/quote-link-utils.test.ts | 9 +++++++- src/lib/utils/quote-link-utils.ts | 12 ++++++++++- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/components/comment-content/comment-content.tsx b/src/components/comment-content/comment-content.tsx index ac412b59..38145f0e 100644 --- a/src/components/comment-content/comment-content.tsx +++ b/src/components/comment-content/comment-content.tsx @@ -6,6 +6,7 @@ import useSubplebbitsPagesStore from '@bitsocialhq/bitsocial-react-hooks/dist/st import usePostNumberStore from '../../stores/use-post-number-store'; import getShortAddress from '../../lib/get-short-address'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; +import { isUnavailableQuoteTarget } from '../../lib/utils/quote-link-utils'; import { isPostPageView } from '../../lib/utils/view-utils'; import useIsMobile from '../../hooks/use-is-mobile'; import useStateString from '../../hooks/use-state-string'; @@ -23,7 +24,7 @@ const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => { // Prefer hook version to ensure 'number' property is populated for deeper nested replies in Virtuoso const quotedComment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore; const isOP = cid === postCid; - const isUnavailable = !quotedComment || quotedComment.deleted || quotedComment.removed; + const isUnavailable = isUnavailableQuoteTarget(quotedComment); return ; }; @@ -129,7 +130,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => { !(deleted || removed) && (filteredQuotedCids.length > 0 ? filteredQuotedCids.map((cid: string) => ) - : shouldShowReplyingToReply && )} + : shouldShowReplyingToReply && )} {removed ? ( reason ? ( <> diff --git a/src/components/markdown/markdown.tsx b/src/components/markdown/markdown.tsx index 20f551f4..b5bece0f 100644 --- a/src/components/markdown/markdown.tsx +++ b/src/components/markdown/markdown.tsx @@ -277,13 +277,13 @@ const NumberQuoteLink = ({ number, threadPostCid, subplebbitAddress }: { number: const comment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore; const isOP = Boolean(threadPostCid && cid === threadPostCid); - if (!comment || isUnavailableQuoteTarget(comment)) { + if (isUnavailableQuoteTarget(comment)) { return ( ); } - return ; + return ; }; const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string, subplebbitAddress?: string) => { diff --git a/src/components/reply-quote-preview/reply-quote-preview.tsx b/src/components/reply-quote-preview/reply-quote-preview.tsx index 5d7c4f96..7de273f2 100644 --- a/src/components/reply-quote-preview/reply-quote-preview.tsx +++ b/src/components/reply-quote-preview/reply-quote-preview.tsx @@ -5,7 +5,7 @@ import { Comment, useAccount } from '@bitsocialhq/bitsocial-react-hooks'; import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react'; import { useDirectories } from '../../hooks/use-directories'; import { getBoardPath } from '../../lib/utils/route-utils'; -import { formatQuoteNumber, isUnavailableQuoteTarget, shouldShowFloatingQuotePreview } from '../../lib/utils/quote-link-utils'; +import { formatQuoteNumber, getQuoteTargetAvailability, shouldShowFloatingQuotePreview } from '../../lib/utils/quote-link-utils'; import useIsMobile from '../../hooks/use-is-mobile'; import styles from '../../views/post/post.module.css'; import { Post } from '../../views/post'; @@ -209,7 +209,9 @@ const DesktopQuotePreview = ({ const resolvedQuotelinkNumber = quotelinkReply?.number ?? quotelinkNumber; const resolvedQuotelinkCid = quotelinkReply?.cid; const resolvedQuotelinkSubplebbitAddress = quotelinkReply?.subplebbitAddress; - const quotelinkUnavailable = Boolean(isQuotelinkUnavailable || isUnavailableQuoteTarget(quotelinkReply)); + const quoteTargetAvailability = getQuoteTargetAvailability(quotelinkReply); + const quotelinkUnavailable = Boolean(isQuotelinkUnavailable || quoteTargetAvailability === 'unavailable'); + const quotelinkPendingResolution = !quotelinkUnavailable && quoteTargetAvailability === 'unresolved'; const quotelinkClassName = quotelinkUnavailable ? `${styles.quoteLink} ${styles.quoteLinkUnavailable}` : styles.quoteLink; const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, directories) : undefined; const quotelinkRoute = quotelinkReply?.cid ? (quotelinkBoardPath ? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}` : `/thread/${quotelinkReply.cid}`) : '#'; @@ -231,6 +233,8 @@ const DesktopQuotePreview = ({ <> {quotelinkUnavailable ? ( {quotelinkLabel} + ) : quotelinkPendingResolution ? ( + {quotelinkLabel} ) : ( handleMouseOver(resolvedQuotelinkCid)} - onMouseLeave={quotelinkUnavailable ? undefined : () => handleMouseLeave(resolvedQuotelinkCid)} + ref={quotelinkUnavailable || quotelinkPendingResolution ? undefined : refs.setReference} + className={quotelinkPendingResolution ? styles.quoteLink : quotelinkClassName} + onMouseOver={quotelinkUnavailable || quotelinkPendingResolution ? undefined : () => handleMouseOver(resolvedQuotelinkCid)} + onMouseLeave={quotelinkUnavailable || quotelinkPendingResolution ? undefined : () => handleMouseLeave(resolvedQuotelinkCid)} > {formatQuoteNumber(resolvedQuotelinkNumber)} {isOP && ' (OP)'} {quotelinkReply?.author?.address === account?.author?.address && ' (You)'} {!quotelinkUnavailable && + !quotelinkPendingResolution && resolvedQuotelinkNumber && (() => { const quotelinkBoardPath = resolvedQuotelinkSubplebbitAddress ? getBoardPath(resolvedQuotelinkSubplebbitAddress, directories) : undefined; diff --git a/src/lib/utils/__tests__/quote-link-utils.test.ts b/src/lib/utils/__tests__/quote-link-utils.test.ts index 1642f81f..c406d9b1 100644 --- a/src/lib/utils/__tests__/quote-link-utils.test.ts +++ b/src/lib/utils/__tests__/quote-link-utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { formatQuoteNumber, isUnavailableQuoteTarget, shouldShowFloatingQuotePreview } from '../quote-link-utils'; +import { formatQuoteNumber, getQuoteTargetAvailability, isUnavailableQuoteTarget, shouldShowFloatingQuotePreview } from '../quote-link-utils'; describe('quote-link-utils', () => { it('formats quote numbers with the expected prefix', () => { @@ -7,6 +7,13 @@ describe('quote-link-utils', () => { expect(formatQuoteNumber()).toBe('>>?'); }); + it('distinguishes unresolved and unavailable quote targets', () => { + expect(getQuoteTargetAvailability(undefined)).toBe('unresolved'); + expect(getQuoteTargetAvailability({ deleted: true, removed: false })).toBe('unavailable'); + expect(getQuoteTargetAvailability({ deleted: false, removed: true })).toBe('unavailable'); + expect(getQuoteTargetAvailability({ deleted: false, removed: false })).toBe('available'); + }); + it('marks deleted and removed comments as unavailable quote targets', () => { expect(isUnavailableQuoteTarget(undefined)).toBe(false); expect(isUnavailableQuoteTarget({ deleted: true, removed: false })).toBe(true); diff --git a/src/lib/utils/quote-link-utils.ts b/src/lib/utils/quote-link-utils.ts index 83a4593c..f2ec0148 100644 --- a/src/lib/utils/quote-link-utils.ts +++ b/src/lib/utils/quote-link-utils.ts @@ -1,8 +1,18 @@ import type { Comment } from '@bitsocialhq/bitsocial-react-hooks'; +type QuoteTargetAvailability = 'available' | 'unresolved' | 'unavailable'; + export const formatQuoteNumber = (number?: number) => `>>${number ?? '?'}`; -export const isUnavailableQuoteTarget = (comment?: Partial> | null) => Boolean(comment?.deleted || comment?.removed); +export const getQuoteTargetAvailability = (comment?: Partial> | null): QuoteTargetAvailability => { + if (!comment) { + return 'unresolved'; + } + + return comment.deleted || comment.removed ? 'unavailable' : 'available'; +}; + +export const isUnavailableQuoteTarget = (comment?: Partial> | null) => getQuoteTargetAvailability(comment) === 'unavailable'; export const shouldShowFloatingQuotePreview = ({ hoveredCid,