diff --git a/src/components/__tests__/post-community-address-compat.test.tsx b/src/components/__tests__/post-community-address-compat.test.tsx index fde22b74..2d7721f8 100644 --- a/src/components/__tests__/post-community-address-compat.test.tsx +++ b/src/components/__tests__/post-community-address-compat.test.tsx @@ -326,6 +326,8 @@ vi.mock('../../lib/utils/replies-preview-utils', () => ({ filterRepliesForDisplay: (replies: TestComment[]) => replies, getPreviewDisplayReplies: (replies: TestComment[]) => replies, getTotalReplyCount: ({ replyCount }: { replyCount?: number }) => replyCount ?? 0, + hasEnoughPreviewReplies: ({ replyCount, loadedCount, visibleCount }: { replyCount?: number; loadedCount: number; visibleCount: number }) => + loadedCount >= Math.min(visibleCount, replyCount ?? visibleCount), })); vi.mock('../../lib/utils/thread-scroll-utils', () => ({ diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index a07ee0f8..6b722dac 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -49,7 +49,13 @@ import useQuotedByMap from '../../hooks/use-quoted-by-map'; import useProgressiveRender from '../../hooks/use-progressive-render'; import useFreshReplies from '../../hooks/use-fresh-replies'; import { BOARD_REPLIES_PREVIEW_FETCH_SIZE, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, REPLIES_PER_PAGE } from '../../lib/constants'; -import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount } from '../../lib/utils/replies-preview-utils'; +import { + computeOmittedCount, + filterRepliesForDisplay, + getPreviewDisplayReplies, + getTotalReplyCount, + hasEnoughPreviewReplies, +} from '../../lib/utils/replies-preview-utils'; import { isCommentArchived } from '../../lib/utils/comment-moderation-utils'; import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils'; import useDeleteFailedPost from '../../hooks/use-delete-failed-post'; @@ -821,11 +827,27 @@ const PostDesktop = ({ const { showOmittedReplies, setShowOmittedReplies } = useShowOmittedReplies(); - const shouldFetchPreview = showReplies && !isModQueue && !showAllReplies && showOmittedReplies[cid] !== true; + const shouldUsePreview = showReplies && !isModQueue && !showAllReplies; const shouldFetchFull = showReplies && !isModQueue && (showAllReplies || showOmittedReplies[cid]); + const cachedPreviewRepliesResult = useReplies({ + comment: shouldUsePreview ? resolvedPost : undefined, + onlyIfCached: true, + sortType: 'new', + flat: true, + repliesPerPage: BOARD_REPLIES_PREVIEW_FETCH_SIZE, + accountComments: { newerThan: Infinity, append: true }, + }); + const cachedPreviewReplies = (cachedPreviewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length + ? (cachedPreviewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies! + : cachedPreviewRepliesResult.replies || []; + const hasEnoughCachedPreview = hasEnoughPreviewReplies({ + replyCount: resolvedPost?.replyCount, + loadedCount: cachedPreviewReplies.length, + visibleCount: BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, + }); const previewRepliesResult = useReplies({ - comment: shouldFetchPreview ? resolvedPost : undefined, + comment: shouldUsePreview && !showOmittedReplies[cid] && !hasEnoughCachedPreview ? resolvedPost : undefined, sortType: 'new', flat: true, repliesPerPage: BOARD_REPLIES_PREVIEW_FETCH_SIZE, @@ -839,9 +861,10 @@ const PostDesktop = ({ accountComments: { newerThan: Infinity, append: true }, }); - const previewReplies = (previewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length + const livePreviewReplies = (previewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length ? (previewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies! : previewRepliesResult.replies || []; + const previewReplies = hasEnoughCachedPreview ? cachedPreviewReplies : livePreviewReplies; const fullReplies = (fullRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length ? (fullRepliesResult as { updatedReplies?: Comment[] }).updatedReplies! : fullRepliesResult.replies || []; @@ -875,7 +898,7 @@ const PostDesktop = ({ const totalReplyCount = getTotalReplyCount({ replyCount: resolvedPost?.replyCount, fullLoadedCount: fullReplies.length, - previewLoadedCount: previewReplies.length, + previewLoadedCount: Math.max(cachedPreviewReplies.length, livePreviewReplies.length), }); const repliesCount = computeOmittedCount({ totalReplyCount, diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 63790563..c211ab1c 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -44,7 +44,7 @@ import useProgressiveRender from '../../hooks/use-progressive-render'; import useFreshReplies from '../../hooks/use-fresh-replies'; import { BOARD_REPLIES_PREVIEW_FETCH_SIZE, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, REPLIES_PER_PAGE } from '../../lib/constants'; import { isCommentArchived } from '../../lib/utils/comment-moderation-utils'; -import { filterRepliesForDisplay, getPreviewDisplayReplies } from '../../lib/utils/replies-preview-utils'; +import { filterRepliesForDisplay, getPreviewDisplayReplies, hasEnoughPreviewReplies } from '../../lib/utils/replies-preview-utils'; import { getRenderableMobileBacklinks } from '../../lib/utils/reply-backlink-utils'; import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils'; import useDeleteFailedPost from '../../hooks/use-delete-failed-post'; @@ -564,8 +564,26 @@ const PostMobile = ({ const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined; const linksCount = useCountLinksInReplies(resolvedPost); const shouldFetchReplies = showReplies && !isModQueue; + const shouldUsePreview = shouldFetchReplies && !showAllReplies; + const cachedPreviewRepliesResult = useReplies({ + comment: shouldUsePreview ? resolvedPost : undefined, + onlyIfCached: true, + sortType: 'new', + flat: true, + repliesPerPage: BOARD_REPLIES_PREVIEW_FETCH_SIZE, + accountComments: { newerThan: Infinity, append: true }, + }); + const cachedPreviewReplies = (cachedPreviewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length + ? (cachedPreviewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies! + : cachedPreviewRepliesResult.replies || []; + const cachedPreviewDisplayCount = filterRepliesForDisplay(cachedPreviewReplies).length; + const hasEnoughCachedPreview = hasEnoughPreviewReplies({ + replyCount: resolvedPost?.replyCount, + loadedCount: cachedPreviewDisplayCount, + visibleCount: BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, + }); const previewRepliesResult = useReplies({ - comment: shouldFetchReplies && !showAllReplies ? resolvedPost : undefined, + comment: shouldUsePreview && !hasEnoughCachedPreview ? resolvedPost : undefined, sortType: 'new', flat: true, repliesPerPage: BOARD_REPLIES_PREVIEW_FETCH_SIZE, @@ -578,7 +596,11 @@ const PostMobile = ({ repliesPerPage: REPLIES_PER_PAGE, accountComments: { newerThan: Infinity, append: true }, }); - const repliesResult = showAllReplies ? fullRepliesResult : previewRepliesResult; + const livePreviewReplies = (previewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length + ? (previewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies! + : previewRepliesResult.replies || []; + const previewReplies = hasEnoughCachedPreview ? cachedPreviewReplies : livePreviewReplies; + const repliesResult = showAllReplies ? fullRepliesResult : { ...previewRepliesResult, replies: previewReplies, updatedReplies: previewReplies }; const { replies, hasMore, loadMore } = repliesResult; const updatedReplies = (repliesResult as { updatedReplies?: Comment[] }).updatedReplies; const repliesForRender = updatedReplies?.length ? updatedReplies : replies || []; diff --git a/src/lib/utils/__tests__/misc-utils.test.ts b/src/lib/utils/__tests__/misc-utils.test.ts index 7b2c0018..915f0010 100644 --- a/src/lib/utils/__tests__/misc-utils.test.ts +++ b/src/lib/utils/__tests__/misc-utils.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { copyToClipboard } from '../clipboard-utils'; import { hashStringToColor, getTextColorForBackground, removeMarkdown } from '../post-utils'; import { preloadReplyModal, preloadThemeAssets, resolveAssetUrl } from '../preload-utils'; -import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount } from '../replies-preview-utils'; +import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount, hasEnoughPreviewReplies } from '../replies-preview-utils'; import { getQuotedCidsFromContent, mergeQuotedCids } from '../reply-quote-utils'; import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../string-utils'; import { getFormattedDate, getFormattedTimeAgo, isChristmas } from '../time-utils'; @@ -186,6 +186,10 @@ describe('misc utils', () => { expect(computeOmittedCount({ totalReplyCount: 2, visibleCount: 5 })).toBe(0); expect(computeOmittedCount({ totalReplyCount: 9, visibleCount: 5 })).toBe(4); + expect(hasEnoughPreviewReplies({ replyCount: 2, loadedCount: 2, visibleCount: 5 })).toBe(true); + expect(hasEnoughPreviewReplies({ replyCount: 9, loadedCount: 4, visibleCount: 5 })).toBe(false); + expect(hasEnoughPreviewReplies({ replyCount: undefined, loadedCount: 5, visibleCount: 5 })).toBe(true); + expect(hasEnoughPreviewReplies({ replyCount: undefined, loadedCount: 4, visibleCount: 5 })).toBe(false); expect(getTotalReplyCount({ replyCount: undefined, fullLoadedCount: 7, previewLoadedCount: 5 })).toBe(7); expect(getTotalReplyCount({ replyCount: 12, fullLoadedCount: 7, previewLoadedCount: 5 })).toBe(12); }); diff --git a/src/lib/utils/replies-preview-utils.ts b/src/lib/utils/replies-preview-utils.ts index 20d0cd12..908d37dc 100644 --- a/src/lib/utils/replies-preview-utils.ts +++ b/src/lib/utils/replies-preview-utils.ts @@ -54,6 +54,22 @@ export function computeOmittedCount({ totalReplyCount, visibleCount }: ComputeOm return Math.max(0, totalReplyCount - visibleCount); } +interface HasEnoughPreviewRepliesParams { + replyCount: number | undefined; + loadedCount: number; + visibleCount: number; +} + +/** + * Board previews can skip a live fetch when cached replies already cover all + * replies that could be shown. If total reply count is unknown, require a full + * visible slice so UX matches the current live-preview behavior. + */ +export function hasEnoughPreviewReplies({ replyCount, loadedCount, visibleCount }: HasEnoughPreviewRepliesParams): boolean { + const requiredCount = typeof replyCount === 'number' && replyCount >= 0 ? Math.min(visibleCount, replyCount) : visibleCount; + return requiredCount === 0 || loadedCount >= requiredCount; +} + interface GetTotalReplyCountParams { replyCount: number | undefined; fullLoadedCount: number;