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(() => ({
|
const testState = vi.hoisted(() => ({
|
||||||
comments: {} as Record<string, TestComment>,
|
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>(),
|
embeddableHosts: new Set<string>(),
|
||||||
|
cidToNumber: {} as Record<string, number>,
|
||||||
internalPathByHref: {} as Record<string, string | null>,
|
internalPathByHref: {} as Record<string, string | null>,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
mediaInfoByHref: {} as Record<string, { thumbnail?: string; type: string; url: string }>,
|
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,
|
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', () => ({
|
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({
|
selector({
|
||||||
|
cidToNumber: testState.cidToNumber,
|
||||||
numberToCid: testState.numberToCid,
|
numberToCid: testState.numberToCid,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
@@ -129,7 +172,8 @@ vi.mock('../../reply-quote-preview', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../external-number-quote-link', () => ({
|
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;
|
let container: HTMLDivElement;
|
||||||
@@ -145,7 +189,9 @@ describe('Markdown', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
testState.comments = {};
|
testState.comments = {};
|
||||||
|
testState.directories = [{ address: 'music-posting.eth', name: 'music-posting.bso', title: '/mu/ - Music' }];
|
||||||
testState.embeddableHosts = new Set<string>();
|
testState.embeddableHosts = new Set<string>();
|
||||||
|
testState.cidToNumber = {};
|
||||||
testState.internalPathByHref = {};
|
testState.internalPathByHref = {};
|
||||||
testState.isMobile = false;
|
testState.isMobile = false;
|
||||||
testState.mediaInfoByHref = {};
|
testState.mediaInfoByHref = {};
|
||||||
@@ -188,6 +234,9 @@ describe('Markdown', () => {
|
|||||||
testState.comments = {
|
testState.comments = {
|
||||||
'comment-42': { cid: 'comment-42', number: 42 },
|
'comment-42': { cid: 'comment-42', number: 42 },
|
||||||
};
|
};
|
||||||
|
testState.cidToNumber = {
|
||||||
|
'comment-42': 42,
|
||||||
|
};
|
||||||
testState.numberToCid = {
|
testState.numberToCid = {
|
||||||
'music-posting.eth': {
|
'music-posting.eth': {
|
||||||
42: 'comment-42',
|
42: 'comment-42',
|
||||||
@@ -208,6 +257,80 @@ describe('Markdown', () => {
|
|||||||
expect(quotePreview?.textContent).toBe('comment-42');
|
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 () => {
|
it('renders lazy same-board and cross-board number quotes when the cid is not cached', async () => {
|
||||||
await renderMarkdown({
|
await renderMarkdown({
|
||||||
content: '>>42 >>>/fit/77',
|
content: '>>42 >>>/fit/77',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Post } from '../../views/post';
|
|||||||
import styles from './markdown.module.css';
|
import styles from './markdown.module.css';
|
||||||
|
|
||||||
interface ExternalNumberQuoteLinkProps {
|
interface ExternalNumberQuoteLinkProps {
|
||||||
|
isOP?: boolean;
|
||||||
reference: ExternalQuoteReference;
|
reference: ExternalQuoteReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ type PreviewPosition = {
|
|||||||
top: number;
|
top: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExternalNumberQuoteLink = ({ reference }: ExternalNumberQuoteLinkProps) => {
|
const ExternalNumberQuoteLink = ({ isOP = false, reference }: ExternalNumberQuoteLinkProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
@@ -61,6 +62,7 @@ const ExternalNumberQuoteLink = ({ reference }: ExternalNumberQuoteLinkProps) =>
|
|||||||
const latestStatusMessageRef = useRef('');
|
const latestStatusMessageRef = useRef('');
|
||||||
|
|
||||||
const boardLabel = getExternalQuoteBoardLabel(reference, directories);
|
const boardLabel = getExternalQuoteBoardLabel(reference, directories);
|
||||||
|
const linkLabel = isOP ? `${reference.raw} (OP)` : reference.raw;
|
||||||
|
|
||||||
const updatePreviewPosition = (anchor: HTMLElement | null) => {
|
const updatePreviewPosition = (anchor: HTMLElement | null) => {
|
||||||
if (!anchor || isMobile) {
|
if (!anchor || isMobile) {
|
||||||
@@ -276,7 +278,7 @@ const ExternalNumberQuoteLink = ({ reference }: ExternalNumberQuoteLinkProps) =>
|
|||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
ref={anchorRef}
|
ref={anchorRef}
|
||||||
>
|
>
|
||||||
{reference.raw}
|
{linkLabel}
|
||||||
</a>
|
</a>
|
||||||
{!isMobile &&
|
{!isMobile &&
|
||||||
isPreviewOpen &&
|
isPreviewOpen &&
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { canEmbed } from '../embed';
|
|||||||
import { is5chanLink, transform5chanLinkToInternal, isValidCrossboardPattern } from '../../lib/utils/url-utils';
|
import { is5chanLink, transform5chanLinkToInternal, isValidCrossboardPattern } from '../../lib/utils/url-utils';
|
||||||
import { CROSSBOARD_NUMBER_QUOTE_TOKEN_REGEX, type ExternalQuoteReference } from '../../lib/utils/external-quote-utils';
|
import { CROSSBOARD_NUMBER_QUOTE_TOKEN_REGEX, type ExternalQuoteReference } from '../../lib/utils/external-quote-utils';
|
||||||
import { isUnavailableQuoteTarget } from '../../lib/utils/quote-link-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 useCommunitiesPagesStore from '@bitsocialnet/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||||
import { useComment } from '@bitsocialnet/bitsocial-react-hooks';
|
import { useComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import ReplyQuotePreview from '../reply-quote-preview';
|
import ReplyQuotePreview from '../reply-quote-preview';
|
||||||
@@ -299,11 +299,12 @@ interface MarkdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NumberQuoteLink = ({ number, threadPostCid, communityAddress }: { number: number; threadPostCid?: string; communityAddress?: string }) => {
|
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 commentFromStore = useCommunitiesPagesStore((state) => (cid ? state.comments[cid] : undefined));
|
||||||
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
|
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
|
||||||
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) || (threadPostNumber !== undefined && number === threadPostNumber));
|
||||||
|
|
||||||
if (isUnavailableQuoteTarget(comment)) {
|
if (isUnavailableQuoteTarget(comment)) {
|
||||||
return (
|
return (
|
||||||
@@ -314,11 +315,12 @@ const NumberQuoteLink = ({ number, threadPostCid, communityAddress }: { number:
|
|||||||
if (!cid && communityAddress) {
|
if (!cid && communityAddress) {
|
||||||
return (
|
return (
|
||||||
<ExternalNumberQuoteLink
|
<ExternalNumberQuoteLink
|
||||||
|
isOP={isOP}
|
||||||
reference={{
|
reference={{
|
||||||
kind: 'same-board',
|
kind: 'same-board',
|
||||||
number,
|
number,
|
||||||
raw: `>>${number}`,
|
raw: `>>${number}`,
|
||||||
subplebbitAddress: communityAddress,
|
communityAddress,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
const resolvedAddress = useResolvedCommunityAddress();
|
const resolvedAddress = useResolvedCommunityAddress();
|
||||||
const communityAddress = resolvedAddress || accountComment?.communityAddress;
|
const communityAddress = resolvedAddress || accountComment?.communityAddress;
|
||||||
const { setPublishPostOptions, postIndex, publishPost, publishPostError, publishPostOptions, resetPublishPostOptions } = usePublishPost({
|
const { setPublishPostOptions, postIndex, publishPost, publishPostError, publishPostOptions, resetPublishPostOptions } = usePublishPost({
|
||||||
subplebbitAddress: communityAddress,
|
communityAddress,
|
||||||
});
|
});
|
||||||
const effectiveBoardAddress = communityAddress || publishPostOptions.communityAddress;
|
const effectiveBoardAddress = communityAddress || publishPostOptions.communityAddress;
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
const isInPostView = isPostPageView(location.pathname, params);
|
const isInPostView = isPostPageView(location.pathname, params);
|
||||||
const cid = params?.commentCid as string;
|
const cid = params?.commentCid as string;
|
||||||
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
||||||
usePublishReply({ cid, subplebbitAddress: communityAddress, postCid });
|
usePublishReply({ cid, communityAddress, postCid });
|
||||||
|
|
||||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const content = e.target.value;
|
const content = e.target.value;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
||||||
usePublishReply({
|
usePublishReply({
|
||||||
cid: parentCid,
|
cid: parentCid,
|
||||||
subplebbitAddress: communityAddress,
|
communityAddress,
|
||||||
postCid,
|
postCid,
|
||||||
});
|
});
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ let latestValue: ReturnType<typeof usePublishPost>;
|
|||||||
let root: Root;
|
let root: Root;
|
||||||
|
|
||||||
const HookHarness = () => {
|
const HookHarness = () => {
|
||||||
latestValue = usePublishPost({ subplebbitAddress: 'music.eth' });
|
latestValue = usePublishPost({ communityAddress: 'music.eth' });
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +92,6 @@ describe('usePublishPost', () => {
|
|||||||
spoiler: true,
|
spoiler: true,
|
||||||
title: 'Hello world',
|
title: 'Hello world',
|
||||||
});
|
});
|
||||||
expect('subplebbitAddress' in latestValue.publishPostOptions).toBe(false);
|
|
||||||
expect(typeof latestValue.publishPostOptions.onChallengeVerification).toBe('function');
|
expect(typeof latestValue.publishPostOptions.onChallengeVerification).toBe('function');
|
||||||
expect(typeof latestValue.publishPostOptions.onError).toBe('function');
|
expect(typeof latestValue.publishPostOptions.onError).toBe('function');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
|||||||
|
|
||||||
vi.mock('../../hooks/use-directories', () => ({
|
vi.mock('../../hooks/use-directories', () => ({
|
||||||
useDirectories: () => testState.directories,
|
useDirectories: () => testState.directories,
|
||||||
|
normalizeBoardAddress: (address?: string) => address?.replace(/(\.bso|\.eth)$/u, ''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../lib/utils/external-quote-resolver', () => ({
|
vi.mock('../../lib/utils/external-quote-resolver', () => ({
|
||||||
@@ -60,7 +61,7 @@ let latestValue: ReturnType<typeof usePublishReply>;
|
|||||||
let root: Root;
|
let root: Root;
|
||||||
|
|
||||||
const HookHarness = () => {
|
const HookHarness = () => {
|
||||||
latestValue = usePublishReply({ cid: 'parent-cid', subplebbitAddress: 'music.eth' });
|
latestValue = usePublishReply({ cid: 'parent-cid', communityAddress: 'music.eth' });
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,14 +123,13 @@ describe('usePublishReply', () => {
|
|||||||
quotedCids: ['quoted-cid'],
|
quotedCids: ['quoted-cid'],
|
||||||
spoiler: true,
|
spoiler: true,
|
||||||
});
|
});
|
||||||
expect('subplebbitAddress' in (testState.lastPublishOptions || {})).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves same-board external quote references before triggering publish', async () => {
|
it('resolves same-board external quote references before triggering publish', async () => {
|
||||||
testState.resolveExternalQuoteTargetMock.mockResolvedValue({
|
testState.resolveExternalQuoteTargetMock.mockResolvedValue({
|
||||||
cid: 'external-cid',
|
cid: 'external-cid',
|
||||||
route: '/music/thread/external-cid',
|
route: '/music/thread/external-cid',
|
||||||
subplebbitAddress: 'music.eth',
|
communityAddress: 'music.eth',
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -72,4 +72,32 @@ describe('useQuotedByMap', () => {
|
|||||||
|
|
||||||
expect(latestValue.get('op-cid')?.[0]?.number).toBe(42);
|
expect(latestValue.get('op-cid')?.[0]?.number).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches same-board quote numbers across .eth and .bso aliases', () => {
|
||||||
|
usePostNumberStore.setState({
|
||||||
|
cidToNumber: { 'op-cid': 1 },
|
||||||
|
numberToCid: { 'music.eth': { 1: 'op-cid' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
testState.replies = [
|
||||||
|
{
|
||||||
|
cid: 'reply-cid',
|
||||||
|
content: 'replying to >>1',
|
||||||
|
number: 42,
|
||||||
|
state: 'succeeded',
|
||||||
|
subplebbitAddress: 'music.bso',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
createElement(() => {
|
||||||
|
latestValue = useQuotedByMap(testState.replies as never, 'music.bso');
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(latestValue.get('op-cid')?.[0]?.cid).toBe('reply-cid');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import usePublishAuthorDomainGuard, { getPublishAuthorDomainErrorMessage } from
|
|||||||
|
|
||||||
type UsePublishPostOptions = {
|
type UsePublishPostOptions = {
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
/** legacy compatibility */
|
|
||||||
subplebbitAddress?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const usePublishPost = ({ communityAddress: requestedCommunityAddress, subplebbitAddress }: UsePublishPostOptions) => {
|
const usePublishPost = ({ communityAddress }: UsePublishPostOptions) => {
|
||||||
const { author, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore((state) => ({
|
const { author, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore((state) => ({
|
||||||
author: state.author,
|
author: state.author,
|
||||||
title: state.title || undefined,
|
title: state.title || undefined,
|
||||||
@@ -30,8 +28,6 @@ const usePublishPost = ({ communityAddress: requestedCommunityAddress, subplebbi
|
|||||||
await abandonPublishRef.current?.();
|
await abandonPublishRef.current?.();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const communityAddress = requestedCommunityAddress ?? subplebbitAddress;
|
|
||||||
|
|
||||||
const createBaseOptions = useCallback(() => {
|
const createBaseOptions = useCallback(() => {
|
||||||
const baseOptions: Comment = {
|
const baseOptions: Comment = {
|
||||||
communityAddress,
|
communityAddress,
|
||||||
@@ -60,12 +56,8 @@ const usePublishPost = ({ communityAddress: requestedCommunityAddress, subplebbi
|
|||||||
{} as Partial<Comment>,
|
{} as Partial<Comment>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { communityAddress: nextCommunityAddress, ...restOptions } = sanitizedOptions;
|
||||||
communityAddress: nextCommunityAddress,
|
const resolvedCommunityAddress = nextCommunityAddress ?? baseOptions.communityAddress;
|
||||||
subplebbitAddress: legacyCommunityAddress,
|
|
||||||
...restOptions
|
|
||||||
} = sanitizedOptions as Partial<Comment> & { subplebbitAddress?: string };
|
|
||||||
const resolvedCommunityAddress = nextCommunityAddress ?? legacyCommunityAddress ?? baseOptions.communityAddress;
|
|
||||||
const newOptions = {
|
const newOptions = {
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
...restOptions,
|
...restOptions,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Comment, useAccount, usePublishComment } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment, useAccount, usePublishComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import { useDirectories } from './use-directories';
|
import { useDirectories } from './use-directories';
|
||||||
import usePublishReplyStore from '../stores/use-publish-reply-store';
|
import usePublishReplyStore from '../stores/use-publish-reply-store';
|
||||||
import usePostNumberStore from '../stores/use-post-number-store';
|
import usePostNumberStore, { getScopedNumberToCidMap } from '../stores/use-post-number-store';
|
||||||
import { getQuotedCidsFromContent, mergeQuotedCids } from '../lib/utils/reply-quote-utils';
|
import { getQuotedCidsFromContent, mergeQuotedCids } from '../lib/utils/reply-quote-utils';
|
||||||
import { extractUnresolvedExternalQuoteReferences, getExternalQuoteStatusMessage } from '../lib/utils/external-quote-utils';
|
import { extractUnresolvedExternalQuoteReferences, getExternalQuoteStatusMessage } from '../lib/utils/external-quote-utils';
|
||||||
import { resolveExternalQuoteTarget } from '../lib/utils/external-quote-resolver';
|
import { resolveExternalQuoteTarget } from '../lib/utils/external-quote-resolver';
|
||||||
@@ -13,14 +13,10 @@ import usePublishAuthorDomainGuard, { getPublishAuthorDomainErrorMessage } from
|
|||||||
type UsePublishReplyOptions = {
|
type UsePublishReplyOptions = {
|
||||||
cid: string;
|
cid: string;
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
/** legacy compatibility */
|
|
||||||
subplebbitAddress?: string;
|
|
||||||
postCid?: string;
|
postCid?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, subplebbitAddress, postCid }: UsePublishReplyOptions) => {
|
const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOptions) => {
|
||||||
const communityAddress = requestedCommunityAddress ?? subplebbitAddress;
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const parentCid = cid;
|
const parentCid = cid;
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
@@ -78,12 +74,8 @@ const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, sub
|
|||||||
{} as Partial<Comment>,
|
{} as Partial<Comment>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { communityAddress: nextCommunityAddress, ...restOptions } = sanitizedOptions;
|
||||||
communityAddress: nextCommunityAddress,
|
const resolvedCommunityAddress = nextCommunityAddress ?? baseOptions.communityAddress;
|
||||||
subplebbitAddress: legacyCommunityAddress,
|
|
||||||
...restOptions
|
|
||||||
} = sanitizedOptions as Partial<Comment> & { subplebbitAddress?: string };
|
|
||||||
const resolvedCommunityAddress = nextCommunityAddress ?? legacyCommunityAddress ?? baseOptions.communityAddress;
|
|
||||||
const newOptions = {
|
const newOptions = {
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
...restOptions,
|
...restOptions,
|
||||||
@@ -96,7 +88,7 @@ const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, sub
|
|||||||
|
|
||||||
const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]);
|
const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]);
|
||||||
|
|
||||||
const scopedNumberToCid = usePostNumberStore((state) => (communityAddress ? state.numberToCid[communityAddress] : undefined));
|
const scopedNumberToCid = usePostNumberStore((state) => getScopedNumberToCidMap(state.numberToCid, communityAddress));
|
||||||
const quotedCids = useMemo(() => getQuotedCidsFromContent(content, scopedNumberToCid), [content, scopedNumberToCid]);
|
const quotedCids = useMemo(() => getQuotedCidsFromContent(content, scopedNumberToCid), [content, scopedNumberToCid]);
|
||||||
const unresolvedExternalQuoteReferences = useMemo(
|
const unresolvedExternalQuoteReferences = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import { Comment } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import { QUOTE_NUMBER_REGEX } from '../lib/utils/url-utils';
|
import { QUOTE_NUMBER_REGEX } from '../lib/utils/url-utils';
|
||||||
import usePostNumberStore from '../stores/use-post-number-store';
|
import usePostNumberStore, { getScopedNumberToCidMap } from '../stores/use-post-number-store';
|
||||||
|
|
||||||
interface ReplyQuoteTargets {
|
interface ReplyQuoteTargets {
|
||||||
reply: Comment;
|
reply: Comment;
|
||||||
@@ -61,27 +61,27 @@ const extractReplyQuoteTargets = (replies: Comment[]) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useQuotedByMap = (replies: Comment[] = [], subplebbitAddress?: string) => {
|
const useQuotedByMap = (replies: Comment[] = [], communityAddress?: string) => {
|
||||||
const stableQuotedByMapRef = useRef<Map<string, Comment[]>>(new Map());
|
const stableQuotedByMapRef = useRef<Map<string, Comment[]>>(new Map());
|
||||||
const { replyQuoteTargets, quotedPostNumbers } = useMemo(() => extractReplyQuoteTargets(replies), [replies]);
|
const { replyQuoteTargets, quotedPostNumbers } = useMemo(() => extractReplyQuoteTargets(replies), [replies]);
|
||||||
|
|
||||||
const quotedNumbersSignature = usePostNumberStore(
|
const quotedNumbersSignature = usePostNumberStore(
|
||||||
useCallback(
|
useCallback(
|
||||||
(state) => {
|
(state) => {
|
||||||
const scoped = subplebbitAddress ? state.numberToCid[subplebbitAddress] : undefined;
|
const scoped = getScopedNumberToCidMap(state.numberToCid, communityAddress);
|
||||||
return quotedPostNumbers.map((postNumber) => `${postNumber}:${scoped?.[postNumber] ?? ''}`).join('|');
|
return quotedPostNumbers.map((postNumber) => `${postNumber}:${scoped?.[postNumber] ?? ''}`).join('|');
|
||||||
},
|
},
|
||||||
[quotedPostNumbers, subplebbitAddress],
|
[quotedPostNumbers, communityAddress],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const quotedNumberToCid = useMemo(() => {
|
const quotedNumberToCid = useMemo(() => {
|
||||||
if (quotedPostNumbers.length === 0 || !subplebbitAddress) {
|
if (quotedPostNumbers.length === 0 || !communityAddress) {
|
||||||
return {} as Record<number, string>;
|
return {} as Record<number, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { numberToCid } = usePostNumberStore.getState();
|
const { numberToCid } = usePostNumberStore.getState();
|
||||||
const scoped = numberToCid[subplebbitAddress];
|
const scoped = getScopedNumberToCidMap(numberToCid, communityAddress);
|
||||||
if (!scoped) return {} as Record<number, string>;
|
if (!scoped) return {} as Record<number, string>;
|
||||||
|
|
||||||
const nextQuotedNumberToCid: Record<number, string> = {};
|
const nextQuotedNumberToCid: Record<number, string> = {};
|
||||||
@@ -94,7 +94,7 @@ const useQuotedByMap = (replies: Comment[] = [], subplebbitAddress?: string) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nextQuotedNumberToCid;
|
return nextQuotedNumberToCid;
|
||||||
}, [quotedPostNumbers, subplebbitAddress, quotedNumbersSignature]);
|
}, [quotedPostNumbers, communityAddress, quotedNumbersSignature]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const map = new Map<string, Comment[]>();
|
const map = new Map<string, Comment[]>();
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { getExternalQuoteBoardAddress, getExternalQuoteBoardLabel } from '../external-quote-utils';
|
||||||
|
|
||||||
|
const directories = [
|
||||||
|
{
|
||||||
|
address: 'music-posting.eth',
|
||||||
|
name: 'music-posting.bso',
|
||||||
|
title: '/mu/ - Music',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: 'random.eth',
|
||||||
|
directoryCode: 'b',
|
||||||
|
title: 'Random',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('external quote board resolution', () => {
|
||||||
|
it('canonicalizes same-board aliases through the directories list', () => {
|
||||||
|
const reference = {
|
||||||
|
communityAddress: 'music-posting.bso',
|
||||||
|
kind: 'same-board' as const,
|
||||||
|
number: 42,
|
||||||
|
raw: '>>42',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExternalQuoteBoardAddress(reference, directories)).toBe('music-posting.eth');
|
||||||
|
expect(getExternalQuoteBoardLabel(reference, directories)).toBe('mu');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('canonicalizes cross-board aliases through the directories list', () => {
|
||||||
|
const reference = {
|
||||||
|
boardIdentifier: 'music-posting.bso',
|
||||||
|
kind: 'cross-board' as const,
|
||||||
|
number: 77,
|
||||||
|
raw: '>>>/music-posting.bso/77',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExternalQuoteBoardAddress(reference, directories)).toBe('music-posting.eth');
|
||||||
|
expect(getExternalQuoteBoardLabel(reference, directories)).toBe('mu');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -155,7 +155,7 @@ const loadBoardThreads = async ({
|
|||||||
const feedName = getBoardFeedName(accountId, communityAddress);
|
const feedName = getBoardFeedName(accountId, communityAddress);
|
||||||
const feedState = feedsStore.getState();
|
const feedState = feedsStore.getState();
|
||||||
if (!feedState.feedsOptions[feedName]) {
|
if (!feedState.feedsOptions[feedName]) {
|
||||||
await feedState.addFeedToStore(feedName, [communityAddress], BOARD_FEED_SORT_TYPE, account, false, BOARD_SEARCH_POSTS_PER_PAGE);
|
await feedState.addFeedToStore(feedName, [{ address: communityAddress }], [communityAddress], BOARD_FEED_SORT_TYPE, account, false, BOARD_SEARCH_POSTS_PER_PAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitForBoardFeedPage(feedName, 0, 1);
|
await waitForBoardFeedPage(feedName, 0, 1);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { DirectoryCommunity } from '../../hooks/use-directories';
|
import type { DirectoryCommunity } from '../../hooks/use-directories';
|
||||||
import { getBoardPath, getCommunityAddress, getSubplebbitAddress } from './route-utils';
|
import { findDirectoryByAddress } from '../../hooks/use-directories';
|
||||||
|
import { getBoardPath, getCommunityAddress } from './route-utils';
|
||||||
import { QUOTE_NUMBER_REGEX } from './url-utils';
|
import { QUOTE_NUMBER_REGEX } from './url-utils';
|
||||||
|
|
||||||
const CROSSBOARD_NUMBER_BOARD_PART = '(?:[a-zA-Z0-9]{1,10}|12D3KooW[a-zA-Z0-9]{44}|[a-zA-Z0-9\\-.]+)';
|
const CROSSBOARD_NUMBER_BOARD_PART = '(?:[a-zA-Z0-9]{1,10}|12D3KooW[a-zA-Z0-9]{44}|[a-zA-Z0-9\\-.]+)';
|
||||||
@@ -12,8 +13,6 @@ export type SameBoardExternalQuoteReference = {
|
|||||||
number: number;
|
number: number;
|
||||||
raw: string;
|
raw: string;
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
// legacy compatibility alias
|
|
||||||
subplebbitAddress?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CrossBoardExternalQuoteReference = {
|
export type CrossBoardExternalQuoteReference = {
|
||||||
@@ -30,23 +29,27 @@ const getAddressForCanonicalReference = (reference: ExternalQuoteReference, dire
|
|||||||
return resolveLegacyCommunityAddress(reference.boardIdentifier, directories);
|
return resolveLegacyCommunityAddress(reference.boardIdentifier, directories);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveLegacyCommunityAddress(reference.communityAddress || reference.subplebbitAddress || '', directories);
|
return resolveLegacyCommunityAddress(reference.communityAddress || '', directories);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExternalQuoteKey = (reference: ExternalQuoteReference) =>
|
const getExternalQuoteKey = (reference: ExternalQuoteReference) =>
|
||||||
reference.kind === 'cross-board'
|
reference.kind === 'cross-board'
|
||||||
? `${reference.kind}:${reference.boardIdentifier}:${reference.number}`
|
? `${reference.kind}:${reference.boardIdentifier}:${reference.number}`
|
||||||
: `${reference.kind}:${reference.communityAddress || reference.subplebbitAddress}:${reference.number}`;
|
: `${reference.kind}:${reference.communityAddress}:${reference.number}`;
|
||||||
|
|
||||||
const resolveLegacyCommunityAddress = (boardIdentifier: string, communities: DirectoryCommunity[]) => {
|
const resolveLegacyCommunityAddress = (boardIdentifier: string, communities: DirectoryCommunity[]) => {
|
||||||
|
const matchingDirectory = findDirectoryByAddress(communities, boardIdentifier);
|
||||||
|
if (matchingDirectory?.address) {
|
||||||
|
return matchingDirectory.address;
|
||||||
|
}
|
||||||
|
|
||||||
// Canonical resolver in route utils handles directory or address mapping.
|
// Canonical resolver in route utils handles directory or address mapping.
|
||||||
const address = getCommunityAddress(boardIdentifier, communities);
|
const address = getCommunityAddress(boardIdentifier, communities);
|
||||||
if (address) {
|
if (address) {
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward-compat helper alias if needed by callers with older util behavior.
|
return boardIdentifier;
|
||||||
return getSubplebbitAddress(boardIdentifier, communities);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getExternalQuoteBoardAddress = (reference: ExternalQuoteReference, directories: DirectoryCommunity[]) =>
|
export const getExternalQuoteBoardAddress = (reference: ExternalQuoteReference, directories: DirectoryCommunity[]) =>
|
||||||
@@ -61,23 +64,18 @@ export const extractUnresolvedExternalQuoteReferences = ({
|
|||||||
content,
|
content,
|
||||||
scopedNumberToCid,
|
scopedNumberToCid,
|
||||||
communityAddress,
|
communityAddress,
|
||||||
subplebbitAddress,
|
|
||||||
}: {
|
}: {
|
||||||
content?: string;
|
content?: string;
|
||||||
scopedNumberToCid?: Record<number, string>;
|
scopedNumberToCid?: Record<number, string>;
|
||||||
// canonical input
|
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
// backward-compatible input name
|
|
||||||
subplebbitAddress?: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const effectiveCommunityAddress = communityAddress || subplebbitAddress;
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return [] as ExternalQuoteReference[];
|
return [] as ExternalQuoteReference[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const references = new Map<string, ExternalQuoteReference>();
|
const references = new Map<string, ExternalQuoteReference>();
|
||||||
|
|
||||||
if (effectiveCommunityAddress) {
|
if (communityAddress) {
|
||||||
for (const match of content.matchAll(new RegExp(QUOTE_NUMBER_REGEX.source, 'g'))) {
|
for (const match of content.matchAll(new RegExp(QUOTE_NUMBER_REGEX.source, 'g'))) {
|
||||||
const number = Number.parseInt(match[1], 10);
|
const number = Number.parseInt(match[1], 10);
|
||||||
if (Number.isNaN(number) || scopedNumberToCid?.[number]) {
|
if (Number.isNaN(number) || scopedNumberToCid?.[number]) {
|
||||||
@@ -88,8 +86,7 @@ export const extractUnresolvedExternalQuoteReferences = ({
|
|||||||
kind: 'same-board',
|
kind: 'same-board',
|
||||||
number,
|
number,
|
||||||
raw: `>>${number}`,
|
raw: `>>${number}`,
|
||||||
communityAddress: effectiveCommunityAddress,
|
communityAddress,
|
||||||
subplebbitAddress: effectiveCommunityAddress,
|
|
||||||
};
|
};
|
||||||
references.set(getExternalQuoteKey(reference), reference);
|
references.set(getExternalQuoteKey(reference), reference);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import useCreateBoardModalStore from '../use-create-board-modal-store';
|
|||||||
import useDirectoryModalStore from '../use-directory-modal-store';
|
import useDirectoryModalStore from '../use-directory-modal-store';
|
||||||
import useDisclaimerModalStore, { DISCLAIMER_ACCEPTED_KEY } from '../use-disclaimer-modal-store';
|
import useDisclaimerModalStore, { DISCLAIMER_ACCEPTED_KEY } from '../use-disclaimer-modal-store';
|
||||||
import useFeedResetStore from '../use-feed-reset-store';
|
import useFeedResetStore from '../use-feed-reset-store';
|
||||||
import usePostNumberStore from '../use-post-number-store';
|
import usePostNumberStore, { getCidForPostNumber, getScopedNumberToCidMap } from '../use-post-number-store';
|
||||||
import useReplyModalStore from '../use-reply-modal-store';
|
import useReplyModalStore from '../use-reply-modal-store';
|
||||||
import useSelectedTextStore from '../use-selected-text-store';
|
import useSelectedTextStore from '../use-selected-text-store';
|
||||||
import useSortingStore from '../use-sorting-store';
|
import useSortingStore from '../use-sorting-store';
|
||||||
@@ -235,6 +235,26 @@ describe('interaction stores', () => {
|
|||||||
expect(usePostNumberStore.getState().numberToCid).toBe(numberToCidRef);
|
expect(usePostNumberStore.getState().numberToCid).toBe(numberToCidRef);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('merges alias-scoped post number maps when the exact board scope is only partially populated', () => {
|
||||||
|
const numberToCid = {
|
||||||
|
'music.eth': {
|
||||||
|
1: 'op-cid',
|
||||||
|
2: 'reply-cid',
|
||||||
|
},
|
||||||
|
'music.bso': {
|
||||||
|
30: 'late-reply-cid',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getScopedNumberToCidMap(numberToCid, 'music.bso')).toEqual({
|
||||||
|
1: 'op-cid',
|
||||||
|
2: 'reply-cid',
|
||||||
|
30: 'late-reply-cid',
|
||||||
|
});
|
||||||
|
expect(getCidForPostNumber(numberToCid, 'music.bso', 1)).toBe('op-cid');
|
||||||
|
expect(getCidForPostNumber(numberToCid, 'music.bso', 30)).toBe('late-reply-cid');
|
||||||
|
});
|
||||||
|
|
||||||
it('opens reply modals with quoted selection and mobile scroll state', () => {
|
it('opens reply modals with quoted selection and mobile scroll state', () => {
|
||||||
Object.defineProperty(window, 'innerWidth', {
|
Object.defineProperty(window, 'innerWidth', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { Comment } from '@bitsocialnet/bitsocial-react-hooks';
|
import type { Comment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
|
import { normalizeBoardAddress } from '../hooks/use-directories';
|
||||||
|
|
||||||
interface PostNumberState {
|
interface PostNumberState {
|
||||||
// Post numbers are only unique within a board, so scope by canonical community address.
|
// Post numbers are only unique within a board, so scope by canonical community address.
|
||||||
@@ -8,6 +9,38 @@ interface PostNumberState {
|
|||||||
registerComments: (comments: Comment[]) => void;
|
registerComments: (comments: Comment[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getScopedNumberToCidMap = (numberToCid: Record<string, Record<number, string>>, communityAddress?: string) => {
|
||||||
|
if (!communityAddress) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedCommunityAddress = normalizeBoardAddress(communityAddress);
|
||||||
|
const exactMatch = numberToCid[communityAddress];
|
||||||
|
const matchingEntries = Object.entries(numberToCid).filter(([address]) => normalizeBoardAddress(address) === normalizedCommunityAddress);
|
||||||
|
|
||||||
|
if (matchingEntries.length === 0) {
|
||||||
|
return exactMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exactMatch && matchingEntries.length === 1) {
|
||||||
|
return matchingEntries[0][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exactMatch) {
|
||||||
|
return matchingEntries.reduce<Record<number, string>>((mergedMap, [, scopedMap]) => ({ ...mergedMap, ...scopedMap }), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingEntries.length === 1) {
|
||||||
|
return exactMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasEntries = matchingEntries.filter(([address]) => address !== communityAddress);
|
||||||
|
return aliasEntries.reduce<Record<number, string>>((mergedMap, [, scopedMap]) => ({ ...mergedMap, ...scopedMap }), { ...exactMatch });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCidForPostNumber = (numberToCid: Record<string, Record<number, string>>, communityAddress: string | undefined, postNumber: number) =>
|
||||||
|
getScopedNumberToCidMap(numberToCid, communityAddress)?.[postNumber];
|
||||||
|
|
||||||
const usePostNumberStore = create<PostNumberState>((set) => ({
|
const usePostNumberStore = create<PostNumberState>((set) => ({
|
||||||
numberToCid: {},
|
numberToCid: {},
|
||||||
cidToNumber: {},
|
cidToNumber: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user