mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
refactor(multiboards): remove time filter system end-to-end
Backend retention now keeps only recent posts, making the time filter UI and newerThan filtering obsolete. Deleted use-time-filter hook, canonical multiboard routes only, removed footer suggestions, dropped i18n keys.
This commit is contained in:
+18
-15
@@ -243,27 +243,30 @@ const App = () => {
|
||||
<Route path='/faq' element={<FAQ />} />
|
||||
<Route path='/rules/:boardIdentifier?' element={<Rules />} />
|
||||
<Route element={<BoardLayout />}>
|
||||
<Route path='/all/:timeFilterName/:pageNumber' element={boardFeedElement} />
|
||||
<Route path='/all/:timeFilterName?' element={boardFeedElement} />
|
||||
<Route path='/all/:timeFilterName?/settings' element={boardFeedElement} />
|
||||
<Route path='/all/catalog/:timeFilterName?' element={catalogFeedElement} />
|
||||
<Route path='/all/catalog/:timeFilterName?/settings' element={catalogFeedElement} />
|
||||
{/* Canonical multiboard routes (no time filter) */}
|
||||
<Route path='/all' element={boardFeedElement} />
|
||||
<Route path='/all/settings' element={boardFeedElement} />
|
||||
<Route path='/all/catalog' element={catalogFeedElement} />
|
||||
<Route path='/all/catalog/settings' element={catalogFeedElement} />
|
||||
|
||||
<Route path='/subs/:timeFilterName/:pageNumber' element={boardFeedElement} />
|
||||
<Route path='/subs/:timeFilterName?' element={boardFeedElement} />
|
||||
<Route path='/subs/:timeFilterName?/settings' element={boardFeedElement} />
|
||||
<Route path='/subs/catalog/:timeFilterName?' element={catalogFeedElement} />
|
||||
<Route path='/subs/catalog/:timeFilterName?/settings' element={catalogFeedElement} />
|
||||
<Route path='/subs' element={boardFeedElement} />
|
||||
<Route path='/subs/settings' element={boardFeedElement} />
|
||||
<Route path='/subs/catalog' element={catalogFeedElement} />
|
||||
<Route path='/subs/catalog/settings' element={catalogFeedElement} />
|
||||
|
||||
<Route path='/mod/:timeFilterName/:pageNumber' element={boardFeedElement} />
|
||||
<Route path='/mod/:timeFilterName?' element={boardFeedElement} />
|
||||
<Route path='/mod/:timeFilterName?/settings' element={boardFeedElement} />
|
||||
<Route path='/mod/catalog/:timeFilterName?' element={catalogFeedElement} />
|
||||
<Route path='/mod/catalog/:timeFilterName?/settings' element={catalogFeedElement} />
|
||||
<Route path='/mod' element={boardFeedElement} />
|
||||
<Route path='/mod/settings' element={boardFeedElement} />
|
||||
<Route path='/mod/catalog' element={catalogFeedElement} />
|
||||
<Route path='/mod/catalog/settings' element={catalogFeedElement} />
|
||||
|
||||
<Route path='/mod/modqueue' element={<ModQueueRoute />} />
|
||||
<Route path='/mod/modqueue/settings' element={<ModQueueRoute />} />
|
||||
|
||||
{/* Invalid subpaths: old time-filter URLs (e.g. /all/24h) -> not-found */}
|
||||
<Route path='/all/*' element={<Navigate to='/not-found' replace />} />
|
||||
<Route path='/subs/*' element={<Navigate to='/not-found' replace />} />
|
||||
<Route path='/mod/*' element={<Navigate to='/not-found' replace />} />
|
||||
|
||||
<Route path='/:boardIdentifier/:pageNumber' element={boardFeedElement} />
|
||||
<Route path='/:boardIdentifier' element={boardFeedElement} />
|
||||
<Route path='/:boardIdentifier/settings' element={boardFeedElement} />
|
||||
|
||||
@@ -15,7 +15,6 @@ import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||
import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store';
|
||||
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useTimeFilter from '../../hooks/use-time-filter';
|
||||
import CatalogFilters from '../catalog-filters';
|
||||
import CatalogSearch from '../catalog-search';
|
||||
import Tooltip from '../tooltip';
|
||||
@@ -39,16 +38,9 @@ const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModVie
|
||||
const directories = useDirectories();
|
||||
|
||||
const createCatalogLink = () => {
|
||||
if (isInAllView) {
|
||||
if (params?.timeFilterName) return `/all/catalog/${params.timeFilterName}`;
|
||||
return `/all/catalog`;
|
||||
} else if (isInSubscriptionsView) {
|
||||
if (params?.timeFilterName) return `/subs/catalog/${params.timeFilterName}`;
|
||||
return `/subs/catalog`;
|
||||
} else if (isInModView) {
|
||||
if (params?.timeFilterName) return `/mod/catalog/${params.timeFilterName}`;
|
||||
return `/mod/catalog`;
|
||||
}
|
||||
if (isInAllView) return `/all/catalog`;
|
||||
if (isInSubscriptionsView) return `/subs/catalog`;
|
||||
if (isInModView) return `/mod/catalog`;
|
||||
let boardPath = '';
|
||||
if (address) {
|
||||
boardPath = getBoardPath(address, directories);
|
||||
@@ -82,22 +74,16 @@ const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView
|
||||
const directories = useDirectories();
|
||||
|
||||
const createReturnLink = () => {
|
||||
if (isInAllView) {
|
||||
if (params?.timeFilterName) return `/all/${params.timeFilterName}`;
|
||||
return `/all`;
|
||||
} else if (isInSubscriptionsView) {
|
||||
if (params?.timeFilterName) return `/subs/${params.timeFilterName}`;
|
||||
return `/subs`;
|
||||
} else if (isInModQueueView) {
|
||||
if (isInAllView) return `/all`;
|
||||
if (isInSubscriptionsView) return `/subs`;
|
||||
if (isInModQueueView) {
|
||||
// If in mod queue view, return to /mod or /:boardIdentifier
|
||||
if (params?.boardIdentifier) {
|
||||
return `/${params.boardIdentifier}`;
|
||||
}
|
||||
return `/mod`;
|
||||
} else if (isInModView) {
|
||||
if (params?.timeFilterName) return `/mod/${params.timeFilterName}`;
|
||||
return `/mod`;
|
||||
}
|
||||
if (isInModView) return `/mod`;
|
||||
let boardPath = '';
|
||||
if (address) {
|
||||
boardPath = getBoardPath(address, directories);
|
||||
@@ -335,53 +321,6 @@ const ShowOPCommentOption = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView, isInModView, isTopbar = false }: BoardButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { timeFilterName, timeFilterNames } = useTimeFilter();
|
||||
|
||||
const changeTimeFilter = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const timeFilterName = event.target.value;
|
||||
const link = isInAllView
|
||||
? isInCatalogView
|
||||
? `/all/catalog/${timeFilterName}`
|
||||
: `/all/${timeFilterName}`
|
||||
: isInSubscriptionsView
|
||||
? isInCatalogView
|
||||
? `/subs/catalog/${timeFilterName}`
|
||||
: `/subs/${timeFilterName}`
|
||||
: isInModView
|
||||
? isInCatalogView
|
||||
? `/mod/catalog/${timeFilterName}`
|
||||
: `/mod/${timeFilterName}`
|
||||
: null;
|
||||
link && navigate(link);
|
||||
};
|
||||
|
||||
const { sortType } = useSortingStore();
|
||||
|
||||
const allTimeFilterNames = timeFilterName ? Array.from(new Set([timeFilterName, ...timeFilterNames])) : timeFilterNames;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isTopbar ? (
|
||||
<>
|
||||
<span>{isInCatalogView ? (sortType === 'active' ? t('last_bumped') : t('newer_than')) : t('last_bumped')}</span>:
|
||||
</>
|
||||
) : (
|
||||
<> </>
|
||||
)}
|
||||
<select onChange={changeTimeFilter} className={[styles.feedName, styles.menuItem, 'capitalize'].join(' ')} value={timeFilterName}>
|
||||
{allTimeFilterNames.map((name, i) => (
|
||||
<option key={name + i} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AllFeedFilter = () => {
|
||||
const { t } = useTranslation();
|
||||
const { filter, setFilter } = useAllFeedFilterStore();
|
||||
@@ -643,9 +582,6 @@ export const DesktopBoardButtons = () => {
|
||||
</>
|
||||
)}
|
||||
{isInAllView && <AllFeedFilter />}
|
||||
{(isInAllView || isInSubscriptionsView || isInModView) && (
|
||||
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
)}
|
||||
{!(isInAllView || isInSubscriptionsView || isInModView) && (
|
||||
<>
|
||||
[
|
||||
@@ -684,17 +620,11 @@ const SearchOPsBar = () => {
|
||||
let catalogUrl = '';
|
||||
|
||||
if (isInAllView) {
|
||||
catalogUrl = params?.timeFilterName
|
||||
? `/all/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
|
||||
: `/all/catalog?q=${encodeURIComponent(searchQuery)}`;
|
||||
catalogUrl = `/all/catalog?q=${encodeURIComponent(searchQuery)}`;
|
||||
} else if (isInSubscriptionsView) {
|
||||
catalogUrl = params?.timeFilterName
|
||||
? `/subs/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
|
||||
: `/subs/catalog?q=${encodeURIComponent(searchQuery)}`;
|
||||
catalogUrl = `/subs/catalog?q=${encodeURIComponent(searchQuery)}`;
|
||||
} else if (isInModView) {
|
||||
catalogUrl = params?.timeFilterName
|
||||
? `/mod/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
|
||||
: `/mod/catalog?q=${encodeURIComponent(searchQuery)}`;
|
||||
catalogUrl = `/mod/catalog?q=${encodeURIComponent(searchQuery)}`;
|
||||
} else {
|
||||
catalogUrl = `/${boardPath}/catalog?q=${encodeURIComponent(searchQuery)}`;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { TimeFilter, MobileBoardButtons, DesktopBoardButtons } from './board-buttons';
|
||||
export { MobileBoardButtons, DesktopBoardButtons } from './board-buttons';
|
||||
|
||||
@@ -9,7 +9,6 @@ import styles from './feed-cache-container.module.css';
|
||||
interface FeedContextFromKey {
|
||||
viewType: 'all' | 'subs' | 'mod' | 'board';
|
||||
boardIdentifier?: string;
|
||||
timeFilterName?: string;
|
||||
}
|
||||
|
||||
const parseFeedKey = (key: string): FeedContextFromKey => {
|
||||
@@ -17,28 +16,18 @@ const parseFeedKey = (key: string): FeedContextFromKey => {
|
||||
|
||||
const filteredSegments = segments.filter((s) => s !== 'catalog');
|
||||
if (filteredSegments[0] === 'all') {
|
||||
return {
|
||||
viewType: 'all',
|
||||
timeFilterName: filteredSegments[1],
|
||||
};
|
||||
return { viewType: 'all' };
|
||||
}
|
||||
if (filteredSegments[0] === 'subs') {
|
||||
return {
|
||||
viewType: 'subs',
|
||||
timeFilterName: filteredSegments[1],
|
||||
};
|
||||
return { viewType: 'subs' };
|
||||
}
|
||||
if (filteredSegments[0] === 'mod') {
|
||||
return {
|
||||
viewType: 'mod',
|
||||
timeFilterName: filteredSegments[1],
|
||||
};
|
||||
return { viewType: 'mod' };
|
||||
}
|
||||
|
||||
return {
|
||||
viewType: 'board',
|
||||
boardIdentifier: filteredSegments[0],
|
||||
timeFilterName: filteredSegments[1],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,21 +42,9 @@ const CachedFeedWrapper = ({ feed, isVisible }: CachedFeedWrapperProps) => {
|
||||
return (
|
||||
<div className={isVisible ? styles.visible : styles.hidden}>
|
||||
{feed.type === 'catalog' ? (
|
||||
<Catalog
|
||||
feedCacheKey={feed.key}
|
||||
viewType={context.viewType}
|
||||
boardIdentifier={context.boardIdentifier}
|
||||
timeFilterNameFromCache={context.timeFilterName}
|
||||
isVisible={isVisible}
|
||||
/>
|
||||
<Catalog feedCacheKey={feed.key} viewType={context.viewType} boardIdentifier={context.boardIdentifier} isVisible={isVisible} />
|
||||
) : (
|
||||
<Board
|
||||
feedCacheKey={feed.key}
|
||||
viewType={context.viewType}
|
||||
boardIdentifier={context.boardIdentifier}
|
||||
timeFilterNameFromCache={context.timeFilterName}
|
||||
isVisible={isVisible}
|
||||
/>
|
||||
<Board feedCacheKey={feed.key} viewType={context.viewType} boardIdentifier={context.boardIdentifier} isVisible={isVisible} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/v
|
||||
import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories';
|
||||
import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils';
|
||||
import { TimeFilter } from '../board-buttons';
|
||||
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
|
||||
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
|
||||
import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
|
||||
@@ -334,9 +333,6 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
|
||||
<div className={styles.boardSelect}>
|
||||
<strong>{t('board')}</strong>
|
||||
{boardSelect}
|
||||
{(isInAllView || isInSubscriptionsView) && (
|
||||
<TimeFilter isTopbar={true} isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.pageJump}>
|
||||
<Link to={useLocation().pathname.replace(/\/$/, '') + '/settings'}>{t('settings')}</Link>
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import assert from 'assert';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { isBoardFeedPageNumber } from '../lib/utils/route-utils';
|
||||
|
||||
// the timestamp the last time the user visited
|
||||
const lastVisitTimestamp = localStorage.getItem('5chanLastVisitTimestamp');
|
||||
|
||||
// Singleton pattern to ensure only one interval is ever created
|
||||
// This prevents memory leaks from multiple intervals being created during HMR or module re-evaluation
|
||||
// Using window object to persist across hot module reloads
|
||||
declare global {
|
||||
interface Window {
|
||||
_5chanLastVisitTimestampIntervalId?: number;
|
||||
}
|
||||
}
|
||||
|
||||
if (!window._5chanLastVisitTimestampIntervalId) {
|
||||
// update the last visited timestamp every n seconds
|
||||
window._5chanLastVisitTimestampIntervalId = window.setInterval(() => {
|
||||
localStorage.setItem('5chanLastVisitTimestamp', Date.now().toString());
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
const timeFilterNamesToSeconds: Record<string, number | undefined> = {
|
||||
'1h': 60 * 60,
|
||||
'12h': 60 * 60 * 12,
|
||||
'24h': 60 * 60 * 24,
|
||||
'48h': 60 * 60 * 24 * 2,
|
||||
'1w': 60 * 60 * 24 * 7,
|
||||
'1m': 60 * 60 * 24 * 30,
|
||||
'1y': 60 * 60 * 24 * 365,
|
||||
all: undefined,
|
||||
};
|
||||
|
||||
// calculate the last visit timeFilterNamesToSeconds
|
||||
const secondsSinceLastVisit = lastVisitTimestamp ? (Date.now() - parseInt(lastVisitTimestamp, 10)) / 1000 : Infinity;
|
||||
const day = 24 * 60 * 60;
|
||||
let lastVisitTimeFilterName: string | undefined;
|
||||
if (secondsSinceLastVisit > 30 * day) {
|
||||
lastVisitTimeFilterName = '1m';
|
||||
timeFilterNamesToSeconds[lastVisitTimeFilterName] = timeFilterNamesToSeconds['1m'];
|
||||
} else if (secondsSinceLastVisit > 7 * day) {
|
||||
const weeks = Math.ceil(secondsSinceLastVisit / day / 7);
|
||||
lastVisitTimeFilterName = `${weeks}w`;
|
||||
timeFilterNamesToSeconds[lastVisitTimeFilterName] = 60 * 60 * 24 * 7 * weeks;
|
||||
} else if (secondsSinceLastVisit > day) {
|
||||
const days = Math.ceil(secondsSinceLastVisit / day);
|
||||
lastVisitTimeFilterName = `${days}d`;
|
||||
timeFilterNamesToSeconds[lastVisitTimeFilterName] = 60 * 60 * 24 * days;
|
||||
} else {
|
||||
lastVisitTimeFilterName = '24h';
|
||||
timeFilterNamesToSeconds[lastVisitTimeFilterName] = timeFilterNamesToSeconds['24h'];
|
||||
}
|
||||
|
||||
export const timeFilterNames = [lastVisitTimeFilterName, '1h', '12h', '24h', '48h', '1w', '1m', '1y', 'all'];
|
||||
|
||||
export const timeFilterNameToSeconds = (timeFilterName: string | undefined): number | undefined => {
|
||||
if (!timeFilterName || timeFilterName === 'all') return undefined;
|
||||
|
||||
if (timeFilterName in timeFilterNamesToSeconds) {
|
||||
return timeFilterNamesToSeconds[timeFilterName as keyof typeof timeFilterNamesToSeconds];
|
||||
}
|
||||
|
||||
const match = timeFilterName.match(/^(\d+)([hdwmy])$/);
|
||||
if (match) {
|
||||
const [, value, unit] = match;
|
||||
const numValue = parseInt(value, 10);
|
||||
switch (unit) {
|
||||
case 'h':
|
||||
return numValue * 60 * 60;
|
||||
case 'd':
|
||||
return numValue * 24 * 60 * 60;
|
||||
case 'w':
|
||||
return numValue * 7 * 24 * 60 * 60;
|
||||
case 'm':
|
||||
return numValue * 30 * 24 * 60 * 60;
|
||||
case 'y':
|
||||
return numValue * 365 * 24 * 60 * 60;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function convertTimeStringToSeconds(timeString: string): number {
|
||||
const match = timeString.match(/^(\d+)([hdwmy])$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid time filter format: ${timeString}`);
|
||||
}
|
||||
|
||||
const [, value, unit] = match;
|
||||
const numValue = parseInt(value, 10);
|
||||
|
||||
switch (unit) {
|
||||
case 'h':
|
||||
return numValue * 60 * 60;
|
||||
case 'd':
|
||||
return numValue * 24 * 60 * 60;
|
||||
case 'w':
|
||||
return numValue * 7 * 24 * 60 * 60;
|
||||
case 'm':
|
||||
return numValue * 30 * 24 * 60 * 60;
|
||||
case 'y':
|
||||
return numValue * 365 * 24 * 60 * 60;
|
||||
default:
|
||||
throw new Error(`Invalid time unit: ${unit}`);
|
||||
}
|
||||
}
|
||||
|
||||
const useTimeFilter = () => {
|
||||
const params = useParams();
|
||||
let timeFilterName = params.timeFilterName;
|
||||
|
||||
// Ignore when param is a board feed page number (e.g. /all/3, /biz/2)
|
||||
if (timeFilterName && isBoardFeedPageNumber(timeFilterName)) {
|
||||
timeFilterName = undefined;
|
||||
}
|
||||
|
||||
// the default time filter is the last visit time filter
|
||||
if (!timeFilterName) {
|
||||
timeFilterName = lastVisitTimeFilterName;
|
||||
}
|
||||
|
||||
let timeFilterSeconds: number | undefined;
|
||||
|
||||
if (timeFilterName === 'all') {
|
||||
timeFilterSeconds = undefined;
|
||||
} else if (timeFilterName && timeFilterName in timeFilterNamesToSeconds) {
|
||||
timeFilterSeconds = timeFilterNamesToSeconds[timeFilterName as keyof typeof timeFilterNamesToSeconds];
|
||||
} else if (timeFilterName) {
|
||||
try {
|
||||
timeFilterSeconds = convertTimeStringToSeconds(timeFilterName);
|
||||
} catch {
|
||||
console.error(`Invalid time filter format: ${timeFilterName}`);
|
||||
timeFilterSeconds = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have a valid timeFilterSeconds, use the default (24h)
|
||||
if (timeFilterSeconds === undefined && timeFilterName !== 'all') {
|
||||
timeFilterSeconds = timeFilterNamesToSeconds['24h'];
|
||||
}
|
||||
|
||||
assert(timeFilterName === 'all' || timeFilterSeconds !== undefined, `useTimeFilter no filter for timeFilterName '${timeFilterName}'`);
|
||||
|
||||
return { timeFilterSeconds, timeFilterNames, timeFilterName, lastVisitTimeFilterName };
|
||||
};
|
||||
|
||||
export default useTimeFilter;
|
||||
@@ -1,15 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { normalizeMultiboardFeedPath } from '../route-utils';
|
||||
import { isFeedRoute, normalizeMultiboardFeedPath } from '../route-utils';
|
||||
|
||||
describe('normalizeMultiboardFeedPath', () => {
|
||||
it('normalizes /all/3 -> /all', () => {
|
||||
expect(normalizeMultiboardFeedPath('/all/3')).toBe('/all');
|
||||
});
|
||||
|
||||
it('normalizes /all/1w/3 -> /all/1w', () => {
|
||||
expect(normalizeMultiboardFeedPath('/all/1w/3')).toBe('/all/1w');
|
||||
});
|
||||
|
||||
it('normalizes /subs/2/settings -> /subs/settings', () => {
|
||||
expect(normalizeMultiboardFeedPath('/subs/2/settings')).toBe('/subs/settings');
|
||||
});
|
||||
@@ -21,9 +17,28 @@ describe('normalizeMultiboardFeedPath', () => {
|
||||
it('leaves non-multiboard paths unchanged', () => {
|
||||
expect(normalizeMultiboardFeedPath('/biz')).toBe('/biz');
|
||||
expect(normalizeMultiboardFeedPath('/biz/3')).toBe('/biz/3');
|
||||
expect(normalizeMultiboardFeedPath('/biz/1w/2')).toBe('/biz/1w/2');
|
||||
expect(normalizeMultiboardFeedPath('/biz/catalog/4')).toBe('/biz/catalog/4');
|
||||
expect(normalizeMultiboardFeedPath('/pending/0')).toBe('/pending/0');
|
||||
expect(normalizeMultiboardFeedPath('/')).toBe('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFeedRoute', () => {
|
||||
it('returns true for canonical multiboard paths', () => {
|
||||
expect(isFeedRoute('/all')).toBe(true);
|
||||
expect(isFeedRoute('/all/catalog')).toBe(true);
|
||||
expect(isFeedRoute('/subs')).toBe(true);
|
||||
expect(isFeedRoute('/subs/catalog')).toBe(true);
|
||||
expect(isFeedRoute('/mod')).toBe(true);
|
||||
expect(isFeedRoute('/mod/catalog')).toBe(true);
|
||||
expect(isFeedRoute('/all/3')).toBe(true);
|
||||
expect(isFeedRoute('/subs/2')).toBe(true);
|
||||
expect(isFeedRoute('/mod/catalog/4')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for time-filter paths', () => {
|
||||
expect(isFeedRoute('/all/24h')).toBe(false);
|
||||
expect(isFeedRoute('/subs/catalog/1w')).toBe(false);
|
||||
expect(isFeedRoute('/all/1w/3')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,19 +109,12 @@ export const isFeedRoute = (pathname: string): boolean => {
|
||||
if (normalizedPath.includes('/modqueue')) return false;
|
||||
|
||||
const pathWithoutSettings = normalizedPath.replace(/\/settings$/, '');
|
||||
|
||||
if (pathWithoutSettings.startsWith('/all')) return true;
|
||||
if (pathWithoutSettings.startsWith('/subs')) return true;
|
||||
if (pathWithoutSettings.startsWith('/mod')) return true;
|
||||
|
||||
const segments = pathWithoutSettings.split('/').filter(Boolean);
|
||||
if (segments.length >= 1) {
|
||||
if (segments.length === 1) return true;
|
||||
if (segments.length === 2 && segments[1] === 'catalog') return true;
|
||||
if (segments.length === 2 && (/^(?:\d+(?:h|d|w|m|y)|all)$/.test(segments[1]) || /^([1-9]|10)$/.test(segments[1]))) return true;
|
||||
if (segments.length === 3 && segments[1] === 'catalog' && /^(?:\d+(?:h|d|w|m|y)|all)$/.test(segments[2])) return true;
|
||||
if (segments.length === 3 && /^(?:\d+(?:h|d|w|m|y)|all)$/.test(segments[1]) && /^([1-9]|10)$/.test(segments[2])) return true;
|
||||
if (segments.length === 2 && segments[0] !== 'all' && segments[0] !== 'subs' && segments[0] !== 'mod' && /^([1-9]|10)$/.test(segments[1])) return true;
|
||||
if (segments.length === 2 && /^([1-9]|10)$/.test(segments[1])) return true;
|
||||
if (segments.length === 3 && segments[1] === 'catalog' && /^([1-9]|10)$/.test(segments[2])) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -163,8 +156,8 @@ function isMultiboardFeedPath(pathname: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize multiboard feed paths by removing trailing page-number segments (1–10),
|
||||
* while preserving /settings and valid time-filter segments.
|
||||
* Normalize multiboard feed paths by removing trailing page-number segments (1–10)
|
||||
* and preserving /settings.
|
||||
* Non-multiboard paths are returned unchanged.
|
||||
*/
|
||||
export const normalizeMultiboardFeedPath = (pathname: string): string => {
|
||||
|
||||
@@ -3,7 +3,6 @@ export type ParamsType = {
|
||||
boardIdentifier?: string;
|
||||
commentCid?: string;
|
||||
subplebbitAddress?: string; // deprecated, kept for backward compatibility
|
||||
timeFilterName?: string;
|
||||
};
|
||||
|
||||
export const isAllView = (pathname: string): boolean => {
|
||||
@@ -30,7 +29,7 @@ export const isBoardView = (pathname: string, params: ParamsType): boolean => {
|
||||
};
|
||||
|
||||
export const isCatalogView = (pathname: string, params: ParamsType): boolean => {
|
||||
const { boardIdentifier, subplebbitAddress, timeFilterName } = params;
|
||||
const { boardIdentifier, subplebbitAddress } = params;
|
||||
const identifier = boardIdentifier || subplebbitAddress;
|
||||
const decodedPathname = decodeURIComponent(pathname);
|
||||
|
||||
@@ -38,16 +37,10 @@ export const isCatalogView = (pathname: string, params: ParamsType): boolean =>
|
||||
(identifier && (decodedPathname === `/${identifier}/catalog` || decodedPathname === `/${identifier}/catalog/settings`)) ||
|
||||
decodedPathname === `/all/catalog` ||
|
||||
decodedPathname === `/all/catalog/settings` ||
|
||||
decodedPathname === `/all/catalog/${timeFilterName}` ||
|
||||
decodedPathname === `/all/catalog/${timeFilterName}/settings` ||
|
||||
decodedPathname === `/subs/catalog` ||
|
||||
decodedPathname === `/subs/catalog/settings` ||
|
||||
decodedPathname === `/subs/catalog/${timeFilterName}` ||
|
||||
decodedPathname === `/subs/catalog/${timeFilterName}/settings` ||
|
||||
decodedPathname === `/mod/catalog` ||
|
||||
decodedPathname === `/mod/catalog/settings` ||
|
||||
decodedPathname === `/mod/catalog/${timeFilterName}` ||
|
||||
decodedPathname === `/mod/catalog/${timeFilterName}/settings`
|
||||
decodedPathname === `/mod/catalog/settings`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -83,17 +76,7 @@ export const isSettingsView = (pathname: string, params: ParamsType): boolean =>
|
||||
};
|
||||
|
||||
export const isSubscriptionsView = (pathname: string, params: ParamsType): boolean => {
|
||||
const { timeFilterName } = params;
|
||||
return (
|
||||
pathname === '/subs' ||
|
||||
pathname === '/subs/settings' ||
|
||||
pathname === `/subs/${timeFilterName}` ||
|
||||
pathname === `/subs/${timeFilterName}/settings` ||
|
||||
pathname === '/subs/catalog' ||
|
||||
pathname === '/subs/catalog/settings' ||
|
||||
pathname === `/subs/catalog/${timeFilterName}` ||
|
||||
pathname === `/subs/catalog/${timeFilterName}/settings`
|
||||
);
|
||||
return pathname === '/subs' || pathname === '/subs/settings' || pathname === '/subs/catalog' || pathname === '/subs/catalog/settings';
|
||||
};
|
||||
|
||||
export const isNotFoundView = (pathname: string, params: ParamsType): boolean => {
|
||||
|
||||
+9
-143
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { Comment, useAccount, useAccountComments, useAccountSubplebbits, useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
@@ -8,9 +8,8 @@ import styles from './board.module.css';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
import { useDirectoryAddresses, useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
||||
import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||
import useTimeFilter, { timeFilterNameToSeconds } from '../../hooks/use-time-filter';
|
||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||
import useSortingStore from '../../stores/use-sorting-store';
|
||||
import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store';
|
||||
@@ -33,13 +32,6 @@ interface BoardFooterProps {
|
||||
isInAllView: boolean;
|
||||
isInSubscriptionsView: boolean;
|
||||
isInModView: boolean;
|
||||
showMorePostsSuggestion: boolean;
|
||||
feedLength: number;
|
||||
weeklyFeedLength: number;
|
||||
monthlyFeedLength: number;
|
||||
yearlyFeedLength: number;
|
||||
boardPath: string | undefined;
|
||||
currentTimeFilterName: string | undefined;
|
||||
subplebbitState: string | undefined;
|
||||
subscriptionsLength: number;
|
||||
accountSubplebbitAddressesLength: number;
|
||||
@@ -59,13 +51,6 @@ const BoardFooter = ({
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
showMorePostsSuggestion,
|
||||
feedLength,
|
||||
weeklyFeedLength,
|
||||
monthlyFeedLength,
|
||||
yearlyFeedLength,
|
||||
boardPath,
|
||||
currentTimeFilterName,
|
||||
subplebbitState,
|
||||
subscriptionsLength,
|
||||
accountSubplebbitAddressesLength,
|
||||
@@ -73,11 +58,7 @@ const BoardFooter = ({
|
||||
}: BoardFooterProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const loadingStateString =
|
||||
useFeedStateString(subplebbitAddresses) ||
|
||||
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength)
|
||||
? t('loading_feed')
|
||||
: t('looking_for_more_posts'));
|
||||
const loadingStateString = useFeedStateString(subplebbitAddresses) || (combinedFeedLength === 0 ? t('loading_feed') : t('looking_for_more_posts'));
|
||||
|
||||
let footerContent;
|
||||
if (combinedFeedLength === 0) {
|
||||
@@ -86,7 +67,7 @@ const BoardFooter = ({
|
||||
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
|
||||
footerContent = (
|
||||
<>
|
||||
{subplebbitAddressesWithNewerPosts.length > 0 ? (
|
||||
{subplebbitAddressesWithNewerPosts.length > 0 && (
|
||||
<div className={styles.morePostsSuggestion}>
|
||||
<Trans
|
||||
i18nKey='newer_threads_available'
|
||||
@@ -95,44 +76,6 @@ const BoardFooter = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
(isInAllView || isInSubscriptionsView || isInModView) &&
|
||||
showMorePostsSuggestion &&
|
||||
(monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
|
||||
(() => {
|
||||
const basePath = isInAllView ? '/all' : isInSubscriptionsView ? '/subs' : isInModView ? '/mod' : boardPath ? `/${boardPath}` : '';
|
||||
return weeklyFeedLength > feedLength ? (
|
||||
<div className={styles.morePostsSuggestion}>
|
||||
<Trans
|
||||
i18nKey='more_threads_last_week'
|
||||
values={{ currentTimeFilterName, count: feedLength }}
|
||||
components={{
|
||||
1: <Link to={`${basePath}/1w`} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : monthlyFeedLength > feedLength ? (
|
||||
<div className={styles.morePostsSuggestion}>
|
||||
<Trans
|
||||
i18nKey='more_threads_last_month'
|
||||
values={{ currentTimeFilterName, count: feedLength }}
|
||||
components={{
|
||||
1: <Link to={`${basePath}/1m`} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.morePostsSuggestion}>
|
||||
<Trans
|
||||
i18nKey='more_threads_last_year'
|
||||
values={{ currentTimeFilterName, count: feedLength }}
|
||||
components={{
|
||||
1: <Link to={`${basePath}/1y`} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -159,11 +102,10 @@ export interface BoardProps {
|
||||
feedCacheKey?: string;
|
||||
viewType?: 'all' | 'subs' | 'mod' | 'board';
|
||||
boardIdentifier?: string;
|
||||
timeFilterNameFromCache?: string;
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: BoardProps) => {
|
||||
const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, isVisible = true }: BoardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
@@ -180,7 +122,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
return resolvedAddressFromUrl;
|
||||
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
|
||||
|
||||
const boardPath = useBoardPath(subplebbitAddress);
|
||||
const directoryAddresses = useDirectoryAddresses();
|
||||
const filteredDirectoryAddresses = useFilteredDirectoryAddresses();
|
||||
|
||||
@@ -204,9 +145,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
}, [isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directoryAddresses, filteredDirectoryAddresses, subscriptions, accountSubplebbitAddresses]);
|
||||
|
||||
const { sortType } = useSortingStore();
|
||||
const { timeFilterSeconds: timeFilterSecondsFromHook, timeFilterName: timeFilterNameFromHook } = useTimeFilter();
|
||||
const timeFilterName = timeFilterNameFromCache || timeFilterNameFromHook;
|
||||
const timeFilterSeconds = timeFilterNameFromCache ? timeFilterNameToSeconds(timeFilterNameFromCache) : timeFilterSecondsFromHook;
|
||||
|
||||
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
|
||||
const isForcedInfiniteScroll = isInAllView || isInSubscriptionsView || isInModView;
|
||||
@@ -219,25 +157,14 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
postsPerPage: effectiveInfiniteScroll ? infiniteFeedPostsPerPage : paginationFeedPostsPerPage,
|
||||
...(isInAllView || isInSubscriptionsView || isInModView ? { newerThan: timeFilterSeconds } : {}),
|
||||
}),
|
||||
[
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
effectiveInfiniteScroll,
|
||||
infiniteFeedPostsPerPage,
|
||||
paginationFeedPostsPerPage,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
timeFilterSeconds,
|
||||
],
|
||||
[subplebbitAddresses, sortType, effectiveInfiniteScroll, infiniteFeedPostsPerPage, paginationFeedPostsPerPage],
|
||||
);
|
||||
|
||||
const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions);
|
||||
const { accountComments } = useAccountComments();
|
||||
|
||||
const feedContextKey = `${isInAllView ? 'all' : isInSubscriptionsView ? 'subs' : isInModView ? 'mod' : (subplebbitAddress ?? 'board')}-${sortType}-${timeFilterSeconds}-${viewType ?? 'board'}-${effectiveInfiniteScroll}`;
|
||||
const feedContextKey = `${isInAllView ? 'all' : isInSubscriptionsView ? 'subs' : isInModView ? 'mod' : (subplebbitAddress ?? 'board')}-${sortType}-${viewType ?? 'board'}-${effectiveInfiniteScroll}`;
|
||||
const pathWithoutSettings = location.pathname.replace(/\/settings$/, '');
|
||||
const currentPage = getPageFromFeedPath(pathWithoutSettings);
|
||||
const paginationBasePath = stripPageFromFeedPath(pathWithoutSettings);
|
||||
@@ -338,39 +265,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// suggest the user to change time filter if there aren't enough posts
|
||||
const { feed: weeklyFeed } = useFeed({
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 7,
|
||||
});
|
||||
const { feed: monthlyFeed } = useFeed({
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 30,
|
||||
});
|
||||
const { feed: yearlyFeed } = useFeed({
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 365,
|
||||
});
|
||||
|
||||
const feedLength = feed.length;
|
||||
const weeklyFeedLength = weeklyFeed.length;
|
||||
const monthlyFeedLength = monthlyFeed.length;
|
||||
const yearlyFeedLength = yearlyFeed.length;
|
||||
|
||||
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setShowMorePostsSuggestion(true);
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
|
||||
|
||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||
// Note: useFeedStateString is called inside BoardFooter to isolate re-renders from backend state changes
|
||||
const footerComponents = useMemo(
|
||||
@@ -385,13 +279,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||
feedLength={feedLength}
|
||||
weeklyFeedLength={weeklyFeedLength}
|
||||
monthlyFeedLength={monthlyFeedLength}
|
||||
yearlyFeedLength={yearlyFeedLength}
|
||||
boardPath={boardPath}
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
subplebbitState={subplebbitState}
|
||||
subscriptionsLength={subscriptions?.length || 0}
|
||||
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
|
||||
@@ -408,13 +295,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
showMorePostsSuggestion,
|
||||
feedLength,
|
||||
weeklyFeedLength,
|
||||
monthlyFeedLength,
|
||||
yearlyFeedLength,
|
||||
boardPath,
|
||||
currentTimeFilterName,
|
||||
subplebbitState,
|
||||
subscriptions?.length,
|
||||
accountSubplebbitAddresses?.length,
|
||||
@@ -424,7 +304,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
);
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}`;
|
||||
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}` : `${location.pathname}-${sortType}`;
|
||||
const navigationType = useNavigationType();
|
||||
|
||||
const hasBeenVisibleRef = useRef(false);
|
||||
@@ -514,13 +394,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||
feedLength={feedLength}
|
||||
weeklyFeedLength={weeklyFeedLength}
|
||||
monthlyFeedLength={monthlyFeedLength}
|
||||
yearlyFeedLength={yearlyFeedLength}
|
||||
boardPath={boardPath}
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
subplebbitState={subplebbitState}
|
||||
subscriptionsLength={subscriptions?.length || 0}
|
||||
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
|
||||
@@ -544,13 +417,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||
feedLength={feedLength}
|
||||
weeklyFeedLength={weeklyFeedLength}
|
||||
monthlyFeedLength={monthlyFeedLength}
|
||||
yearlyFeedLength={yearlyFeedLength}
|
||||
boardPath={boardPath}
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
subplebbitState={subplebbitState}
|
||||
subscriptionsLength={subscriptions?.length || 0}
|
||||
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
|
||||
|
||||
+13
-179
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { Link, useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Comment, useAccount, useFeed, useSubplebbit, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
@@ -7,9 +7,8 @@ import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows';
|
||||
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size';
|
||||
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
||||
import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||
import useTimeFilter, { timeFilterNameToSeconds } from '../../hooks/use-time-filter';
|
||||
import useWindowWidth from '../../hooks/use-window-width';
|
||||
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
|
||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||
@@ -28,19 +27,12 @@ const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
interface CatalogFooterProps {
|
||||
subplebbitAddresses: string[];
|
||||
hasMore: boolean;
|
||||
feedLength: number;
|
||||
combinedFeedLength: number;
|
||||
subplebbitAddressesWithNewerPosts: string[];
|
||||
onNewerPostsClick: () => void;
|
||||
isInAllView: boolean;
|
||||
isInSubscriptionsView: boolean;
|
||||
isInModView: boolean;
|
||||
showMorePostsSuggestion: boolean;
|
||||
weeklyFeedLength: number;
|
||||
monthlyFeedLength: number;
|
||||
yearlyFeedLength: number;
|
||||
boardPath: string | undefined;
|
||||
currentTimeFilterName: string | undefined;
|
||||
/** When false, suppress the loading ellipsis (e.g. non-infinite mode) */
|
||||
showLoadingEllipsis?: boolean;
|
||||
}
|
||||
@@ -51,39 +43,26 @@ interface CatalogFooterProps {
|
||||
const CatalogFooter = ({
|
||||
subplebbitAddresses,
|
||||
hasMore,
|
||||
feedLength,
|
||||
combinedFeedLength,
|
||||
subplebbitAddressesWithNewerPosts,
|
||||
onNewerPostsClick,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
showMorePostsSuggestion,
|
||||
weeklyFeedLength,
|
||||
monthlyFeedLength,
|
||||
yearlyFeedLength,
|
||||
boardPath,
|
||||
currentTimeFilterName,
|
||||
showLoadingEllipsis = true,
|
||||
}: CatalogFooterProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const loadingStateString =
|
||||
useFeedStateString(subplebbitAddresses) ||
|
||||
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
|
||||
? t('loading_feed')
|
||||
: t('looking_for_more_posts');
|
||||
const loadingStateString = useFeedStateString(subplebbitAddresses) || (combinedFeedLength === 0 ? t('loading_feed') : t('looking_for_more_posts'));
|
||||
|
||||
let footerContent;
|
||||
if (feedLength === 0) {
|
||||
if (combinedFeedLength === 0) {
|
||||
footerContent = t('no_threads');
|
||||
}
|
||||
if (combinedFeedLength === 0) {
|
||||
footerContent = t('no_threads');
|
||||
}
|
||||
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
|
||||
footerContent = (
|
||||
<>
|
||||
{subplebbitAddressesWithNewerPosts.length > 0 ? (
|
||||
{subplebbitAddressesWithNewerPosts.length > 0 && (
|
||||
<div className={styles.stateString}>
|
||||
<Trans
|
||||
i18nKey='newer_threads_available'
|
||||
@@ -92,53 +71,6 @@ const CatalogFooter = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
(isInAllView || isInSubscriptionsView || isInModView) &&
|
||||
showMorePostsSuggestion &&
|
||||
(monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
|
||||
(weeklyFeedLength > feedLength ? (
|
||||
<div className={styles.stateString}>
|
||||
<Trans
|
||||
i18nKey='more_threads_last_week'
|
||||
values={{ currentTimeFilterName, count: feedLength }}
|
||||
components={{
|
||||
1: (
|
||||
<Link
|
||||
to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : isInModView ? '/mod/catalog' : `/${boardPath}/catalog`) + '/1w'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : monthlyFeedLength > feedLength ? (
|
||||
<div className={styles.stateString}>
|
||||
<Trans
|
||||
i18nKey='more_threads_last_month'
|
||||
values={{ currentTimeFilterName, count: feedLength }}
|
||||
components={{
|
||||
1: (
|
||||
<Link
|
||||
to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : isInModView ? '/mod/catalog' : `/${boardPath}/catalog`) + '/1m'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.stateString}>
|
||||
<Trans
|
||||
i18nKey='more_threads_last_year'
|
||||
values={{ currentTimeFilterName, count: feedLength }}
|
||||
components={{
|
||||
1: (
|
||||
<Link
|
||||
to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : isInModView ? '/mod/catalog' : `/${boardPath}/catalog`) + '/1y'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{showLoadingEllipsis && (
|
||||
<div className={styles.stateString}>
|
||||
@@ -156,35 +88,17 @@ const CatalogFooter = ({
|
||||
interface CatalogLoadingProps {
|
||||
subplebbitAddresses: string[];
|
||||
hasMore: boolean;
|
||||
feedLength: number;
|
||||
weeklyFeedLength: number;
|
||||
monthlyFeedLength: number;
|
||||
yearlyFeedLength: number;
|
||||
combinedFeedLength: number;
|
||||
state: string | undefined;
|
||||
subscriptionsLength: number;
|
||||
combinedFeedLength: number;
|
||||
error: Error | undefined;
|
||||
}
|
||||
|
||||
const CatalogLoading = ({
|
||||
subplebbitAddresses,
|
||||
hasMore,
|
||||
feedLength,
|
||||
weeklyFeedLength,
|
||||
monthlyFeedLength,
|
||||
yearlyFeedLength,
|
||||
state,
|
||||
subscriptionsLength,
|
||||
combinedFeedLength,
|
||||
error,
|
||||
}: CatalogLoadingProps) => {
|
||||
const CatalogLoading = ({ subplebbitAddresses, hasMore, combinedFeedLength, state, subscriptionsLength, error }: CatalogLoadingProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const rawFeedStateString = useFeedStateString(subplebbitAddresses);
|
||||
const loadingStateString =
|
||||
rawFeedStateString || (feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
|
||||
? t('loading_feed')
|
||||
: t('looking_for_more_posts');
|
||||
const loadingStateString = rawFeedStateString || (combinedFeedLength === 0 ? t('loading_feed') : t('looking_for_more_posts'));
|
||||
|
||||
return (
|
||||
<div className={styles.stateString}>
|
||||
@@ -289,11 +203,10 @@ export interface CatalogProps {
|
||||
feedCacheKey?: string;
|
||||
viewType?: 'all' | 'subs' | 'mod' | 'board';
|
||||
boardIdentifier?: string;
|
||||
timeFilterNameFromCache?: string;
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: CatalogProps) => {
|
||||
const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, isVisible = true }: CatalogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@@ -316,7 +229,6 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
return resolvedAddressFromUrl;
|
||||
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
|
||||
|
||||
const boardPath = useBoardPath(subplebbitAddress);
|
||||
const { filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore();
|
||||
|
||||
const account = useAccount();
|
||||
@@ -352,10 +264,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
}
|
||||
}, [isInAllView, isInSubscriptionsView, isInModView, location.pathname, navigate]);
|
||||
|
||||
const { timeFilterSeconds: timeFilterSecondsFromHook, timeFilterName: timeFilterNameFromHook } = useTimeFilter();
|
||||
const { sortType } = useSortingStore();
|
||||
const timeFilterName = timeFilterNameFromCache || timeFilterNameFromHook;
|
||||
const timeFilterSeconds = timeFilterNameFromCache ? timeFilterNameToSeconds(timeFilterNameFromCache) : timeFilterSecondsFromHook;
|
||||
|
||||
// Create a stable callback for filter matching
|
||||
const handleFilterMatch = useCallback((filterIndex: number, cid: string, subplebbitAddress: string) => {
|
||||
@@ -380,18 +289,12 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
? infiniteFeedPostsPerPage
|
||||
: paginationFeedPostsPerPage;
|
||||
|
||||
const options: any = {
|
||||
return {
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
postsPerPage: catalogPostsPerPage,
|
||||
filter: createCombinedFilter(filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
|
||||
};
|
||||
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
options.newerThan = timeFilterSeconds;
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
@@ -401,7 +304,6 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
effectiveInfiniteScroll,
|
||||
infiniteFeedPostsPerPage,
|
||||
paginationFeedPostsPerPage,
|
||||
timeFilterSeconds,
|
||||
filterItems,
|
||||
searchText,
|
||||
subplebbitAddress,
|
||||
@@ -474,36 +376,6 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
};
|
||||
|
||||
// suggest the user to change time filter if there aren't enough posts
|
||||
const { feed: weeklyFeed } = useFeed({
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 7,
|
||||
filter: createCombinedFilter(filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
|
||||
});
|
||||
|
||||
const { feed: monthlyFeed } = useFeed({
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 30,
|
||||
filter: createCombinedFilter(filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
|
||||
});
|
||||
|
||||
const { feed: yearlyFeed } = useFeed({
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 365,
|
||||
filter: createCombinedFilter(filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
|
||||
});
|
||||
|
||||
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setShowMorePostsSuggestion(true);
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const setResetFunction = useFeedResetStore((state) => state.setResetFunction);
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
@@ -513,12 +385,6 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { error, shortAddress, state, title } = subplebbit || {};
|
||||
const feedLength = feed.length;
|
||||
const weeklyFeedLength = weeklyFeed.length;
|
||||
const monthlyFeedLength = monthlyFeed.length;
|
||||
const yearlyFeedLength = yearlyFeed.length;
|
||||
|
||||
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
|
||||
|
||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||
// Note: useFeedStateString is called inside CatalogFooter to isolate re-renders from backend state changes
|
||||
@@ -528,19 +394,12 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
<CatalogFooter
|
||||
subplebbitAddresses={subplebbitAddresses}
|
||||
hasMore={hasMore}
|
||||
feedLength={feedLength}
|
||||
combinedFeedLength={cappedFeed.length}
|
||||
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
|
||||
onNewerPostsClick={handleNewerPostsButtonClick}
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||
weeklyFeedLength={weeklyFeedLength}
|
||||
monthlyFeedLength={monthlyFeedLength}
|
||||
yearlyFeedLength={yearlyFeedLength}
|
||||
boardPath={boardPath}
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
showLoadingEllipsis={effectiveInfiniteScroll}
|
||||
/>
|
||||
),
|
||||
@@ -548,19 +407,12 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
[
|
||||
subplebbitAddresses,
|
||||
hasMore,
|
||||
feedLength,
|
||||
cappedFeed.length,
|
||||
subplebbitAddressesWithNewerPosts,
|
||||
handleNewerPostsButtonClick,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
showMorePostsSuggestion,
|
||||
weeklyFeedLength,
|
||||
monthlyFeedLength,
|
||||
yearlyFeedLength,
|
||||
boardPath,
|
||||
currentTimeFilterName,
|
||||
effectiveInfiniteScroll,
|
||||
],
|
||||
);
|
||||
@@ -603,7 +455,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const rows = useCatalogFeedRows(columnCount, processedFeed, isFeedLoaded, subplebbit);
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}-catalog`;
|
||||
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}` : `${location.pathname}-${sortType}-catalog`;
|
||||
const navigationType = useNavigationType();
|
||||
|
||||
const hasBeenVisibleRef = useRef(false);
|
||||
@@ -709,19 +561,12 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
<CatalogFooter
|
||||
subplebbitAddresses={subplebbitAddresses}
|
||||
hasMore={hasMore}
|
||||
feedLength={feedLength}
|
||||
combinedFeedLength={cappedFeed.length}
|
||||
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
|
||||
onNewerPostsClick={handleNewerPostsButtonClick}
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||
weeklyFeedLength={weeklyFeedLength}
|
||||
monthlyFeedLength={monthlyFeedLength}
|
||||
yearlyFeedLength={yearlyFeedLength}
|
||||
boardPath={boardPath}
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
showLoadingEllipsis={true}
|
||||
/>
|
||||
</>
|
||||
@@ -734,19 +579,12 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
<CatalogFooter
|
||||
subplebbitAddresses={subplebbitAddresses}
|
||||
hasMore={hasMore}
|
||||
feedLength={feedLength}
|
||||
combinedFeedLength={cappedFeed.length}
|
||||
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
|
||||
onNewerPostsClick={handleNewerPostsButtonClick}
|
||||
isInAllView={isInAllView}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||
weeklyFeedLength={weeklyFeedLength}
|
||||
monthlyFeedLength={monthlyFeedLength}
|
||||
yearlyFeedLength={yearlyFeedLength}
|
||||
boardPath={boardPath}
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
showLoadingEllipsis={false}
|
||||
/>
|
||||
</>
|
||||
@@ -757,13 +595,9 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
<CatalogLoading
|
||||
subplebbitAddresses={subplebbitAddresses}
|
||||
hasMore={hasMore}
|
||||
feedLength={feedLength}
|
||||
weeklyFeedLength={weeklyFeedLength}
|
||||
monthlyFeedLength={monthlyFeedLength}
|
||||
yearlyFeedLength={yearlyFeedLength}
|
||||
combinedFeedLength={cappedFeed.length}
|
||||
state={state}
|
||||
subscriptionsLength={isInSubscriptionsView ? subscriptions?.length || 0 : 1}
|
||||
combinedFeedLength={cappedFeed.length}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user