mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(quotes): keep same-thread board previews local
This commit is contained in:
@@ -15,7 +15,13 @@ type TestComment = {
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
comments: {} as Record<string, TestComment>,
|
||||
directories: [{ address: 'music-posting.eth', name: 'music-posting.bso', title: '/mu/ - Music' }] as Array<{
|
||||
address: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
}>,
|
||||
embeddableHosts: new Set<string>(),
|
||||
cidToNumber: {} as Record<string, number>,
|
||||
internalPathByHref: {} as Record<string, string | null>,
|
||||
isMobile: false,
|
||||
mediaInfoByHref: {} as Record<string, { thumbnail?: string; type: string; url: string }>,
|
||||
@@ -88,9 +94,46 @@ vi.mock('../../../lib/utils/url-utils', () => ({
|
||||
transform5chanLinkToInternal: (href: string) => testState.internalPathByHref[href] ?? null,
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/use-directories', () => ({
|
||||
findDirectoryByAddress: (directories: Array<{ address: string; name?: string }>, address: string | undefined) => {
|
||||
if (!address) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalize = (value: string) => value.replace(/(\.bso|\.eth)$/, '');
|
||||
return directories.find((directory) => [directory.address, directory.name].some((identifier) => identifier && normalize(identifier) === normalize(address)));
|
||||
},
|
||||
useDirectories: () => testState.directories,
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores/use-post-number-store', () => ({
|
||||
default: (selector: (state: { numberToCid: typeof testState.numberToCid }) => unknown) =>
|
||||
getCidForPostNumber: (numberToCid: typeof testState.numberToCid, communityAddress: string | undefined, postNumber: number) => {
|
||||
if (!communityAddress) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalize = (value: string) => value.replace(/(\.bso|\.eth)$/, '');
|
||||
const exactMatch = numberToCid[communityAddress];
|
||||
const matchingEntries = Object.entries(numberToCid).filter(([address]) => normalize(address) === normalize(communityAddress));
|
||||
const scoped =
|
||||
exactMatch && matchingEntries.length <= 1
|
||||
? exactMatch
|
||||
: matchingEntries.reduce<Record<number, string>>(
|
||||
(mergedMap, [address, scopedMap]) => {
|
||||
if (address === communityAddress) {
|
||||
return { ...mergedMap, ...scopedMap };
|
||||
}
|
||||
|
||||
return { ...scopedMap, ...mergedMap };
|
||||
},
|
||||
exactMatch ? { ...exactMatch } : {},
|
||||
);
|
||||
|
||||
return scoped?.[postNumber];
|
||||
},
|
||||
default: (selector: (state: { cidToNumber: typeof testState.cidToNumber; numberToCid: typeof testState.numberToCid }) => unknown) =>
|
||||
selector({
|
||||
cidToNumber: testState.cidToNumber,
|
||||
numberToCid: testState.numberToCid,
|
||||
}),
|
||||
}));
|
||||
@@ -129,7 +172,8 @@ vi.mock('../../reply-quote-preview', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../external-number-quote-link', () => ({
|
||||
default: ({ reference }: { reference: { raw: string } }) => createElement('a', { 'data-testid': 'external-number-quote-link', href: '#' }, reference.raw),
|
||||
default: ({ isOP, reference }: { isOP?: boolean; reference: { raw: string } }) =>
|
||||
createElement('a', { 'data-op': String(Boolean(isOP)), 'data-testid': 'external-number-quote-link', href: '#' }, `${reference.raw}${isOP ? ' (OP)' : ''}`),
|
||||
}));
|
||||
|
||||
let container: HTMLDivElement;
|
||||
@@ -145,7 +189,9 @@ describe('Markdown', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testState.comments = {};
|
||||
testState.directories = [{ address: 'music-posting.eth', name: 'music-posting.bso', title: '/mu/ - Music' }];
|
||||
testState.embeddableHosts = new Set<string>();
|
||||
testState.cidToNumber = {};
|
||||
testState.internalPathByHref = {};
|
||||
testState.isMobile = false;
|
||||
testState.mediaInfoByHref = {};
|
||||
@@ -188,6 +234,9 @@ describe('Markdown', () => {
|
||||
testState.comments = {
|
||||
'comment-42': { cid: 'comment-42', number: 42 },
|
||||
};
|
||||
testState.cidToNumber = {
|
||||
'comment-42': 42,
|
||||
};
|
||||
testState.numberToCid = {
|
||||
'music-posting.eth': {
|
||||
42: 'comment-42',
|
||||
@@ -208,6 +257,80 @@ describe('Markdown', () => {
|
||||
expect(quotePreview?.textContent).toBe('comment-42');
|
||||
});
|
||||
|
||||
it('resolves same-board OP quotes across alias-scoped post number store entries', async () => {
|
||||
testState.comments = {
|
||||
'thread-cid': { cid: 'thread-cid', number: 42 },
|
||||
};
|
||||
testState.cidToNumber = {
|
||||
'thread-cid': 42,
|
||||
};
|
||||
testState.numberToCid = {
|
||||
'music-posting.eth': {
|
||||
42: 'thread-cid',
|
||||
},
|
||||
};
|
||||
testState.directories = [{ address: 'music-posting.bso', title: '/mu/ - Music' }];
|
||||
|
||||
await renderMarkdown({
|
||||
content: '>>42',
|
||||
postCid: 'thread-cid',
|
||||
communityAddress: 'music-posting.bso',
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="external-number-quote-link"]')).toBeNull();
|
||||
const quotePreview = container.querySelector('[data-testid="reply-quote-preview"]');
|
||||
expect(quotePreview?.getAttribute('data-number')).toBe('42');
|
||||
expect(quotePreview?.getAttribute('data-op')).toBe('true');
|
||||
expect(quotePreview?.textContent).toBe('thread-cid');
|
||||
});
|
||||
|
||||
it('resolves board-preview quotes when the exact alias scope exists but another alias owns the quoted number', async () => {
|
||||
testState.comments = {
|
||||
'thread-cid': { cid: 'thread-cid', number: 3 },
|
||||
};
|
||||
testState.cidToNumber = {
|
||||
'thread-cid': 3,
|
||||
};
|
||||
testState.numberToCid = {
|
||||
'music-posting.eth': {
|
||||
3: 'thread-cid',
|
||||
6: 'reply-cid',
|
||||
},
|
||||
'music-posting.bso': {
|
||||
28: 'later-reply-cid',
|
||||
},
|
||||
};
|
||||
|
||||
await renderMarkdown({
|
||||
content: '>>3',
|
||||
postCid: 'thread-cid',
|
||||
communityAddress: 'music-posting.bso',
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="external-number-quote-link"]')).toBeNull();
|
||||
const quotePreview = container.querySelector('[data-testid="reply-quote-preview"]');
|
||||
expect(quotePreview?.getAttribute('data-number')).toBe('3');
|
||||
expect(quotePreview?.getAttribute('data-op')).toBe('true');
|
||||
expect(quotePreview?.textContent).toBe('thread-cid');
|
||||
});
|
||||
|
||||
it('preserves the OP label when a same-board quote still falls back to external resolution', async () => {
|
||||
testState.cidToNumber = {
|
||||
'thread-cid': 42,
|
||||
};
|
||||
testState.directories = [{ address: 'music-posting.bso', title: '/mu/ - Music' }];
|
||||
|
||||
await renderMarkdown({
|
||||
content: '>>42',
|
||||
postCid: 'thread-cid',
|
||||
communityAddress: 'music-posting.bso',
|
||||
});
|
||||
|
||||
const externalLink = container.querySelector('[data-testid="external-number-quote-link"]');
|
||||
expect(externalLink?.getAttribute('data-op')).toBe('true');
|
||||
expect(externalLink?.textContent).toBe('>>42 (OP)');
|
||||
});
|
||||
|
||||
it('renders lazy same-board and cross-board number quotes when the cid is not cached', async () => {
|
||||
await renderMarkdown({
|
||||
content: '>>42 >>>/fit/77',
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Post } from '../../views/post';
|
||||
import styles from './markdown.module.css';
|
||||
|
||||
interface ExternalNumberQuoteLinkProps {
|
||||
isOP?: boolean;
|
||||
reference: ExternalQuoteReference;
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ type PreviewPosition = {
|
||||
top: number;
|
||||
};
|
||||
|
||||
const ExternalNumberQuoteLink = ({ reference }: ExternalNumberQuoteLinkProps) => {
|
||||
const ExternalNumberQuoteLink = ({ isOP = false, reference }: ExternalNumberQuoteLinkProps) => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount();
|
||||
const directories = useDirectories();
|
||||
@@ -61,6 +62,7 @@ const ExternalNumberQuoteLink = ({ reference }: ExternalNumberQuoteLinkProps) =>
|
||||
const latestStatusMessageRef = useRef('');
|
||||
|
||||
const boardLabel = getExternalQuoteBoardLabel(reference, directories);
|
||||
const linkLabel = isOP ? `${reference.raw} (OP)` : reference.raw;
|
||||
|
||||
const updatePreviewPosition = (anchor: HTMLElement | null) => {
|
||||
if (!anchor || isMobile) {
|
||||
@@ -276,7 +278,7 @@ const ExternalNumberQuoteLink = ({ reference }: ExternalNumberQuoteLinkProps) =>
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={anchorRef}
|
||||
>
|
||||
{reference.raw}
|
||||
{linkLabel}
|
||||
</a>
|
||||
{!isMobile &&
|
||||
isPreviewOpen &&
|
||||
|
||||
@@ -12,7 +12,7 @@ import { canEmbed } from '../embed';
|
||||
import { is5chanLink, transform5chanLinkToInternal, isValidCrossboardPattern } from '../../lib/utils/url-utils';
|
||||
import { CROSSBOARD_NUMBER_QUOTE_TOKEN_REGEX, type ExternalQuoteReference } from '../../lib/utils/external-quote-utils';
|
||||
import { isUnavailableQuoteTarget } from '../../lib/utils/quote-link-utils';
|
||||
import usePostNumberStore from '../../stores/use-post-number-store';
|
||||
import usePostNumberStore, { getCidForPostNumber } from '../../stores/use-post-number-store';
|
||||
import useCommunitiesPagesStore from '@bitsocialnet/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||
import { useComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
@@ -299,11 +299,12 @@ interface MarkdownProps {
|
||||
}
|
||||
|
||||
const NumberQuoteLink = ({ number, threadPostCid, communityAddress }: { number: number; threadPostCid?: string; communityAddress?: string }) => {
|
||||
const cid = usePostNumberStore((state) => (communityAddress ? state.numberToCid[communityAddress]?.[number] : undefined));
|
||||
const cid = usePostNumberStore((state) => getCidForPostNumber(state.numberToCid, communityAddress, number));
|
||||
const threadPostNumber = usePostNumberStore((state) => (threadPostCid ? state.cidToNumber[threadPostCid] : undefined));
|
||||
const commentFromStore = useCommunitiesPagesStore((state) => (cid ? state.comments[cid] : undefined));
|
||||
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
|
||||
const comment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
|
||||
const isOP = Boolean(threadPostCid && cid === threadPostCid);
|
||||
const isOP = Boolean((threadPostCid && cid === threadPostCid) || (threadPostNumber !== undefined && number === threadPostNumber));
|
||||
|
||||
if (isUnavailableQuoteTarget(comment)) {
|
||||
return (
|
||||
@@ -314,11 +315,12 @@ const NumberQuoteLink = ({ number, threadPostCid, communityAddress }: { number:
|
||||
if (!cid && communityAddress) {
|
||||
return (
|
||||
<ExternalNumberQuoteLink
|
||||
isOP={isOP}
|
||||
reference={{
|
||||
kind: 'same-board',
|
||||
number,
|
||||
raw: `>>${number}`,
|
||||
subplebbitAddress: communityAddress,
|
||||
communityAddress,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user