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,