mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
refactor: migrate 5chan to the community hooks API (#1073)
* refactor(community-api): migrate 5chan to community hooks * fix(review): address PR feedback * fix(review): preserve legacy board context fallbacks * fix(review): address latest bot feedback * fix(review): use communityAddress in edit menu privileges
This commit is contained in:
@@ -24,7 +24,16 @@ type TestComment = {
|
||||
postCid?: string;
|
||||
removed?: boolean;
|
||||
replyCount?: number;
|
||||
replies?: {
|
||||
pages?: Record<
|
||||
string,
|
||||
{
|
||||
comments?: TestComment[];
|
||||
}
|
||||
>;
|
||||
};
|
||||
spoiler?: boolean;
|
||||
communityAddress?: string;
|
||||
subplebbitAddress?: string;
|
||||
thumbnailUrl?: string;
|
||||
timestamp?: number;
|
||||
@@ -44,6 +53,7 @@ const testState = vi.hoisted(() => ({
|
||||
linkCount: 0,
|
||||
matchedFilters: new Map<string, string>(),
|
||||
mediaInfoByLink: {} as Record<string, { patternThumbnailUrl?: string; thumbnail?: string; type: string; url: string }>,
|
||||
lastRepliesComment: undefined as TestComment | undefined,
|
||||
replies: [] as TestComment[],
|
||||
roleByAddress: {} as Record<string, { commentAuthorRole?: string; isCommentAuthorMod: boolean }>,
|
||||
showOPComment: true,
|
||||
@@ -63,9 +73,28 @@ vi.mock('react-i18next', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
||||
useReplies: ({ comment }: { comment?: TestComment }) => ({
|
||||
replies: comment ? testState.replies : [],
|
||||
}),
|
||||
useReplies: ({ comment, sortType }: { comment?: TestComment; sortType?: string }) => {
|
||||
if (comment) {
|
||||
testState.lastRepliesComment = comment;
|
||||
}
|
||||
|
||||
const preloadedReplies =
|
||||
comment?.replies?.pages?.[sortType || 'best']?.comments ?? Object.values(comment?.replies?.pages ?? {}).find((page) => page?.comments?.length)?.comments;
|
||||
|
||||
const compatiblePreloadedReplies: TestComment[] = [];
|
||||
if (preloadedReplies?.length && comment?.communityAddress) {
|
||||
for (const reply of preloadedReplies) {
|
||||
if (!reply?.communityAddress || reply.communityAddress !== comment.communityAddress) {
|
||||
break;
|
||||
}
|
||||
compatiblePreloadedReplies.push(reply);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
replies: comment ? (compatiblePreloadedReplies.length ? compatiblePreloadedReplies : testState.replies) : [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks/dist/lib/localforage-lru/index.js', () => ({
|
||||
@@ -202,6 +231,7 @@ describe('CatalogRow', () => {
|
||||
testState.linkCount = 0;
|
||||
testState.matchedFilters = new Map<string, string>();
|
||||
testState.mediaInfoByLink = {};
|
||||
testState.lastRepliesComment = undefined;
|
||||
testState.replies = [];
|
||||
testState.roleByAddress = {};
|
||||
testState.showOPComment = true;
|
||||
@@ -296,7 +326,7 @@ describe('CatalogRow', () => {
|
||||
locked: true,
|
||||
pinned: true,
|
||||
replyCount: 5,
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
communityAddress: 'music-posting.eth',
|
||||
timestamp: 100,
|
||||
title: 'Thread title',
|
||||
};
|
||||
@@ -335,7 +365,7 @@ describe('CatalogRow', () => {
|
||||
content: 'Alias test',
|
||||
link: 'https://example.com/media.png',
|
||||
replyCount: 4,
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
communityAddress: 'music-posting.eth',
|
||||
title: 'Alias title',
|
||||
};
|
||||
|
||||
@@ -348,6 +378,52 @@ describe('CatalogRow', () => {
|
||||
expect(container.querySelector('[title=\"(R)eplies / (I)mage Replies\"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('normalizes legacy board addresses before fetching hover preview replies', async () => {
|
||||
testState.directories = [{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' }];
|
||||
testState.mediaInfoByLink['https://example.com/legacy.png'] = { type: 'image', url: 'https://example.com/legacy.png' };
|
||||
testState.replies = [];
|
||||
|
||||
const post: TestComment = {
|
||||
author: { address: 'author-1', displayName: 'Alice' },
|
||||
cid: 'post-legacy',
|
||||
content: 'Legacy address thread',
|
||||
link: 'https://example.com/legacy.png',
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
pages: {
|
||||
new: {
|
||||
comments: [
|
||||
{
|
||||
author: { address: 'author-2', displayName: 'Bob' },
|
||||
cid: 'reply-legacy',
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
timestamp: 200,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
timestamp: 100,
|
||||
title: 'Legacy title',
|
||||
};
|
||||
|
||||
await renderWithRouter(createElement(CatalogRow, { row: [post] }), '/all/catalog');
|
||||
vi.useFakeTimers();
|
||||
|
||||
const previewTrigger = document.body.querySelector('a[href="/mu/thread/post-legacy"] > div');
|
||||
await act(async () => {
|
||||
previewTrigger?.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
vi.advanceTimersByTime(260);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(testState.lastRepliesComment?.communityAddress).toBe('music-posting.eth');
|
||||
expect(testState.lastRepliesComment?.replies?.pages?.new?.comments?.[0]?.communityAddress).toBe('music-posting.eth');
|
||||
expect(document.body.textContent).toContain('Legacy title by Alice');
|
||||
expect(document.body.textContent).toContain('last_reply_by Bob');
|
||||
});
|
||||
|
||||
it('renders hidden and text-only threads with canonical board thread links', async () => {
|
||||
testState.hiddenCids = new Set(['hidden-1']);
|
||||
testState.showOPComment = false;
|
||||
@@ -358,14 +434,14 @@ describe('CatalogRow', () => {
|
||||
cid: 'hidden-1',
|
||||
content: 'hidden text',
|
||||
link: 'https://example.com/hidden.png',
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
communityAddress: 'music-posting.eth',
|
||||
},
|
||||
{
|
||||
author: { address: 'text-author', displayName: 'Anon' },
|
||||
cid: 'text-1',
|
||||
content: 'Plain thread body',
|
||||
replyCount: 1,
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
communityAddress: 'music-posting.eth',
|
||||
title: 'Text title',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -23,6 +23,7 @@ import PostMenuDesktop from '../post-desktop/post-menu-desktop';
|
||||
import styles from './catalog-row.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||
|
||||
interface CatalogPostMediaProps {
|
||||
cid: string;
|
||||
@@ -117,8 +118,10 @@ export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight
|
||||
const CatalogPost = memo(
|
||||
({ post }: { post: Comment }) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, spoiler, subplebbitAddress, timestamp, title, thumbnailUrl } = post || {};
|
||||
const linkCount = useCountLinksInReplies(post);
|
||||
const resolvedPost = useMemo(() => withResolvedCommentCommunityAddress(post), [post]);
|
||||
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, spoiler, communityAddress, timestamp, title, thumbnailUrl } =
|
||||
resolvedPost || {};
|
||||
const linkCount = useCountLinksInReplies(resolvedPost);
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
@@ -130,10 +133,10 @@ const CatalogPost = memo(
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const directories = useDirectories();
|
||||
const directoryEntry = findDirectoryByAddress(directories, subplebbitAddress);
|
||||
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
|
||||
const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true;
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : '';
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : '';
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(resolvedPost), [resolvedPost]);
|
||||
|
||||
const postLink = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
||||
|
||||
@@ -185,16 +188,16 @@ const CatalogPost = memo(
|
||||
if (showPortal) update();
|
||||
}, [showPortal, update]);
|
||||
|
||||
const { replies } = useReplies({ comment: showPortal ? post : undefined, flat: true });
|
||||
const { replies } = useReplies({ comment: showPortal ? resolvedPost : undefined, flat: true });
|
||||
const lastReply = replies?.length > 0 ? replies[replies.length - 1] : null;
|
||||
|
||||
const { isCommentAuthorMod: isCatalogPostAuthorMod, commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: author?.address,
|
||||
subplebbitAddress,
|
||||
communityAddress: communityAddress ?? '',
|
||||
});
|
||||
const { isCommentAuthorMod: isLastReplyAuthorMod, commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: lastReply?.author?.address,
|
||||
subplebbitAddress,
|
||||
communityAddress: communityAddress ?? '',
|
||||
});
|
||||
|
||||
const postContent = (
|
||||
@@ -292,7 +295,7 @@ const CatalogPost = memo(
|
||||
{author?.displayName || capitalize(t('anonymous'))}
|
||||
{isCatalogPostAuthorMod && <span className='capitalize'>{` ## Board ${catalogPostAuthorRole}`}</span>}
|
||||
</span>
|
||||
{(isInAllView || isInSubscriptionsView) && subplebbitAddress && ` to p/${getShortAddress(subplebbitAddress)}`}
|
||||
{(isInAllView || isInSubscriptionsView) && communityAddress && ` to p/${getShortAddress(communityAddress)}`}
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(timestamp)}</span>
|
||||
{replyCount > 0 && (
|
||||
<div className={styles.postLast}>
|
||||
@@ -313,6 +316,8 @@ const CatalogPost = memo(
|
||||
(prevProps, nextProps) => {
|
||||
const prev = prevProps.post;
|
||||
const next = nextProps.post;
|
||||
const prevCommunityAddress = prev?.communityAddress ?? prev?.subplebbitAddress;
|
||||
const nextCommunityAddress = next?.communityAddress ?? next?.subplebbitAddress;
|
||||
// Compare all fields that affect rendering to avoid stale displays
|
||||
return (
|
||||
prev?.cid === next?.cid &&
|
||||
@@ -329,7 +334,7 @@ const CatalogPost = memo(
|
||||
prev?.thumbnailUrl === next?.thumbnailUrl &&
|
||||
prev?.linkWidth === next?.linkWidth &&
|
||||
prev?.linkHeight === next?.linkHeight &&
|
||||
prev?.subplebbitAddress === next?.subplebbitAddress
|
||||
prevCommunityAddress === nextCommunityAddress
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user