mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(markdown): handle unavailable reply quote links
This commit is contained in:
@@ -17,12 +17,15 @@ import styles from '../../views/post/post.module.css';
|
|||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
|
||||||
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
||||||
|
const quotedNumber = usePostNumberStore((state) => state.cidToNumber[cid]);
|
||||||
const commentFromStore = useSubplebbitsPagesStore((state) => state.comments[cid]);
|
const commentFromStore = useSubplebbitsPagesStore((state) => state.comments[cid]);
|
||||||
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
|
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
|
||||||
// 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;
|
||||||
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} isOP={isOP} />;
|
const isUnavailable = !quotedComment || quotedComment.deleted || quotedComment.removed;
|
||||||
|
|
||||||
|
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} quotelinkNumber={quotedNumber} isQuotelinkUnavailable={isUnavailable} isOP={isOP} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useScopedCidToNumber = (cids: string[]) => {
|
const useScopedCidToNumber = (cids: string[]) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import styles from './markdown.module.css';
|
|||||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||||
import { canEmbed } from '../embed';
|
import { canEmbed } from '../embed';
|
||||||
import { is5chanLink, transform5chanLinkToInternal, isValidCrossboardPattern } from '../../lib/utils/url-utils';
|
import { is5chanLink, transform5chanLinkToInternal, isValidCrossboardPattern } from '../../lib/utils/url-utils';
|
||||||
|
import { isUnavailableQuoteTarget } from '../../lib/utils/quote-link-utils';
|
||||||
import usePostNumberStore from '../../stores/use-post-number-store';
|
import usePostNumberStore from '../../stores/use-post-number-store';
|
||||||
import useSubplebbitsPagesStore from '@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits-pages';
|
import useSubplebbitsPagesStore from '@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits-pages';
|
||||||
import { useComment } from '@bitsocialhq/bitsocial-react-hooks';
|
import { useComment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||||
@@ -276,8 +277,10 @@ 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) {
|
if (!comment || isUnavailableQuoteTarget(comment)) {
|
||||||
return <span>{`>>${number}`}</span>;
|
return (
|
||||||
|
<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} isOP={isOP} showTrailingBreak={false} />;
|
||||||
|
|||||||
@@ -5,6 +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 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';
|
||||||
@@ -14,6 +15,8 @@ interface ReplyQuotePreviewProps {
|
|||||||
backlinkReply?: Comment;
|
backlinkReply?: Comment;
|
||||||
isQuotelinkReply?: boolean;
|
isQuotelinkReply?: boolean;
|
||||||
quotelinkReply?: Comment;
|
quotelinkReply?: Comment;
|
||||||
|
quotelinkNumber?: number;
|
||||||
|
isQuotelinkUnavailable?: boolean;
|
||||||
isOP?: boolean;
|
isOP?: boolean;
|
||||||
showTrailingBreak?: boolean;
|
showTrailingBreak?: boolean;
|
||||||
}
|
}
|
||||||
@@ -80,7 +83,16 @@ const scrollToReplyOnPage = (cid: string) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP, showTrailingBreak = true }: ReplyQuotePreviewProps) => {
|
const DesktopQuotePreview = ({
|
||||||
|
backlinkReply,
|
||||||
|
quotelinkReply,
|
||||||
|
quotelinkNumber,
|
||||||
|
isBacklinkReply,
|
||||||
|
isQuotelinkReply,
|
||||||
|
isQuotelinkUnavailable,
|
||||||
|
isOP,
|
||||||
|
showTrailingBreak = true,
|
||||||
|
}: ReplyQuotePreviewProps) => {
|
||||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||||
const [placement, setPlacement] = useState<Placement>('right');
|
const [placement, setPlacement] = useState<Placement>('right');
|
||||||
@@ -194,26 +206,45 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
|||||||
|
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
|
|
||||||
|
const resolvedQuotelinkNumber = quotelinkReply?.number ?? quotelinkNumber;
|
||||||
|
const resolvedQuotelinkCid = quotelinkReply?.cid;
|
||||||
|
const resolvedQuotelinkSubplebbitAddress = quotelinkReply?.subplebbitAddress;
|
||||||
|
const quotelinkUnavailable = Boolean(isQuotelinkUnavailable || isUnavailableQuoteTarget(quotelinkReply));
|
||||||
|
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}`) : '#';
|
||||||
|
const shouldShowQuotelinkPreview = shouldShowFloatingQuotePreview({
|
||||||
|
hoveredCid,
|
||||||
|
outOfViewCid,
|
||||||
|
quoteCid: resolvedQuotelinkCid,
|
||||||
|
isUnavailable: quotelinkUnavailable,
|
||||||
|
});
|
||||||
|
const quotelinkLabel = (
|
||||||
|
<>
|
||||||
|
{formatQuoteNumber(resolvedQuotelinkNumber)}
|
||||||
|
{isOP && ' (OP)'}
|
||||||
|
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const replyQuotelink = (
|
const replyQuotelink = (
|
||||||
<>
|
<>
|
||||||
<Link
|
{quotelinkUnavailable ? (
|
||||||
to={quotelinkRoute}
|
<span className={quotelinkClassName}>{quotelinkLabel}</span>
|
||||||
ref={refs.setReference}
|
) : (
|
||||||
className={styles.quoteLink}
|
<Link
|
||||||
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
|
to={quotelinkRoute}
|
||||||
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
|
ref={refs.setReference}
|
||||||
onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress, !!isOP)}
|
className={quotelinkClassName}
|
||||||
>
|
onMouseOver={() => handleMouseOver(resolvedQuotelinkCid)}
|
||||||
{`>>${quotelinkReply?.number ?? '?'}`}
|
onMouseLeave={() => handleMouseLeave(resolvedQuotelinkCid)}
|
||||||
{isOP && ' (OP)'}
|
onClick={(e) => handleClick(e, resolvedQuotelinkCid, resolvedQuotelinkSubplebbitAddress, !!isOP)}
|
||||||
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
>
|
||||||
</Link>
|
{quotelinkLabel}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
{showTrailingBreak && <br />}
|
{showTrailingBreak && <br />}
|
||||||
{hoveredCid === quotelinkReply?.cid &&
|
{shouldShowQuotelinkPreview &&
|
||||||
outOfViewCid === quotelinkReply?.cid &&
|
|
||||||
createPortal(
|
createPortal(
|
||||||
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||||
<Post post={quotelinkReply} showReplies={false} />
|
<Post post={quotelinkReply} showReplies={false} />
|
||||||
@@ -226,7 +257,16 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
|||||||
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP, showTrailingBreak = true }: ReplyQuotePreviewProps) => {
|
const MobileQuotePreview = ({
|
||||||
|
backlinkReply,
|
||||||
|
quotelinkReply,
|
||||||
|
quotelinkNumber,
|
||||||
|
isBacklinkReply,
|
||||||
|
isQuotelinkReply,
|
||||||
|
isQuotelinkUnavailable,
|
||||||
|
isOP,
|
||||||
|
showTrailingBreak = true,
|
||||||
|
}: ReplyQuotePreviewProps) => {
|
||||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
@@ -320,37 +360,48 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
|||||||
);
|
);
|
||||||
|
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
|
const resolvedQuotelinkNumber = quotelinkReply?.number ?? quotelinkNumber;
|
||||||
|
const resolvedQuotelinkCid = quotelinkReply?.cid;
|
||||||
|
const resolvedQuotelinkSubplebbitAddress = quotelinkReply?.subplebbitAddress;
|
||||||
|
const quotelinkUnavailable = Boolean(isQuotelinkUnavailable || isUnavailableQuoteTarget(quotelinkReply));
|
||||||
|
const quotelinkClassName = quotelinkUnavailable ? `${styles.quoteLink} ${styles.quoteLinkUnavailable}` : styles.quoteLink;
|
||||||
|
const shouldShowQuotelinkPreview = shouldShowFloatingQuotePreview({
|
||||||
|
hoveredCid,
|
||||||
|
outOfViewCid,
|
||||||
|
quoteCid: resolvedQuotelinkCid,
|
||||||
|
isUnavailable: quotelinkUnavailable,
|
||||||
|
});
|
||||||
|
|
||||||
const replyQuotelink = (
|
const replyQuotelink = (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
ref={refs.setReference}
|
ref={quotelinkUnavailable ? undefined : refs.setReference}
|
||||||
className={styles.quoteLink}
|
className={quotelinkClassName}
|
||||||
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
|
onMouseOver={quotelinkUnavailable ? undefined : () => handleMouseOver(resolvedQuotelinkCid)}
|
||||||
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
|
onMouseLeave={quotelinkUnavailable ? undefined : () => handleMouseLeave(resolvedQuotelinkCid)}
|
||||||
>
|
>
|
||||||
{`>>${quotelinkReply?.number ?? '?'}`}
|
{formatQuoteNumber(resolvedQuotelinkNumber)}
|
||||||
{isOP && ' (OP)'}
|
{isOP && ' (OP)'}
|
||||||
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
||||||
</span>
|
</span>
|
||||||
{quotelinkReply?.number &&
|
{!quotelinkUnavailable &&
|
||||||
|
resolvedQuotelinkNumber &&
|
||||||
(() => {
|
(() => {
|
||||||
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, directories) : undefined;
|
const quotelinkBoardPath = resolvedQuotelinkSubplebbitAddress ? getBoardPath(resolvedQuotelinkSubplebbitAddress, directories) : undefined;
|
||||||
const quotelinkRoute = quotelinkReply?.cid
|
const quotelinkRoute = resolvedQuotelinkCid
|
||||||
? quotelinkBoardPath
|
? quotelinkBoardPath
|
||||||
? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}`
|
? `/${quotelinkBoardPath}/thread/${resolvedQuotelinkCid}`
|
||||||
: `/thread/${quotelinkReply.cid}`
|
: `/thread/${resolvedQuotelinkCid}`
|
||||||
: '#';
|
: '#';
|
||||||
return (
|
return (
|
||||||
<Link className={styles.quoteLink} to={quotelinkRoute} onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress, !!isOP)}>
|
<Link className={quotelinkClassName} to={quotelinkRoute} onClick={(e) => handleClick(e, resolvedQuotelinkCid, resolvedQuotelinkSubplebbitAddress, !!isOP)}>
|
||||||
{' '}
|
{' '}
|
||||||
#
|
#
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{showTrailingBreak && <br />}
|
{showTrailingBreak && <br />}
|
||||||
{hoveredCid === quotelinkReply?.cid &&
|
{shouldShowQuotelinkPreview &&
|
||||||
outOfViewCid === quotelinkReply?.cid &&
|
|
||||||
createPortal(
|
createPortal(
|
||||||
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||||
<Post post={quotelinkReply} showReplies={false} />
|
<Post post={quotelinkReply} showReplies={false} />
|
||||||
@@ -363,15 +414,26 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
|||||||
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
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();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return isMobile ? (
|
return isMobile ? (
|
||||||
<MobileQuotePreview
|
<MobileQuotePreview
|
||||||
backlinkReply={backlinkReply}
|
backlinkReply={backlinkReply}
|
||||||
quotelinkReply={quotelinkReply}
|
quotelinkReply={quotelinkReply}
|
||||||
|
quotelinkNumber={quotelinkNumber}
|
||||||
isBacklinkReply={isBacklinkReply}
|
isBacklinkReply={isBacklinkReply}
|
||||||
isQuotelinkReply={isQuotelinkReply}
|
isQuotelinkReply={isQuotelinkReply}
|
||||||
|
isQuotelinkUnavailable={isQuotelinkUnavailable}
|
||||||
isOP={isOP}
|
isOP={isOP}
|
||||||
showTrailingBreak={showTrailingBreak}
|
showTrailingBreak={showTrailingBreak}
|
||||||
/>
|
/>
|
||||||
@@ -379,8 +441,10 @@ const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQ
|
|||||||
<DesktopQuotePreview
|
<DesktopQuotePreview
|
||||||
backlinkReply={backlinkReply}
|
backlinkReply={backlinkReply}
|
||||||
quotelinkReply={quotelinkReply}
|
quotelinkReply={quotelinkReply}
|
||||||
|
quotelinkNumber={quotelinkNumber}
|
||||||
isBacklinkReply={isBacklinkReply}
|
isBacklinkReply={isBacklinkReply}
|
||||||
isQuotelinkReply={isQuotelinkReply}
|
isQuotelinkReply={isQuotelinkReply}
|
||||||
|
isQuotelinkUnavailable={isQuotelinkUnavailable}
|
||||||
isOP={isOP}
|
isOP={isOP}
|
||||||
showTrailingBreak={showTrailingBreak}
|
showTrailingBreak={showTrailingBreak}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Comment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||||
|
|
||||||
|
export const formatQuoteNumber = (number?: number) => `>>${number ?? '?'}`;
|
||||||
|
|
||||||
|
export const isUnavailableQuoteTarget = (comment?: Partial<Pick<Comment, 'deleted' | 'removed'>> | 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);
|
||||||
@@ -490,6 +490,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quoteLinkUnavailable,
|
||||||
|
.quoteLinkUnavailable:hover {
|
||||||
|
color: var(--post-quotelink-text-color);
|
||||||
|
text-decoration: line-through;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.replyMobile {
|
.replyMobile {
|
||||||
padding-top: 7px;
|
padding-top: 7px;
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user