mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(board): prevent transient no threads state (#1151)
* fix(board): prevent transient no threads state * fix(board): scope raw thread fallback by sort * fix(board): keep flash table loading during feed sync
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { useAccount, type Comment, type CommunitiesPages, type Community } from '@bitsocial/bitsocial-react-hooks';
|
import { useAccount, type Comment } from '@bitsocial/bitsocial-react-hooks';
|
||||||
import accountsStore from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts';
|
import accountsStore from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts';
|
||||||
import communitiesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities';
|
import communitiesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities';
|
||||||
import communitiesPagesStore, { getCommunityFirstPageCid, getCommunityPages } from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
import communitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||||
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
||||||
import { isCommentArchived } from '../lib/utils/comment-moderation-utils';
|
import { isCommentArchived } from '../lib/utils/comment-moderation-utils';
|
||||||
|
import { getRawBoardThreadState } from '../lib/utils/raw-board-thread-state';
|
||||||
import { useDirectories } from './use-directories';
|
import { useDirectories } from './use-directories';
|
||||||
import { getBoardAddressKeys, isBoardAddressInScope } from './use-hidden-catalog-threads';
|
import { getBoardAddressKeys, isBoardAddressInScope } from './use-hidden-catalog-threads';
|
||||||
|
|
||||||
@@ -15,72 +16,12 @@ type UsePruneHiddenCatalogThreadsOptions = {
|
|||||||
sortType: 'active' | 'new';
|
sortType: 'active' | 'new';
|
||||||
};
|
};
|
||||||
|
|
||||||
type RawBoardCatalogState = {
|
const EMPTY_RAW_BOARD_THREAD_STATE = getRawBoardThreadState({
|
||||||
isFullyLoaded: boolean;
|
accountId: undefined,
|
||||||
rootThreadCids: Set<string>;
|
communitiesPages: {},
|
||||||
};
|
community: undefined,
|
||||||
|
sortType: 'active',
|
||||||
const EMPTY_RAW_BOARD_CATALOG_STATE: RawBoardCatalogState = {
|
});
|
||||||
isFullyLoaded: false,
|
|
||||||
rootThreadCids: new Set<string>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const addRootThreadCids = (cids: Set<string>, comments: readonly Comment[] | undefined) => {
|
|
||||||
for (const comment of comments || []) {
|
|
||||||
if (comment?.cid && !comment.parentCid) {
|
|
||||||
cids.add(comment.cid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRawBoardCatalogState = ({
|
|
||||||
accountId,
|
|
||||||
communitiesPages,
|
|
||||||
community,
|
|
||||||
sortType,
|
|
||||||
}: {
|
|
||||||
accountId: string | undefined;
|
|
||||||
communitiesPages: CommunitiesPages;
|
|
||||||
community: Community | undefined;
|
|
||||||
sortType: 'active' | 'new';
|
|
||||||
}): RawBoardCatalogState => {
|
|
||||||
if (!community) {
|
|
||||||
return EMPTY_RAW_BOARD_CATALOG_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootThreadCids = new Set<string>();
|
|
||||||
const preloadedSortPage = community.posts?.pages?.[sortType];
|
|
||||||
addRootThreadCids(rootThreadCids, preloadedSortPage?.comments);
|
|
||||||
|
|
||||||
const firstPageCid = getCommunityFirstPageCid(community, sortType, 'posts');
|
|
||||||
const pages = firstPageCid ? getCommunityPages(community, sortType, communitiesPages, 'posts', accountId) : [];
|
|
||||||
for (const page of pages) {
|
|
||||||
addRootThreadCids(rootThreadCids, page?.comments);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pages.length > 0) {
|
|
||||||
return {
|
|
||||||
isFullyLoaded: !pages[pages.length - 1]?.nextCid,
|
|
||||||
rootThreadCids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageCids = community.posts?.pageCids || {};
|
|
||||||
const hasPageCids = Object.keys(pageCids).length > 0;
|
|
||||||
const preloadedPages = Object.values(community.posts?.pages || {}) as Array<{ comments?: Comment[]; nextCid?: string }>;
|
|
||||||
const hasCompletePreloadedPage = !hasPageCids && preloadedPages.some((page) => Array.isArray(page?.comments)) && preloadedPages.every((page) => !page?.nextCid);
|
|
||||||
|
|
||||||
if (hasCompletePreloadedPage) {
|
|
||||||
for (const page of preloadedPages) {
|
|
||||||
addRootThreadCids(rootThreadCids, page?.comments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFullyLoaded: hasCompletePreloadedPage,
|
|
||||||
rootThreadCids,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const usePruneHiddenCatalogThreads = ({ enabled, hiddenThreadCandidates, communityAddress, sortType }: UsePruneHiddenCatalogThreadsOptions) => {
|
const usePruneHiddenCatalogThreads = ({ enabled, hiddenThreadCandidates, communityAddress, sortType }: UsePruneHiddenCatalogThreadsOptions) => {
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
@@ -92,13 +33,13 @@ const usePruneHiddenCatalogThreads = ({ enabled, hiddenThreadCandidates, communi
|
|||||||
const rawBoardCatalogState = useMemo(
|
const rawBoardCatalogState = useMemo(
|
||||||
() =>
|
() =>
|
||||||
enabled
|
enabled
|
||||||
? getRawBoardCatalogState({
|
? getRawBoardThreadState({
|
||||||
accountId: account?.id,
|
accountId: account?.id,
|
||||||
communitiesPages,
|
communitiesPages,
|
||||||
community,
|
community,
|
||||||
sortType,
|
sortType,
|
||||||
})
|
})
|
||||||
: EMPTY_RAW_BOARD_CATALOG_STATE,
|
: EMPTY_RAW_BOARD_THREAD_STATE,
|
||||||
[account?.id, communitiesPages, community, enabled, sortType],
|
[account?.id, communitiesPages, community, enabled, sortType],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { Comment, CommunitiesPages, Community } from '@bitsocial/bitsocial-react-hooks';
|
||||||
|
import { getRawBoardThreadState } from '../raw-board-thread-state';
|
||||||
|
|
||||||
|
const rootThread = (cid: string): Comment =>
|
||||||
|
({
|
||||||
|
cid,
|
||||||
|
postCid: cid,
|
||||||
|
}) as Comment;
|
||||||
|
|
||||||
|
describe('getRawBoardThreadState', () => {
|
||||||
|
it('keeps the preloaded fallback scoped to the requested sort type', () => {
|
||||||
|
const community = {
|
||||||
|
posts: {
|
||||||
|
pageCids: {
|
||||||
|
new: 'new-page-1',
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
active: {
|
||||||
|
comments: [],
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
comments: [rootThread('new-thread')],
|
||||||
|
nextCid: 'new-page-2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Community;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getRawBoardThreadState({
|
||||||
|
accountId: undefined,
|
||||||
|
communitiesPages: {} as CommunitiesPages,
|
||||||
|
community,
|
||||||
|
sortType: 'active',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
isFullyLoaded: true,
|
||||||
|
rootThreadCids: new Set<string>(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats an empty preloaded requested-sort page as a fully loaded empty board', () => {
|
||||||
|
const community = {
|
||||||
|
posts: {
|
||||||
|
pages: {
|
||||||
|
active: {
|
||||||
|
comments: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Community;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getRawBoardThreadState({
|
||||||
|
accountId: undefined,
|
||||||
|
communitiesPages: {} as CommunitiesPages,
|
||||||
|
community,
|
||||||
|
sortType: 'active',
|
||||||
|
}).isFullyLoaded,
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type { Comment, CommunitiesPages, Community } from '@bitsocial/bitsocial-react-hooks';
|
||||||
|
import { getCommunityFirstPageCid, getCommunityPages } from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||||
|
|
||||||
|
export type RawBoardThreadState = {
|
||||||
|
isFullyLoaded: boolean;
|
||||||
|
rootThreadCids: Set<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_RAW_BOARD_THREAD_STATE: RawBoardThreadState = {
|
||||||
|
isFullyLoaded: false,
|
||||||
|
rootThreadCids: new Set<string>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRootThreadCids = (cids: Set<string>, comments: readonly Comment[] | undefined) => {
|
||||||
|
for (const comment of comments || []) {
|
||||||
|
if (comment?.cid && !comment.parentCid) {
|
||||||
|
cids.add(comment.cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRawBoardThreadState = ({
|
||||||
|
accountId,
|
||||||
|
communitiesPages,
|
||||||
|
community,
|
||||||
|
sortType,
|
||||||
|
}: {
|
||||||
|
accountId: string | undefined;
|
||||||
|
communitiesPages: CommunitiesPages;
|
||||||
|
community: Community | undefined;
|
||||||
|
sortType: 'active' | 'new';
|
||||||
|
}): RawBoardThreadState => {
|
||||||
|
if (!community) {
|
||||||
|
return EMPTY_RAW_BOARD_THREAD_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootThreadCids = new Set<string>();
|
||||||
|
const preloadedSortPage = community.posts?.pages?.[sortType];
|
||||||
|
addRootThreadCids(rootThreadCids, preloadedSortPage?.comments);
|
||||||
|
|
||||||
|
const firstPageCid = getCommunityFirstPageCid(community, sortType, 'posts');
|
||||||
|
const pages = firstPageCid ? getCommunityPages(community, sortType, communitiesPages, 'posts', accountId) : [];
|
||||||
|
for (const page of pages) {
|
||||||
|
addRootThreadCids(rootThreadCids, page?.comments);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.length > 0) {
|
||||||
|
return {
|
||||||
|
isFullyLoaded: !pages[pages.length - 1]?.nextCid,
|
||||||
|
rootThreadCids,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPageCid = Boolean(community.posts?.pageCids?.[sortType]);
|
||||||
|
const preloadedPages = (preloadedSortPage ? [preloadedSortPage] : []) as Array<{ comments?: Comment[]; nextCid?: string }>;
|
||||||
|
const hasCompletePreloadedPage = !hasPageCid && preloadedPages.some((page) => Array.isArray(page?.comments)) && preloadedPages.every((page) => !page?.nextCid);
|
||||||
|
|
||||||
|
if (hasCompletePreloadedPage) {
|
||||||
|
for (const page of preloadedPages) {
|
||||||
|
addRootThreadCids(rootThreadCids, page?.comments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFullyLoaded: hasCompletePreloadedPage,
|
||||||
|
rootThreadCids,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { createElement } from 'react';
|
|||||||
import { createRoot, type Root } from 'react-dom/client';
|
import { createRoot, type Root } from 'react-dom/client';
|
||||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import communitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||||
import Board, { type BoardProps } from '../board';
|
import Board, { type BoardProps } from '../board';
|
||||||
import { clearStableLastVisitTimeFilterName, LAST_VISIT_STORAGE_KEY } from '../../../lib/utils/time-filter-utils';
|
import { clearStableLastVisitTimeFilterName, LAST_VISIT_STORAGE_KEY } from '../../../lib/utils/time-filter-utils';
|
||||||
|
|
||||||
@@ -35,6 +36,10 @@ type TestComment = {
|
|||||||
type TestCommunity = {
|
type TestCommunity = {
|
||||||
error?: Error;
|
error?: Error;
|
||||||
nameResolved?: boolean;
|
nameResolved?: boolean;
|
||||||
|
posts?: {
|
||||||
|
pageCids?: Record<string, string>;
|
||||||
|
pages?: Record<string, { comments?: TestComment[]; nextCid?: string }>;
|
||||||
|
};
|
||||||
shortAddress?: string;
|
shortAddress?: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -312,6 +317,19 @@ const flushEffects = async (count = 5) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const markRawBoardThreadsFullyLoaded = (comments: TestComment[] = []) => {
|
||||||
|
testState.community = {
|
||||||
|
...testState.community,
|
||||||
|
posts: {
|
||||||
|
pages: {
|
||||||
|
active: {
|
||||||
|
comments,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const renderBoard = async ({
|
const renderBoard = async ({
|
||||||
boardProps,
|
boardProps,
|
||||||
initialEntry,
|
initialEntry,
|
||||||
@@ -393,6 +411,7 @@ describe('Board', () => {
|
|||||||
document.title = 'before';
|
document.title = 'before';
|
||||||
clearStableLastVisitTimeFilterName();
|
clearStableLastVisitTimeFilterName();
|
||||||
localStorage.setItem(LAST_VISIT_STORAGE_KEY, String(Date.now()));
|
localStorage.setItem(LAST_VISIT_STORAGE_KEY, String(Date.now()));
|
||||||
|
communitiesPagesStore.setState({ communitiesPages: {}, comments: {} });
|
||||||
Object.defineProperty(window, 'scrollTo', {
|
Object.defineProperty(window, 'scrollTo', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: vi.fn(),
|
value: vi.fn(),
|
||||||
@@ -407,6 +426,7 @@ describe('Board', () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
act(() => root.unmount());
|
act(() => root.unmount());
|
||||||
container.remove();
|
container.remove();
|
||||||
|
communitiesPagesStore.setState({ communitiesPages: {}, comments: {} });
|
||||||
clearStableLastVisitTimeFilterName();
|
clearStableLastVisitTimeFilterName();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
@@ -622,6 +642,7 @@ describe('Board', () => {
|
|||||||
shortAddress: 'flash-posting.bso',
|
shortAddress: 'flash-posting.bso',
|
||||||
title: '/f/ - Flash',
|
title: '/f/ - Flash',
|
||||||
};
|
};
|
||||||
|
markRawBoardThreadsFullyLoaded();
|
||||||
|
|
||||||
await renderBoard({ initialEntry: '/f', routePath: '/:boardIdentifier/*' });
|
await renderBoard({ initialEntry: '/f', routePath: '/:boardIdentifier/*' });
|
||||||
|
|
||||||
@@ -655,6 +676,7 @@ describe('Board', () => {
|
|||||||
title: '/f/ - Flash',
|
title: '/f/ - Flash',
|
||||||
};
|
};
|
||||||
testState.hasMore = true;
|
testState.hasMore = true;
|
||||||
|
markRawBoardThreadsFullyLoaded();
|
||||||
|
|
||||||
await renderBoard({ initialEntry: '/f', routePath: '/:boardIdentifier/*' });
|
await renderBoard({ initialEntry: '/f', routePath: '/:boardIdentifier/*' });
|
||||||
|
|
||||||
@@ -988,10 +1010,28 @@ describe('Board', () => {
|
|||||||
state: 'succeeded',
|
state: 'succeeded',
|
||||||
title: '/mu/ - Music',
|
title: '/mu/ - Music',
|
||||||
};
|
};
|
||||||
|
markRawBoardThreadsFullyLoaded();
|
||||||
|
|
||||||
await renderBoard({ initialEntry: '/mu', routePath: '/:boardIdentifier/*' });
|
await renderBoard({ initialEntry: '/mu', routePath: '/:boardIdentifier/*' });
|
||||||
|
|
||||||
expect(container.textContent).toContain('no_threads');
|
expect(container.textContent).toContain('no_threads');
|
||||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')).toBeNull();
|
expect(container.querySelector('[data-testid="loading-ellipsis"]')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show no threads after board metadata loads but raw thread pages are still missing', async () => {
|
||||||
|
testState.feedStateString = undefined;
|
||||||
|
testState.feedState = 'succeeded';
|
||||||
|
testState.hasMore = false;
|
||||||
|
testState.community = {
|
||||||
|
error: undefined,
|
||||||
|
shortAddress: 'music-posting.eth',
|
||||||
|
state: 'succeeded',
|
||||||
|
title: '/mu/ - Music',
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderBoard({ initialEntry: '/mu', routePath: '/:boardIdentifier/*' });
|
||||||
|
|
||||||
|
expect(container.textContent).not.toContain('no_threads');
|
||||||
|
expect(container.querySelector('[data-testid="loading-ellipsis"]')?.textContent).toBe('downloading_board');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|||||||
import { Link, useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
import { Link, useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
||||||
import { Comment, useAccount, useAccountComments, useCommunity, useFeed } from '@bitsocial/bitsocial-react-hooks';
|
import { Comment, useAccount, useAccountComments, useCommunity, useFeed } from '@bitsocial/bitsocial-react-hooks';
|
||||||
import { useCommunityField } from '../../hooks/use-stable-community';
|
import { useCommunityField } from '../../hooks/use-stable-community';
|
||||||
|
import communitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import styles from './board.module.css';
|
import styles from './board.module.css';
|
||||||
@@ -19,6 +20,7 @@ import usePostNumberStore from '../../stores/use-post-number-store';
|
|||||||
import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size';
|
import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size';
|
||||||
import useExpandedTimeFilter from '../../hooks/use-expanded-time-filter';
|
import useExpandedTimeFilter from '../../hooks/use-expanded-time-filter';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
|
import { useNowSeconds } from '../../hooks/use-now-seconds';
|
||||||
import { useSuggestionFeedLoader } from '../../hooks/use-suggestion-feed-loader';
|
import { useSuggestionFeedLoader } from '../../hooks/use-suggestion-feed-loader';
|
||||||
import useTimeFilter from '../../hooks/use-time-filter';
|
import useTimeFilter from '../../hooks/use-time-filter';
|
||||||
import { getPageSlice } from '../../lib/utils/board-feed-pagination';
|
import { getPageSlice } from '../../lib/utils/board-feed-pagination';
|
||||||
@@ -26,6 +28,7 @@ import { getPageFromFeedPath, isDirectoryBoard, normalizeMultiboardFeedPath, str
|
|||||||
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
||||||
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||||
import { getNonokoPendingAccountCommentIndex } from '../../lib/utils/post-options-utils';
|
import { getNonokoPendingAccountCommentIndex } from '../../lib/utils/post-options-utils';
|
||||||
|
import { getRawBoardThreadState } from '../../lib/utils/raw-board-thread-state';
|
||||||
import { getSearchWithTimeFilter, getTimeFilterSuggestion, type TimeFilterSuggestion } from '../../lib/utils/time-filter-utils';
|
import { getSearchWithTimeFilter, getTimeFilterSuggestion, type TimeFilterSuggestion } from '../../lib/utils/time-filter-utils';
|
||||||
import { getPretextItemSizeFromElement, resolveFeedVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
|
import { getPretextItemSizeFromElement, resolveFeedVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
|
||||||
import { isFlashDirectory, isFlashDirectoryCode } from '../../lib/flash-tags';
|
import { isFlashDirectory, isFlashDirectoryCode } from '../../lib/flash-tags';
|
||||||
@@ -45,6 +48,7 @@ const MONTH_IN_SECONDS = 30 * 24 * 60 * 60;
|
|||||||
const YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
|
const YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
|
||||||
// Keep the hook on its indexed fast path when this view should not inject local posts.
|
// Keep the hook on its indexed fast path when this view should not inject local posts.
|
||||||
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
||||||
|
const EMPTY_COMMUNITIES_PAGES = {};
|
||||||
|
|
||||||
/** Board feed always uses 'active' sort; catalog dropdown does not affect board ordering. */
|
/** Board feed always uses 'active' sort; catalog dropdown does not affect board ordering. */
|
||||||
const BOARD_SORT_TYPE = 'active' as const;
|
const BOARD_SORT_TYPE = 'active' as const;
|
||||||
@@ -55,6 +59,7 @@ interface BoardFooterProps {
|
|||||||
feedState: string | undefined;
|
feedState: string | undefined;
|
||||||
combinedFeedLength: number;
|
combinedFeedLength: number;
|
||||||
isSingleCommunityBoard: boolean;
|
isSingleCommunityBoard: boolean;
|
||||||
|
isRawBoardThreadStateFullyLoaded: boolean;
|
||||||
isInSubscriptionsView: boolean;
|
isInSubscriptionsView: boolean;
|
||||||
isInModView: boolean;
|
isInModView: boolean;
|
||||||
currentTimeFilterName: string;
|
currentTimeFilterName: string;
|
||||||
@@ -78,6 +83,7 @@ const BoardFooter = ({
|
|||||||
feedState,
|
feedState,
|
||||||
combinedFeedLength,
|
combinedFeedLength,
|
||||||
isSingleCommunityBoard,
|
isSingleCommunityBoard,
|
||||||
|
isRawBoardThreadStateFullyLoaded,
|
||||||
isInSubscriptionsView,
|
isInSubscriptionsView,
|
||||||
isInModView,
|
isInModView,
|
||||||
currentTimeFilterName,
|
currentTimeFilterName,
|
||||||
@@ -96,7 +102,7 @@ const BoardFooter = ({
|
|||||||
const isLoadedCommunityState = communityState === 'succeeded' || communityState === 'ready';
|
const isLoadedCommunityState = communityState === 'succeeded' || communityState === 'ready';
|
||||||
const isFeedSucceeded = feedState === 'succeeded';
|
const isFeedSucceeded = feedState === 'succeeded';
|
||||||
const isFeedFailed = feedState === 'failed';
|
const isFeedFailed = feedState === 'failed';
|
||||||
const canShowNoThreads = isSingleCommunityBoard ? isLoadedCommunityState && isFeedSucceeded : isFeedSucceeded && !hasMore;
|
const canShowNoThreads = isSingleCommunityBoard ? isLoadedCommunityState && isFeedSucceeded && isRawBoardThreadStateFullyLoaded : isFeedSucceeded && !hasMore;
|
||||||
const isEmptyFeedLoading = combinedFeedLength === 0 && !canShowNoThreads && (isSingleCommunityBoard ? communityState !== 'failed' : !isFeedFailed);
|
const isEmptyFeedLoading = combinedFeedLength === 0 && !canShowNoThreads && (isSingleCommunityBoard ? communityState !== 'failed' : !isFeedFailed);
|
||||||
const showFooterLoading = showLoadingEllipsis && (hasMore || isEmptyFeedLoading);
|
const showFooterLoading = showLoadingEllipsis && (hasMore || isEmptyFeedLoading);
|
||||||
|
|
||||||
@@ -306,6 +312,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
[communityAddress],
|
[communityAddress],
|
||||||
);
|
);
|
||||||
const { accountComments: recentAccountComments } = useAccountComments(accountCommentLookupOptions);
|
const { accountComments: recentAccountComments } = useAccountComments(accountCommentLookupOptions);
|
||||||
|
const nowSeconds = useNowSeconds(recentAccountComments.length > 0);
|
||||||
const nonokoPendingAccountCommentIndex = getNonokoPendingAccountCommentIndex(routerLocation.state);
|
const nonokoPendingAccountCommentIndex = getNonokoPendingAccountCommentIndex(routerLocation.state);
|
||||||
const nonokoPendingAccountCommentLookupOptions = useMemo(
|
const nonokoPendingAccountCommentLookupOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -353,7 +360,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
return (
|
return (
|
||||||
!deleted &&
|
!deleted &&
|
||||||
!removed &&
|
!removed &&
|
||||||
timestamp > Date.now() / 1000 - RECENT_ACCOUNT_COMMENT_WINDOW_SECONDS &&
|
timestamp > nowSeconds - RECENT_ACCOUNT_COMMENT_WINDOW_SECONDS &&
|
||||||
state === 'succeeded' &&
|
state === 'succeeded' &&
|
||||||
cid &&
|
cid &&
|
||||||
cid === postCid &&
|
cid === postCid &&
|
||||||
@@ -361,7 +368,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
!feedCids.has(cid)
|
!feedCids.has(cid)
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
[recentAccountComments, communityAddress, feedCids],
|
[recentAccountComments, communityAddress, feedCids, nowSeconds],
|
||||||
);
|
);
|
||||||
const localAccountComments = useMemo(() => {
|
const localAccountComments = useMemo(() => {
|
||||||
if (!nonokoPendingAccountComment) return filteredComments;
|
if (!nonokoPendingAccountComment) return filteredComments;
|
||||||
@@ -451,6 +458,20 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
// useCommunityField only reads from store, doesn't trigger fetching
|
// useCommunityField only reads from store, doesn't trigger fetching
|
||||||
const communityData = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
const communityData = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const { error: communityError, state: communityState } = communityData || {};
|
const { error: communityError, state: communityState } = communityData || {};
|
||||||
|
const communitiesPages = communitiesPagesStore((state) => (isMultiboardView ? EMPTY_COMMUNITIES_PAGES : state.communitiesPages));
|
||||||
|
const rawBoardThreadState = useMemo(
|
||||||
|
() =>
|
||||||
|
isMultiboardView
|
||||||
|
? undefined
|
||||||
|
: getRawBoardThreadState({
|
||||||
|
accountId: account?.id,
|
||||||
|
communitiesPages,
|
||||||
|
community: communityData,
|
||||||
|
sortType: BOARD_SORT_TYPE,
|
||||||
|
}),
|
||||||
|
[account?.id, communitiesPages, communityData, isMultiboardView],
|
||||||
|
);
|
||||||
|
const isRawBoardThreadStateFullyLoaded = rawBoardThreadState?.isFullyLoaded ?? false;
|
||||||
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : communityTitle;
|
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : communityTitle;
|
||||||
|
|
||||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||||
@@ -466,6 +487,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
feedState={feedState}
|
feedState={feedState}
|
||||||
combinedFeedLength={combinedFeed.length}
|
combinedFeedLength={combinedFeed.length}
|
||||||
isSingleCommunityBoard={!isInAllView && !isInSubscriptionsView && !isInModView}
|
isSingleCommunityBoard={!isInAllView && !isInSubscriptionsView && !isInModView}
|
||||||
|
isRawBoardThreadStateFullyLoaded={isRawBoardThreadStateFullyLoaded}
|
||||||
isInSubscriptionsView={isInSubscriptionsView}
|
isInSubscriptionsView={isInSubscriptionsView}
|
||||||
isInModView={isInModView}
|
isInModView={isInModView}
|
||||||
currentTimeFilterName={currentTimeFilterName}
|
currentTimeFilterName={currentTimeFilterName}
|
||||||
@@ -546,6 +568,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
communityAddresses,
|
communityAddresses,
|
||||||
hasMore,
|
hasMore,
|
||||||
combinedFeed.length,
|
combinedFeed.length,
|
||||||
|
isRawBoardThreadStateFullyLoaded,
|
||||||
isInAllView,
|
isInAllView,
|
||||||
isInSubscriptionsView,
|
isInSubscriptionsView,
|
||||||
isInModView,
|
isInModView,
|
||||||
@@ -644,8 +667,8 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
const displayFeed = effectiveInfiniteScroll ? combinedFeed : currentPageFeed;
|
const displayFeed = effectiveInfiniteScroll ? combinedFeed : currentPageFeed;
|
||||||
const isLoadedCommunityState = communityState === 'succeeded' || communityState === 'ready';
|
const isLoadedCommunityState = communityState === 'succeeded' || communityState === 'ready';
|
||||||
const isFeedSucceeded = feedState === 'succeeded';
|
const isFeedSucceeded = feedState === 'succeeded';
|
||||||
const shouldShowFlashTableLoading =
|
const canShowEmptyFlashTable = isLoadedCommunityState && isFeedSucceeded && isRawBoardThreadStateFullyLoaded;
|
||||||
shouldUseFlashTable && displayFeed.length === 0 && !(isLoadedCommunityState && isFeedSucceeded) && communityState !== 'failed' && feedState !== 'failed';
|
const shouldShowFlashTableLoading = shouldUseFlashTable && displayFeed.length === 0 && !canShowEmptyFlashTable && communityState !== 'failed' && feedState !== 'failed';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user