diff --git a/src/app.tsx b/src/app.tsx
index b1c1aee1..cadb588f 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -9,10 +9,8 @@ import useSpecialThemeStore from './stores/use-special-theme-store';
import useIsMobile from './hooks/use-is-mobile';
import useTheme from './hooks/use-theme';
import { useDefaultSubplebbits } from './hooks/use-default-subplebbits';
-import { getSubplebbitAddress } from './lib/utils/route-utils';
+import { getSubplebbitAddress, isPostRoute, isPendingPostRoute } from './lib/utils/route-utils';
import styles from './app.module.css';
-import Board from './views/board';
-import Catalog from './views/catalog';
import FAQ from './views/faq';
import Home from './views/home';
import NotFound from './views/not-found';
@@ -22,6 +20,7 @@ import { DesktopBoardButtons, MobileBoardButtons } from './components/board-butt
import BoardHeader from './components/board-header';
import ChallengeModal from './components/challenge-modal';
import CreateBoardModal from './components/create-board-modal';
+import FeedCacheContainer from './components/feed-cache-container';
import ReplyModal from './components/reply-modal';
import PostForm from './components/post-form';
import SubplebbitStats from './components/subplebbit-stats';
@@ -43,6 +42,9 @@ const BoardLayout = () => {
const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined });
const { closeCreateBoardModal } = useCreateBoardModalStore();
+ const isOnPostRoute = isPostRoute(location.pathname);
+ const isOnPendingPostRoute = isPendingPostRoute(location.pathname);
+
// Christmas theme
const { isEnabled: isSpecialEnabled } = useSpecialThemeStore();
useEffect(() => {
@@ -86,7 +88,8 @@ const BoardLayout = () => {
>
)}
-
+
+ {(isOnPostRoute || isOnPendingPostRoute) && }
);
};
@@ -141,36 +144,36 @@ const App = () => (
} />
} />
}>
- } />
- } />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
} />
- } />
- } />
-
- } />
- } />
- } />
- } />
-
- } />
- } />
- } />
- } />
-
- } />
- } />
-
- } />
- } />
- } />
- } />
-
} />
} />
} />
} />
} />
} />
+
+ } />
+ } />
} />
} />
diff --git a/src/components/feed-cache-container/feed-cache-container.module.css b/src/components/feed-cache-container/feed-cache-container.module.css
new file mode 100644
index 00000000..79003f5c
--- /dev/null
+++ b/src/components/feed-cache-container/feed-cache-container.module.css
@@ -0,0 +1,13 @@
+.visible {
+}
+
+.hidden {
+ visibility: hidden;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ pointer-events: none;
+ overflow: hidden;
+ height: 0;
+}
diff --git a/src/components/feed-cache-container/feed-cache-container.tsx b/src/components/feed-cache-container/feed-cache-container.tsx
new file mode 100644
index 00000000..3547e1ec
--- /dev/null
+++ b/src/components/feed-cache-container/feed-cache-container.tsx
@@ -0,0 +1,99 @@
+import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+import useFeedCacheStore, { CachedFeed } from '../../stores/use-feed-cache-store';
+import { getFeedCacheKey, getFeedType, isFeedRoute } from '../../lib/utils/route-utils';
+import Board from '../../views/board';
+import Catalog from '../../views/catalog';
+import styles from './feed-cache-container.module.css';
+
+interface FeedContextFromKey {
+ viewType: 'all' | 'subs' | 'mod' | 'board';
+ boardIdentifier?: string;
+ timeFilterName?: string;
+}
+
+const parseFeedKey = (key: string): FeedContextFromKey => {
+ const segments = key.split('/').filter(Boolean);
+
+ const filteredSegments = segments.filter((s) => s !== 'catalog');
+ if (filteredSegments[0] === 'all') {
+ return {
+ viewType: 'all',
+ timeFilterName: filteredSegments[1],
+ };
+ }
+ if (filteredSegments[0] === 'subs') {
+ return {
+ viewType: 'subs',
+ timeFilterName: filteredSegments[1],
+ };
+ }
+ if (filteredSegments[0] === 'mod') {
+ return {
+ viewType: 'mod',
+ timeFilterName: filteredSegments[1],
+ };
+ }
+
+ return {
+ viewType: 'board',
+ boardIdentifier: filteredSegments[0],
+ timeFilterName: filteredSegments[1],
+ };
+};
+
+interface CachedFeedWrapperProps {
+ feed: CachedFeed;
+ isVisible: boolean;
+}
+
+const CachedFeedWrapper = ({ feed, isVisible }: CachedFeedWrapperProps) => {
+ const context = parseFeedKey(feed.key);
+
+ return (
+
+ {feed.type === 'catalog' ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const FeedCacheContainer = () => {
+ const location = useLocation();
+ const { cachedFeeds, accessFeed } = useFeedCacheStore();
+
+ const currentFeedKey = getFeedCacheKey(location.pathname);
+ const isOnFeedRoute = isFeedRoute(location.pathname);
+ const feedType = getFeedType(location.pathname);
+
+ useEffect(() => {
+ if (isOnFeedRoute && currentFeedKey && feedType) {
+ accessFeed(currentFeedKey, feedType);
+ }
+ }, [currentFeedKey, isOnFeedRoute, feedType, accessFeed]);
+
+ return (
+ <>
+ {cachedFeeds.map((feed) => (
+
+ ))}
+ >
+ );
+};
+
+export default FeedCacheContainer;
diff --git a/src/components/feed-cache-container/index.ts b/src/components/feed-cache-container/index.ts
new file mode 100644
index 00000000..74e8a166
--- /dev/null
+++ b/src/components/feed-cache-container/index.ts
@@ -0,0 +1 @@
+export { default } from './feed-cache-container';
diff --git a/src/lib/utils/route-utils.ts b/src/lib/utils/route-utils.ts
index 265154fa..693dc990 100644
--- a/src/lib/utils/route-utils.ts
+++ b/src/lib/utils/route-utils.ts
@@ -100,3 +100,85 @@ export const isDirectoryBoard = (identifier: string, subplebbits: MultisubSubple
const directoryToAddress = getDirectoryToAddressMap(subplebbits);
return directoryToAddress.has(identifier);
};
+
+export const isFeedRoute = (pathname: string): boolean => {
+ const normalizedPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
+
+ if (normalizedPath.includes('/thread/')) return false;
+ if (normalizedPath.endsWith('/description')) return false;
+ if (normalizedPath.endsWith('/rules')) return false;
+ if (normalizedPath.startsWith('/pending/')) 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 && /^(1h|24h|1w|1m|1y|all)$/.test(segments[1])) return true;
+ if (segments.length === 3 && segments[1] === 'catalog' && /^(1h|24h|1w|1m|1y|all)$/.test(segments[2])) return true;
+ }
+
+ return false;
+};
+
+export const isPostRoute = (pathname: string): boolean => {
+ const normalizedPath = pathname.replace(/\/settings$/, '');
+
+ if (normalizedPath.includes('/thread/')) return true;
+ if (normalizedPath.endsWith('/description')) return true;
+ if (normalizedPath.endsWith('/rules')) return true;
+
+ return false;
+};
+
+export const isPendingPostRoute = (pathname: string): boolean => {
+ const normalizedPath = pathname.replace(/\/settings$/, '');
+ return normalizedPath.startsWith('/pending/');
+};
+
+export const getFeedCacheKey = (pathname: string): string | null => {
+ let normalizedPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
+ normalizedPath = normalizedPath.replace(/\/settings$/, '');
+
+ if (normalizedPath.includes('/thread/')) {
+ const parts = normalizedPath.split('/thread/');
+ return parts[0] || null;
+ }
+
+ if (normalizedPath.endsWith('/description') || normalizedPath.endsWith('/rules')) {
+ return normalizedPath.replace(/\/(description|rules)$/, '');
+ }
+
+ if (normalizedPath.startsWith('/pending/')) {
+ return null;
+ }
+
+ if (isFeedRoute(pathname)) {
+ return normalizedPath;
+ }
+
+ return null;
+};
+
+export const getFeedType = (pathname: string): 'board' | 'catalog' | null => {
+ const normalizedPath = pathname.replace(/\/settings$/, '');
+
+ if (normalizedPath.includes('/catalog')) {
+ return 'catalog';
+ }
+
+ if (isFeedRoute(pathname)) {
+ return 'board';
+ }
+
+ if (isPostRoute(pathname)) {
+ return 'board';
+ }
+
+ return null;
+};
diff --git a/src/stores/use-feed-cache-store.ts b/src/stores/use-feed-cache-store.ts
new file mode 100644
index 00000000..5fbbafc5
--- /dev/null
+++ b/src/stores/use-feed-cache-store.ts
@@ -0,0 +1,54 @@
+import { create } from 'zustand';
+
+export interface CachedFeed {
+ key: string;
+ type: 'board' | 'catalog';
+ lastAccessed: number;
+}
+
+interface FeedCacheState {
+ cachedFeeds: CachedFeed[];
+ maxCacheSize: number;
+ accessFeed: (key: string, type: 'board' | 'catalog') => void;
+ removeFeed: (key: string) => void;
+ isFeedCached: (key: string) => boolean;
+}
+
+const useFeedCacheStore = create((set, get) => ({
+ cachedFeeds: [],
+ maxCacheSize: 2,
+
+ accessFeed: (key: string, type: 'board' | 'catalog') => {
+ const { cachedFeeds, maxCacheSize } = get();
+ const now = Date.now();
+ const existingIndex = cachedFeeds.findIndex((feed) => feed.key === key);
+
+ if (existingIndex !== -1) {
+ const updatedFeeds = [...cachedFeeds];
+ updatedFeeds[existingIndex] = { ...updatedFeeds[existingIndex], lastAccessed: now };
+ set({ cachedFeeds: updatedFeeds });
+ } else {
+ const newFeed: CachedFeed = { key, type, lastAccessed: now };
+ let updatedFeeds = [...cachedFeeds, newFeed];
+
+ if (updatedFeeds.length > maxCacheSize) {
+ updatedFeeds.sort((a, b) => a.lastAccessed - b.lastAccessed);
+ updatedFeeds = updatedFeeds.slice(1);
+ }
+
+ set({ cachedFeeds: updatedFeeds });
+ }
+ },
+
+ removeFeed: (key: string) => {
+ const { cachedFeeds } = get();
+ set({ cachedFeeds: cachedFeeds.filter((feed) => feed.key !== key) });
+ },
+
+ isFeedCached: (key: string) => {
+ const { cachedFeeds } = get();
+ return cachedFeeds.some((feed) => feed.key === key);
+ },
+}));
+
+export default useFeedCacheStore;
diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx
index 19164450..198b1ad6 100644
--- a/src/views/board/board.tsx
+++ b/src/views/board/board.tsx
@@ -6,14 +6,14 @@ import { Trans, useTranslation } from 'react-i18next';
import styles from './board.module.css';
import { shouldShowSnow } from '../../lib/snow';
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
-import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
-import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
+import { useDefaultSubplebbitAddresses, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
import { useFeedStateString } from '../../hooks/use-state-string';
import useTimeFilter from '../../hooks/use-time-filter';
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
import useSortingStore from '../../stores/use-sorting-store';
+import { getSubplebbitAddress } from '../../lib/utils/route-utils';
import ErrorDisplay from '../../components/error-display/error-display';
import LoadingEllipsis from '../../components/loading-ellipsis';
import SubplebbitDescription from '../../components/subplebbit-description';
@@ -33,21 +33,42 @@ const createThreadsWithoutImagesFilter = () => ({
key: 'threads-with-images-only',
});
-const Board = () => {
+export interface BoardProps {
+ // Props from FeedCacheContainer for cached feeds
+ feedCacheKey?: string;
+ viewType?: 'all' | 'subs' | 'mod' | 'board';
+ boardIdentifier?: string;
+ timeFilterNameFromCache?: string;
+ isVisible?: boolean;
+}
+
+const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: BoardProps) => {
const { t } = useTranslation();
const location = useLocation();
- const subplebbitAddress = useResolvedSubplebbitAddress();
- const boardPath = useBoardPath(subplebbitAddress);
+ const params = useParams();
const { hideThreadsWithoutImages } = useInterfaceSettingsStore();
- const isInAllView = isAllView(location.pathname);
+ // Use props from cache if provided, otherwise fall back to URL-derived values
+ const isInAllView = viewType ? viewType === 'all' : false;
+ const isInSubscriptionsView = viewType ? viewType === 'subs' : false;
+ const isInModView = viewType ? viewType === 'mod' : false;
+
+ // Resolve subplebbit address from cache props or URL
+ const defaultSubplebbits = useDefaultSubplebbits();
+ const resolvedAddressFromUrl = useResolvedSubplebbitAddress();
+ const subplebbitAddress = useMemo(() => {
+ if (boardIdentifierProp) {
+ return getSubplebbitAddress(boardIdentifierProp, defaultSubplebbits);
+ }
+ return resolvedAddressFromUrl;
+ }, [boardIdentifierProp, defaultSubplebbits, resolvedAddressFromUrl]);
+
+ const boardPath = useBoardPath(subplebbitAddress);
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
const account = useAccount();
const subscriptions = account?.subscriptions;
- const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
- const isInModView = isModView(location.pathname);
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
@@ -65,7 +86,9 @@ const Board = () => {
}, [isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, defaultSubplebbitAddresses, subscriptions, accountSubplebbitAddresses]);
const { sortType } = useSortingStore();
- const { timeFilterSeconds, timeFilterName } = useTimeFilter();
+ const { timeFilterSeconds, timeFilterName: timeFilterNameFromHook } = useTimeFilter();
+ // Use time filter from cache if provided
+ const timeFilterName = timeFilterNameFromCache || timeFilterNameFromHook;
const feedOptions = {
subplebbitAddresses,
@@ -82,8 +105,11 @@ const Board = () => {
const setResetFunction = useFeedResetStore((state) => state.setResetFunction);
useEffect(() => {
- setResetFunction(reset);
- }, [reset, setResetFunction, feed]);
+ // Only set reset function when this feed is visible
+ if (isVisible) {
+ setResetFunction(reset);
+ }
+ }, [reset, setResetFunction, feed, isVisible]);
// show account comments instantly in the feed once published (cid defined), instead of waiting for the feed to update
const filteredComments = useMemo(
@@ -175,7 +201,6 @@ const Board = () => {
return () => clearTimeout(timer);
}, []);
- const params = useParams();
const currentTimeFilterName = params?.timeFilterName || timeFilterName;
const Footer = () => {
@@ -272,21 +297,23 @@ const Board = () => {
);
};
- // save the last Virtuoso state to restore it when navigating back
const virtuosoRef = useRef(null);
- // include pathname in key so each board has its own scroll state
- const virtuosoStateKey = `${location.pathname}-${sortType}-${timeFilterSeconds}`;
+ const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}`;
const navigationType = useNavigationType();
- // When entering a board via link (PUSH/REPLACE), force scroll to top to avoid inheriting prior view scroll.
+ const hasBeenVisibleRef = useRef(false);
useEffect(() => {
- if (navigationType !== 'POP') {
- window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
+ if (isVisible && !hasBeenVisibleRef.current) {
+ hasBeenVisibleRef.current = true;
+ if (navigationType !== 'POP') {
+ window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
+ }
}
- }, [navigationType, location.pathname]);
+ }, [isVisible, navigationType]);
useEffect(() => {
- // capture the key at effect creation time to prevent race conditions during navigation
+ if (!isVisible) return;
+
const currentKey = virtuosoStateKey;
const setLastVirtuosoState = () => {
virtuosoRef.current?.getState((snapshot: StateSnapshot) => {
@@ -297,15 +324,15 @@ const Board = () => {
};
window.addEventListener('scroll', setLastVirtuosoState);
return () => window.removeEventListener('scroll', setLastVirtuosoState);
- }, [virtuosoStateKey]);
+ }, [virtuosoStateKey, isVisible]);
- // only restore scroll state on back/forward navigation (POP), not when clicking links (PUSH)
const lastVirtuosoState = navigationType === 'POP' ? lastVirtuosoStates?.[virtuosoStateKey] : undefined;
useEffect(() => {
+ if (!isVisible) return;
const boardTitle = title ? title : shortAddress || subplebbitAddress;
document.title = boardTitle + ' - 5chan';
- }, [title, shortAddress, subplebbitAddress]);
+ }, [title, shortAddress, subplebbitAddress, isVisible]);
const shouldShowErrorToUser = error?.message && feed.length === 0;
diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx
index 7594b42e..eb0693d0 100644
--- a/src/views/catalog/catalog.tsx
+++ b/src/views/catalog/catalog.tsx
@@ -4,7 +4,6 @@ import { Trans, useTranslation } from 'react-i18next';
import { Comment, useAccount, useFeed, useSubplebbit, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
-import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows';
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
@@ -16,6 +15,7 @@ import useFeedResetStore from '../../stores/use-feed-reset-store';
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
import useSortingStore from '../../stores/use-sorting-store';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
+import { getSubplebbitAddress } from '../../lib/utils/route-utils';
import CatalogRow from '../../components/catalog-row';
import LoadingEllipsis from '../../components/loading-ellipsis';
import styles from './catalog.module.css';
@@ -123,20 +123,40 @@ const createCombinedFilter = (
};
};
-const Catalog = () => {
+export interface CatalogProps {
+ // Props from FeedCacheContainer for cached feeds
+ feedCacheKey?: string;
+ viewType?: 'all' | 'subs' | 'mod' | 'board';
+ boardIdentifier?: string;
+ timeFilterNameFromCache?: string;
+ isVisible?: boolean;
+}
+
+const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: CatalogProps) => {
const { t } = useTranslation();
const location = useLocation();
- const subplebbitAddress = useResolvedSubplebbitAddress();
- const boardPath = useBoardPath(subplebbitAddress);
+ const params = useParams();
- const isInAllView = isAllView(location.pathname);
+ // Use props from cache if provided, otherwise fall back to URL-derived values
+ const isInAllView = viewType ? viewType === 'all' : false;
+ const isInSubscriptionsView = viewType ? viewType === 'subs' : false;
+
+ // Resolve subplebbit address from cache props or URL
const defaultSubplebbits = useDefaultSubplebbits();
+ const resolvedAddressFromUrl = useResolvedSubplebbitAddress();
+ const subplebbitAddress = useMemo(() => {
+ if (boardIdentifierProp) {
+ return getSubplebbitAddress(boardIdentifierProp, defaultSubplebbits);
+ }
+ return resolvedAddressFromUrl;
+ }, [boardIdentifierProp, defaultSubplebbits, resolvedAddressFromUrl]);
+
+ const boardPath = useBoardPath(subplebbitAddress);
const { hideAdultBoards } = useInterfaceSettingsStore();
const { showTextOnlyThreads, filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore();
const account = useAccount();
const subscriptions = account?.subscriptions;
- const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subplebbitAddresses = useMemo(() => {
const filteredDefaultSubplebbits = defaultSubplebbits
@@ -166,8 +186,10 @@ const Catalog = () => {
const columnCount = Math.floor(useWindowWidth() / columnWidth);
const postsPerPage = columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25;
- const { timeFilterSeconds, timeFilterName } = useTimeFilter();
+ const { timeFilterSeconds, timeFilterName: timeFilterNameFromHook } = useTimeFilter();
const { sortType } = useSortingStore();
+ // Use time filter from cache if provided
+ const timeFilterName = timeFilterNameFromCache || timeFilterNameFromHook;
// Create a stable callback for filter matching
const handleFilterMatch = useCallback((filterIndex: number, cid: string, subplebbitAddress: string) => {
@@ -303,8 +325,11 @@ const Catalog = () => {
const setResetFunction = useFeedResetStore((state) => state.setResetFunction);
useEffect(() => {
- setResetFunction(reset);
- }, [reset, setResetFunction]);
+ // Only set reset function when this feed is visible
+ if (isVisible) {
+ setResetFunction(reset);
+ }
+ }, [reset, setResetFunction, isVisible]);
const subplebbit = useSubplebbit({ subplebbitAddress });
const { error, shortAddress, state, title } = subplebbit || {};
@@ -344,7 +369,6 @@ const Catalog = () => {
);
- const params = useParams<{ sortType?: string; timeFilterName?: string }>();
const currentTimeFilterName = params?.timeFilterName || timeFilterName;
const Footer = () => {
@@ -450,21 +474,23 @@ const Catalog = () => {
const rows = useCatalogFeedRows(columnCount, processedFeed, isFeedLoaded, subplebbit);
- // save the last Virtuoso state to restore it when navigating back
const virtuosoRef = useRef(null);
- // include pathname in key so each board has its own scroll state
- const virtuosoStateKey = `${location.pathname}-${sortType}-${timeFilterSeconds}-catalog`;
+ const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}-catalog`;
const navigationType = useNavigationType();
- // When entering a board via link (PUSH/REPLACE), force scroll to top to avoid inheriting prior view scroll.
+ const hasBeenVisibleRef = useRef(false);
useEffect(() => {
- if (navigationType !== 'POP') {
- window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
+ if (isVisible && !hasBeenVisibleRef.current) {
+ hasBeenVisibleRef.current = true;
+ if (navigationType !== 'POP') {
+ window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
+ }
}
- }, [navigationType, location.pathname]);
+ }, [isVisible, navigationType]);
useEffect(() => {
- // capture the key at effect creation time to prevent race conditions during navigation
+ if (!isVisible) return;
+
const currentKey = virtuosoStateKey;
const setLastVirtuosoState = () =>
virtuosoRef.current?.getState((snapshot: StateSnapshot) => {
@@ -474,17 +500,17 @@ const Catalog = () => {
});
window.addEventListener('scroll', setLastVirtuosoState);
return () => window.removeEventListener('scroll', setLastVirtuosoState);
- }, [virtuosoStateKey]);
+ }, [virtuosoStateKey, isVisible]);
- // only restore scroll state on back/forward navigation (POP), not when clicking links (PUSH)
const lastVirtuosoState = navigationType === 'POP' ? lastVirtuosoStates?.[virtuosoStateKey] : undefined;
useEffect(() => {
+ if (!isVisible) return;
let documentTitle = title ? title : shortAddress;
if (isInAllView) documentTitle = t('all');
else if (isInSubscriptionsView) documentTitle = t('subscriptions');
document.title = documentTitle + ` - ${t('catalog')} - 5chan`;
- }, [title, shortAddress, isInAllView, isInSubscriptionsView, t]);
+ }, [title, shortAddress, isInAllView, isInSubscriptionsView, t, isVisible]);
// Clear matched filters when component mounts or when subplebbit changes
useEffect(() => {