feat(archive): implement comment.archived and add archive page (#1074)

* feat(archive): implement comment.archived and add archive page

Add isCommentArchived() utility, /board/:boardIdentifier/archive route,
archive view with filtered feed, and UI integration (board buttons, edit
menu, catalog row, post). Archived indicator on catalog and posts.

* fix(archive): address review feedback from Bugbot and CodeRabbit

Add missing i18n keys (archived, thread_archived, view, loading_archive),
add /archive/settings route, replace hardcoded English in archive.tsx, and
fix mobile test state.
This commit is contained in:
Tommaso Casaburi
2026-03-13 16:06:36 +08:00
committed by GitHub
parent d7703953fb
commit aedee9fb83
65 changed files with 1384 additions and 62 deletions
@@ -15,6 +15,9 @@ type TestComment = {
};
cid: string;
content?: string;
commentModeration?: {
archived?: boolean;
};
link?: string;
linkHeight?: number;
linkWidth?: number;
@@ -276,6 +279,25 @@ describe('CatalogRow', () => {
expect(container.querySelector<HTMLImageElement>('img[src="assets/filedeleted-res.gif"]')).toBeTruthy();
});
it('renders the archived icon for archived threads', async () => {
const post: TestComment = {
author: { address: 'author-1', displayName: 'Alice' },
cid: 'post-archived',
commentModeration: {
archived: true,
},
content: 'Archived thread',
replyCount: 3,
subplebbitAddress: 'music-posting.eth',
title: 'Old thread',
};
testState.mediaInfoByLink['https://example.com/media.png'] = { type: 'image', url: 'https://example.com/media.png' };
await renderWithRouter(createElement(CatalogRow, { row: [post] }));
expect(container.querySelector('[title="archived"]')).toBeTruthy();
});
it('renders audio players and video first-frame fallbacks for media without thumbnails', async () => {
await act(async () => {
root.render(
@@ -66,6 +66,10 @@
background-image: url("/assets/icons/closed.gif");
}
.threadIcons .archivedIcon {
background-image: url("/assets/icons/archived.gif");
}
.mediaWrapper {
background-color: var(--media-thumbnail-background-color);
width: var(--width);
@@ -18,6 +18,7 @@ import { useCommentMediaInfo } from '../../hooks/use-comment-media-info';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useHide from '../../hooks/use-hide';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import { removeMarkdown } from '../../lib/utils/post-utils';
import PostMenuDesktop from '../post-desktop/post-menu-desktop';
import styles from './catalog-row.module.css';
@@ -121,6 +122,7 @@ const CatalogPost = memo(
const resolvedPost = useMemo(() => withResolvedCommentCommunityAddress(post), [post]);
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, spoiler, communityAddress, timestamp, title, thumbnailUrl } =
resolvedPost || {};
const archived = isCommentArchived(resolvedPost);
const linkCount = useCountLinksInReplies(resolvedPost);
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
@@ -144,6 +146,7 @@ const CatalogPost = memo(
<div className={styles.threadIcons}>
{pinned && <span className={styles.stickyIcon} title={t('sticky')} />}
{locked && <span className={styles.closedIcon} title={t('closed')} />}
{archived && <span className={styles.archivedIcon} title={t('archived')} />}
</div>
);
@@ -324,6 +327,7 @@ const CatalogPost = memo(
prev?.replyCount === next?.replyCount &&
prev?.locked === next?.locked &&
prev?.pinned === next?.pinned &&
isCommentArchived(prev) === isCommentArchived(next) &&
prev?.title === next?.title &&
prev?.content === next?.content &&
prev?.spoiler === next?.spoiler &&