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
@@ -185,6 +185,7 @@ const renderWithRoute = async (element: React.ReactElement, initialEntry: string
createElement(Route, { path: '/all/catalog', element }),
createElement(Route, { path: '/mod/queue', element }),
createElement(Route, { path: '/:boardIdentifier/catalog', element }),
createElement(Route, { path: '/:boardIdentifier/archive', element }),
createElement(Route, { path: '/:boardIdentifier/thread/:commentCid', element }),
createElement(Route, { path: '/:boardIdentifier', element }),
),
@@ -288,8 +289,9 @@ describe('BoardButtons', () => {
expect(testState.resetMock).toHaveBeenCalledTimes(1);
expect(testState.subscribeMock).toHaveBeenCalledTimes(1);
expect(testState.navigateMock).toHaveBeenCalledWith('/mu/archive');
expect(globalThis.alert).toHaveBeenNthCalledWith(1, 'vote_button_unavailable_intro\n\nvote_button_unavailable_outro');
expect(globalThis.alert).toHaveBeenNthCalledWith(2, 'Work in progress');
expect(globalThis.alert).toHaveBeenCalledTimes(1);
});
it('renders desktop catalog controls and wires sort, style, filter, and refresh updates', async () => {
@@ -299,6 +301,7 @@ describe('BoardButtons', () => {
expect(container.textContent).toContain('filtered_threads');
expect(container.textContent).toContain('4');
expect(container.textContent).not.toContain('archive');
expect(container.querySelector('[data-testid="catalog-filters"]')?.textContent).toBe('catalog-filters');
expect(container.querySelector('[data-testid="catalog-search"]')?.textContent).toBe('catalog-search');
@@ -322,6 +325,7 @@ describe('BoardButtons', () => {
testState.commentsByCid = {
'comment-1': {
cid: 'comment-1',
archived: true,
closed: true,
number: 99,
pinned: true,
@@ -335,6 +339,7 @@ describe('BoardButtons', () => {
const tooltips = Array.from(container.querySelectorAll<HTMLElement>('[data-testid="tooltip"]'));
expect(tooltips.map((tooltip) => tooltip.dataset.content)).toEqual(['Replies', 'Links', 'pagination.pageLabel']);
expect(tooltips.map((tooltip) => tooltip.textContent)).toEqual(['9', '3', '7']);
expect(container.textContent).toContain('Archived /');
expect(container.textContent).toContain('Sticky /');
expect(container.textContent).toContain('Closed /');
+16 -5
View File
@@ -19,6 +19,7 @@ import CatalogFilters from '../catalog-filters';
import CatalogSearch from '../catalog-search';
import Tooltip from '../tooltip';
import { ModQueueButton } from '../../views/mod-queue/mod-queue';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import styles from './board-buttons.module.css';
import capitalize from 'lodash/capitalize';
@@ -59,13 +60,21 @@ export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isI
export const ArchiveButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const params = useParams();
const directories = useDirectories();
const handleClick = () => {
window.alert('Work in progress');
};
const isInvalidArchiveContext = isInAllView || isInSubscriptionsView || isInModView;
const boardIdentifier = params.boardIdentifier || params.subplebbitAddress;
const archiveBoardIdentifier = address ? getBoardPath(address, directories) : boardIdentifier ? getBoardPath(boardIdentifier, directories) : '';
const archivePath = archiveBoardIdentifier ? `/${archiveBoardIdentifier}/archive` : '';
if (isInvalidArchiveContext || !archivePath) {
return null;
}
return (
<button className='button' onClick={handleClick}>
<button className='button' onClick={() => navigate(archivePath)}>
{t('archive')}
</button>
);
@@ -183,7 +192,7 @@ export const AutoButton = () => {
);
};
const BottomButton = () => {
export const BottomButton = () => {
const { t } = useTranslation();
const handleClick = () => {
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' });
@@ -495,6 +504,7 @@ export const PostPageStats = () => {
const postCid = comment?.postCid ?? commentCid;
const post = useComment({ commentCid: postCid });
const archived = isCommentArchived(post);
const { closed, pinned, replyCount } = post || {};
const linkCount = useCountLinksInReplies(post);
const directoryEntry = useDirectoryByAddress(communityAddress);
@@ -513,6 +523,7 @@ export const PostPageStats = () => {
return (
<span>
{pinned && `${capitalize(t('sticky'))} / `}
{archived && `${capitalize(t('archived'))} / `}
{closed && `${capitalize(t('closed'))} / `}
<Tooltip content={replyCountTooltip}>{displayReplyCount}</Tooltip> /{' '}
<Tooltip content={capitalize(requirePostLinkIsMedia ? t('images') : t('links'))}>{linkCount?.toString()}</Tooltip>