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
@@ -7,6 +7,7 @@ import {
getFeedType,
getPageFromFeedPath,
getSubplebbitAddress,
isArchiveRoute,
isBoardModRoute,
isDirectoryBoard,
isFeedRoute,
@@ -98,6 +99,13 @@ describe('isFeedRoute', () => {
expect(isFeedRoute('/biz/mod/queue')).toBe(false);
});
it('returns false for board archive paths', () => {
expect(isFeedRoute('/biz/archive')).toBe(false);
expect(isFeedRoute('/biz/archive/settings')).toBe(false);
expect(isArchiveRoute('/biz/archive')).toBe(true);
expect(isArchiveRoute('/biz/archive/settings')).toBe(true);
});
it('returns false for posts and pending items', () => {
expect(isFeedRoute('/biz/thread/abc')).toBe(false);
expect(isFeedRoute('/pending/4')).toBe(false);
@@ -175,6 +183,7 @@ describe('feed cache helpers', () => {
expect(getFeedCacheKey('/biz/3/settings')).toBe('/biz');
expect(getFeedCacheKey('/biz/catalog/4')).toBe('/biz/catalog');
expect(getFeedCacheKey('/biz/thread/abc')).toBe('/biz');
expect(getFeedCacheKey('/biz/archive')).toBeNull();
});
it('returns null cache keys for non-feed routes', () => {
@@ -187,5 +196,6 @@ describe('feed cache helpers', () => {
expect(getFeedType('/biz/thread/abc')).toBe('board');
expect(getFeedType('/biz/catalog/settings')).toBe('catalog');
expect(getFeedType('/pending/3')).toBeNull();
expect(getFeedType('/biz/archive')).toBeNull();
});
});
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
isAllView,
isArchiveView,
isBoardView,
isCatalogView,
isHomeView,
@@ -48,5 +49,8 @@ describe('view-utils', () => {
expect(isPostPageView('/emoji-%F0%9F%8E%B5.eth/thread/cid-123', params)).toBe(true);
expect(isNotFoundView('/definitely-not-a-route', params)).toBe(true);
expect(isNotFoundView('/emoji-%F0%9F%8E%B5.eth/thread/cid-123', params)).toBe(false);
expect(isArchiveView('/music.eth/archive', { boardIdentifier: 'music.eth' })).toBe(true);
expect(isBoardView('/music.eth/archive', { boardIdentifier: 'music.eth' })).toBe(false);
expect(isNotFoundView('/music.eth/archive', { boardIdentifier: 'music.eth' })).toBe(false);
});
});
+15
View File
@@ -0,0 +1,15 @@
type MaybeArchivedComment = {
archived?: boolean;
commentModeration?: {
archived?: boolean;
};
};
export const isCommentArchived = (comment: unknown): boolean => {
if (!comment || typeof comment !== 'object') {
return false;
}
const archivedComment = comment as MaybeArchivedComment;
return Boolean(archivedComment.archived || archivedComment.commentModeration?.archived);
};
+7 -1
View File
@@ -120,11 +120,17 @@ export const isDirectoryBoard = (identifier: string, communities: DirectoryCommu
return directoryToAddress.has(identifier);
};
export const isArchiveRoute = (pathname: string): boolean => {
const normalizedPath = pathname.replace(/\/settings$/, '').replace(/\/$/, '');
return /\/archive$/.test(normalizedPath);
};
export const isFeedRoute = (pathname: string): boolean => {
const normalizedPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
if (normalizedPath.includes('/thread/')) return false;
if (normalizedPath.startsWith('/pending/')) return false;
if (isArchiveRoute(normalizedPath)) return false;
if (isBoardModRoute(normalizedPath) || isModQueueRoute(normalizedPath)) return false;
const pathWithoutSettings = normalizedPath.replace(/\/settings$/, '');
@@ -262,7 +268,7 @@ export const getFeedCacheKey = (pathname: string): string | null => {
return null;
}
if (isBoardModRoute(normalizedPath) || isModQueueRoute(normalizedPath)) {
if (isArchiveRoute(normalizedPath) || isBoardModRoute(normalizedPath) || isModQueueRoute(normalizedPath)) {
return null;
}
+11 -1
View File
@@ -1,4 +1,4 @@
import { isBoardModRoute, isModQueueRoute } from './route-utils';
import { isArchiveRoute, isBoardModRoute, isModQueueRoute } from './route-utils';
type ParamsType = {
accountCommentIndex?: string;
@@ -19,6 +19,7 @@ export const isBoardView = (pathname: string, params: ParamsType): boolean => {
pathname.startsWith('/all') ||
pathname.startsWith('/subs') ||
pathname.startsWith('/mod') ||
isArchiveRoute(pathname) ||
isBoardModRoute(pathname) ||
pathname.startsWith('/pending') ||
pathname === '/' ||
@@ -82,10 +83,19 @@ export const isSubscriptionsView = (pathname: string, params: ParamsType): boole
return pathname === '/subs' || pathname === '/subs/settings' || pathname === '/subs/catalog' || pathname === '/subs/catalog/settings';
};
export const isArchiveView = (pathname: string, params: ParamsType): boolean => {
const { boardIdentifier, subplebbitAddress } = params;
const identifier = boardIdentifier || subplebbitAddress;
const decodedPathname = decodeURIComponent(pathname);
return Boolean(identifier && isArchiveRoute(decodedPathname) && decodedPathname === `/${identifier}/archive`);
};
export const isNotFoundView = (pathname: string, params: ParamsType): boolean => {
return (
!isAllView(pathname) &&
!isBoardView(pathname, params) &&
!isArchiveView(pathname, params) &&
!isCatalogView(pathname, params) &&
!isHomeView(pathname) &&
!isPendingPostView(pathname, params) &&