mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(quotes): avoid striking unresolved quote links
This commit is contained in:
@@ -6,6 +6,7 @@ import useSubplebbitsPagesStore from '@bitsocialhq/bitsocial-react-hooks/dist/st
|
|||||||
import usePostNumberStore from '../../stores/use-post-number-store';
|
import usePostNumberStore from '../../stores/use-post-number-store';
|
||||||
import getShortAddress from '../../lib/get-short-address';
|
import getShortAddress from '../../lib/get-short-address';
|
||||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||||
|
import { isUnavailableQuoteTarget } from '../../lib/utils/quote-link-utils';
|
||||||
import { isPostPageView } from '../../lib/utils/view-utils';
|
import { isPostPageView } from '../../lib/utils/view-utils';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import useStateString from '../../hooks/use-state-string';
|
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
|
// Prefer hook version to ensure 'number' property is populated for deeper nested replies in Virtuoso
|
||||||
const quotedComment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
|
const quotedComment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
|
||||||
const isOP = cid === postCid;
|
const isOP = cid === postCid;
|
||||||
const isUnavailable = !quotedComment || quotedComment.deleted || quotedComment.removed;
|
const isUnavailable = isUnavailableQuoteTarget(quotedComment);
|
||||||
|
|
||||||
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} quotelinkNumber={quotedNumber} isQuotelinkUnavailable={isUnavailable} isOP={isOP} />;
|
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} quotelinkNumber={quotedNumber} isQuotelinkUnavailable={isUnavailable} isOP={isOP} />;
|
||||||
};
|
};
|
||||||
@@ -129,7 +130,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
|||||||
!(deleted || removed) &&
|
!(deleted || removed) &&
|
||||||
(filteredQuotedCids.length > 0
|
(filteredQuotedCids.length > 0
|
||||||
? filteredQuotedCids.map((cid: string) => <QuotedCidLink key={cid} cid={cid} postCid={postCid} />)
|
? filteredQuotedCids.map((cid: string) => <QuotedCidLink key={cid} cid={cid} postCid={postCid} />)
|
||||||
: shouldShowReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />)}
|
: shouldShowReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} quotelinkNumber={parentNumber} />)}
|
||||||
{removed ? (
|
{removed ? (
|
||||||
reason ? (
|
reason ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -277,13 +277,13 @@ const NumberQuoteLink = ({ number, threadPostCid, subplebbitAddress }: { number:
|
|||||||
const comment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
|
const comment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
|
||||||
const isOP = Boolean(threadPostCid && cid === threadPostCid);
|
const isOP = Boolean(threadPostCid && cid === threadPostCid);
|
||||||
|
|
||||||
if (!comment || isUnavailableQuoteTarget(comment)) {
|
if (isUnavailableQuoteTarget(comment)) {
|
||||||
return (
|
return (
|
||||||
<ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} quotelinkNumber={number} isQuotelinkUnavailable={true} isOP={isOP} showTrailingBreak={false} />
|
<ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} quotelinkNumber={number} isQuotelinkUnavailable={true} isOP={isOP} showTrailingBreak={false} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} isOP={isOP} showTrailingBreak={false} />;
|
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} quotelinkNumber={number} isOP={isOP} showTrailingBreak={false} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string, subplebbitAddress?: string) => {
|
const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string, subplebbitAddress?: string) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Comment, useAccount } from '@bitsocialhq/bitsocial-react-hooks';
|
|||||||
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||||
import { useDirectories } from '../../hooks/use-directories';
|
import { useDirectories } from '../../hooks/use-directories';
|
||||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
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 useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import styles from '../../views/post/post.module.css';
|
import styles from '../../views/post/post.module.css';
|
||||||
import { Post } from '../../views/post';
|
import { Post } from '../../views/post';
|
||||||
@@ -209,7 +209,9 @@ const DesktopQuotePreview = ({
|
|||||||
const resolvedQuotelinkNumber = quotelinkReply?.number ?? quotelinkNumber;
|
const resolvedQuotelinkNumber = quotelinkReply?.number ?? quotelinkNumber;
|
||||||
const resolvedQuotelinkCid = quotelinkReply?.cid;
|
const resolvedQuotelinkCid = quotelinkReply?.cid;
|
||||||
const resolvedQuotelinkSubplebbitAddress = quotelinkReply?.subplebbitAddress;
|
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 quotelinkClassName = quotelinkUnavailable ? `${styles.quoteLink} ${styles.quoteLinkUnavailable}` : styles.quoteLink;
|
||||||
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, directories) : undefined;
|
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, directories) : undefined;
|
||||||
const quotelinkRoute = quotelinkReply?.cid ? (quotelinkBoardPath ? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}` : `/thread/${quotelinkReply.cid}`) : '#';
|
const quotelinkRoute = quotelinkReply?.cid ? (quotelinkBoardPath ? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}` : `/thread/${quotelinkReply.cid}`) : '#';
|
||||||
@@ -231,6 +233,8 @@ const DesktopQuotePreview = ({
|
|||||||
<>
|
<>
|
||||||
{quotelinkUnavailable ? (
|
{quotelinkUnavailable ? (
|
||||||
<span className={quotelinkClassName}>{quotelinkLabel}</span>
|
<span className={quotelinkClassName}>{quotelinkLabel}</span>
|
||||||
|
) : quotelinkPendingResolution ? (
|
||||||
|
<span className={styles.quoteLink}>{quotelinkLabel}</span>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
to={quotelinkRoute}
|
to={quotelinkRoute}
|
||||||
@@ -363,7 +367,9 @@ const MobileQuotePreview = ({
|
|||||||
const resolvedQuotelinkNumber = quotelinkReply?.number ?? quotelinkNumber;
|
const resolvedQuotelinkNumber = quotelinkReply?.number ?? quotelinkNumber;
|
||||||
const resolvedQuotelinkCid = quotelinkReply?.cid;
|
const resolvedQuotelinkCid = quotelinkReply?.cid;
|
||||||
const resolvedQuotelinkSubplebbitAddress = quotelinkReply?.subplebbitAddress;
|
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 quotelinkClassName = quotelinkUnavailable ? `${styles.quoteLink} ${styles.quoteLinkUnavailable}` : styles.quoteLink;
|
||||||
const shouldShowQuotelinkPreview = shouldShowFloatingQuotePreview({
|
const shouldShowQuotelinkPreview = shouldShowFloatingQuotePreview({
|
||||||
hoveredCid,
|
hoveredCid,
|
||||||
@@ -375,16 +381,17 @@ const MobileQuotePreview = ({
|
|||||||
const replyQuotelink = (
|
const replyQuotelink = (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
ref={quotelinkUnavailable ? undefined : refs.setReference}
|
ref={quotelinkUnavailable || quotelinkPendingResolution ? undefined : refs.setReference}
|
||||||
className={quotelinkClassName}
|
className={quotelinkPendingResolution ? styles.quoteLink : quotelinkClassName}
|
||||||
onMouseOver={quotelinkUnavailable ? undefined : () => handleMouseOver(resolvedQuotelinkCid)}
|
onMouseOver={quotelinkUnavailable || quotelinkPendingResolution ? undefined : () => handleMouseOver(resolvedQuotelinkCid)}
|
||||||
onMouseLeave={quotelinkUnavailable ? undefined : () => handleMouseLeave(resolvedQuotelinkCid)}
|
onMouseLeave={quotelinkUnavailable || quotelinkPendingResolution ? undefined : () => handleMouseLeave(resolvedQuotelinkCid)}
|
||||||
>
|
>
|
||||||
{formatQuoteNumber(resolvedQuotelinkNumber)}
|
{formatQuoteNumber(resolvedQuotelinkNumber)}
|
||||||
{isOP && ' (OP)'}
|
{isOP && ' (OP)'}
|
||||||
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
||||||
</span>
|
</span>
|
||||||
{!quotelinkUnavailable &&
|
{!quotelinkUnavailable &&
|
||||||
|
!quotelinkPendingResolution &&
|
||||||
resolvedQuotelinkNumber &&
|
resolvedQuotelinkNumber &&
|
||||||
(() => {
|
(() => {
|
||||||
const quotelinkBoardPath = resolvedQuotelinkSubplebbitAddress ? getBoardPath(resolvedQuotelinkSubplebbitAddress, directories) : undefined;
|
const quotelinkBoardPath = resolvedQuotelinkSubplebbitAddress ? getBoardPath(resolvedQuotelinkSubplebbitAddress, directories) : undefined;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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', () => {
|
describe('quote-link-utils', () => {
|
||||||
it('formats quote numbers with the expected prefix', () => {
|
it('formats quote numbers with the expected prefix', () => {
|
||||||
@@ -7,6 +7,13 @@ describe('quote-link-utils', () => {
|
|||||||
expect(formatQuoteNumber()).toBe('>>?');
|
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', () => {
|
it('marks deleted and removed comments as unavailable quote targets', () => {
|
||||||
expect(isUnavailableQuoteTarget(undefined)).toBe(false);
|
expect(isUnavailableQuoteTarget(undefined)).toBe(false);
|
||||||
expect(isUnavailableQuoteTarget({ deleted: true, removed: false })).toBe(true);
|
expect(isUnavailableQuoteTarget({ deleted: true, removed: false })).toBe(true);
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import type { Comment } from '@bitsocialhq/bitsocial-react-hooks';
|
import type { Comment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||||
|
|
||||||
|
type QuoteTargetAvailability = 'available' | 'unresolved' | 'unavailable';
|
||||||
|
|
||||||
export const formatQuoteNumber = (number?: number) => `>>${number ?? '?'}`;
|
export const formatQuoteNumber = (number?: number) => `>>${number ?? '?'}`;
|
||||||
|
|
||||||
export const isUnavailableQuoteTarget = (comment?: Partial<Pick<Comment, 'deleted' | 'removed'>> | null) => Boolean(comment?.deleted || comment?.removed);
|
export const getQuoteTargetAvailability = (comment?: Partial<Pick<Comment, 'deleted' | 'removed'>> | null): QuoteTargetAvailability => {
|
||||||
|
if (!comment) {
|
||||||
|
return 'unresolved';
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment.deleted || comment.removed ? 'unavailable' : 'available';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isUnavailableQuoteTarget = (comment?: Partial<Pick<Comment, 'deleted' | 'removed'>> | null) => getQuoteTargetAvailability(comment) === 'unavailable';
|
||||||
|
|
||||||
export const shouldShowFloatingQuotePreview = ({
|
export const shouldShowFloatingQuotePreview = ({
|
||||||
hoveredCid,
|
hoveredCid,
|
||||||
|
|||||||
Reference in New Issue
Block a user