@@ -363,15 +414,26 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
};
-const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP, showTrailingBreak }: ReplyQuotePreviewProps) => {
+const ReplyQuotePreview = ({
+ backlinkReply,
+ quotelinkReply,
+ quotelinkNumber,
+ isBacklinkReply,
+ isQuotelinkReply,
+ isQuotelinkUnavailable,
+ isOP,
+ showTrailingBreak,
+}: ReplyQuotePreviewProps) => {
const isMobile = useIsMobile();
return isMobile ? (
@@ -379,8 +441,10 @@ const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQ
diff --git a/src/lib/utils/__tests__/quote-link-utils.test.ts b/src/lib/utils/__tests__/quote-link-utils.test.ts
new file mode 100644
index 00000000..1642f81f
--- /dev/null
+++ b/src/lib/utils/__tests__/quote-link-utils.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from 'vitest';
+import { formatQuoteNumber, isUnavailableQuoteTarget, shouldShowFloatingQuotePreview } from '../quote-link-utils';
+
+describe('quote-link-utils', () => {
+ it('formats quote numbers with the expected prefix', () => {
+ expect(formatQuoteNumber(123)).toBe('>>123');
+ expect(formatQuoteNumber()).toBe('>>?');
+ });
+
+ it('marks deleted and removed comments as unavailable quote targets', () => {
+ expect(isUnavailableQuoteTarget(undefined)).toBe(false);
+ expect(isUnavailableQuoteTarget({ deleted: true, removed: false })).toBe(true);
+ expect(isUnavailableQuoteTarget({ deleted: false, removed: true })).toBe(true);
+ expect(isUnavailableQuoteTarget({ deleted: false, removed: false })).toBe(false);
+ });
+
+ it('only shows floating previews for available targets that are hovered out of view', () => {
+ expect(
+ shouldShowFloatingQuotePreview({
+ hoveredCid: 'cid-1',
+ outOfViewCid: 'cid-1',
+ quoteCid: 'cid-1',
+ isUnavailable: false,
+ }),
+ ).toBe(true);
+
+ expect(
+ shouldShowFloatingQuotePreview({
+ hoveredCid: 'cid-1',
+ outOfViewCid: 'cid-1',
+ quoteCid: 'cid-1',
+ isUnavailable: true,
+ }),
+ ).toBe(false);
+
+ expect(
+ shouldShowFloatingQuotePreview({
+ hoveredCid: 'cid-1',
+ outOfViewCid: 'cid-2',
+ quoteCid: 'cid-1',
+ isUnavailable: false,
+ }),
+ ).toBe(false);
+ });
+});
diff --git a/src/lib/utils/quote-link-utils.ts b/src/lib/utils/quote-link-utils.ts
new file mode 100644
index 00000000..83a4593c
--- /dev/null
+++ b/src/lib/utils/quote-link-utils.ts
@@ -0,0 +1,17 @@
+import type { Comment } from '@bitsocialhq/bitsocial-react-hooks';
+
+export const formatQuoteNumber = (number?: number) => `>>${number ?? '?'}`;
+
+export const isUnavailableQuoteTarget = (comment?: Partial
> | null) => Boolean(comment?.deleted || comment?.removed);
+
+export const shouldShowFloatingQuotePreview = ({
+ hoveredCid,
+ outOfViewCid,
+ quoteCid,
+ isUnavailable,
+}: {
+ hoveredCid: string | null;
+ outOfViewCid: string | null;
+ quoteCid?: string;
+ isUnavailable?: boolean;
+}) => Boolean(quoteCid && !isUnavailable && hoveredCid === quoteCid && outOfViewCid === quoteCid);
diff --git a/src/views/post/post.module.css b/src/views/post/post.module.css
index 1a9b8bc0..0298b931 100644
--- a/src/views/post/post.module.css
+++ b/src/views/post/post.module.css
@@ -490,6 +490,13 @@
cursor: pointer;
}
+.quoteLinkUnavailable,
+.quoteLinkUnavailable:hover {
+ color: var(--post-quotelink-text-color);
+ text-decoration: line-through;
+ cursor: default;
+}
+
.replyMobile {
padding-top: 7px;
content-visibility: auto;