diff --git a/mobile/src/session/mobile-diff-review-queue.test.ts b/mobile/src/session/mobile-diff-review-queue.test.ts index 1286e22cce4..e22d4cd2642 100644 --- a/mobile/src/session/mobile-diff-review-queue.test.ts +++ b/mobile/src/session/mobile-diff-review-queue.test.ts @@ -5,7 +5,9 @@ import type { MobileGitStatusEntry } from '../source-control/mobile-git-status' import { buildMobileDiffReviewQueue, createMobileDiffReviewFileKey, - filterMobileDiffReviewQueue + filterMobileDiffReviewQueue, + summarizeMobileDiffReviewQueue, + type MobileDiffReviewQueueItem } from './mobile-diff-review-queue' const emptyReviewState: MobileDiffReviewState = { version: 1, files: {} } @@ -117,4 +119,61 @@ describe('mobile diff review queue', () => { 'b.ts' ]) }) + + it('summarizes review counts in one pass without rereading item fields', () => { + const entryCount = 1_000 + const reads = { isReviewed: 0, scope: 0, canStage: 0 } + let expectedReviewedCount = 0 + let expectedReviewedUnstagedItems = 0 + let expectedReviewedUnstagedCount = 0 + const queue = Array.from({ length: entryCount }, (_, index) => { + const isReviewed = index % 2 === 0 + const scope = index % 3 === 0 ? 'unstaged' : 'staged' + const canStage = index % 5 === 0 + if (isReviewed) { + expectedReviewedCount += 1 + if (scope === 'unstaged') { + expectedReviewedUnstagedItems += 1 + if (canStage) { + expectedReviewedUnstagedCount += 1 + } + } + } + const item = {} as MobileDiffReviewQueueItem + Object.defineProperties(item, { + isReviewed: { + enumerable: true, + get: () => { + reads.isReviewed += 1 + return isReviewed + } + }, + scope: { + enumerable: true, + get: () => { + reads.scope += 1 + return scope + } + }, + canStage: { + enumerable: true, + get: () => { + reads.canStage += 1 + return canStage + } + } + }) + return item + }) + + expect(summarizeMobileDiffReviewQueue(queue)).toEqual({ + reviewedCount: expectedReviewedCount, + reviewedUnstagedCount: expectedReviewedUnstagedCount + }) + expect(reads).toEqual({ + isReviewed: entryCount, + scope: expectedReviewedCount, + canStage: expectedReviewedUnstagedItems + }) + }) }) diff --git a/mobile/src/session/mobile-diff-review-queue.ts b/mobile/src/session/mobile-diff-review-queue.ts index f66c7c04bb7..551b9c22a18 100644 --- a/mobile/src/session/mobile-diff-review-queue.ts +++ b/mobile/src/session/mobile-diff-review-queue.ts @@ -272,3 +272,25 @@ export function filterMobileDiffReviewQueue( return [...queue] } } + +export type MobileDiffReviewQueueSummary = { + reviewedCount: number + reviewedUnstagedCount: number +} + +export function summarizeMobileDiffReviewQueue( + queue: readonly MobileDiffReviewQueueItem[] +): MobileDiffReviewQueueSummary { + let reviewedCount = 0 + let reviewedUnstagedCount = 0 + for (const item of queue) { + if (!item.isReviewed) { + continue + } + reviewedCount += 1 + if (item.scope === 'unstaged' && item.canStage) { + reviewedUnstagedCount += 1 + } + } + return { reviewedCount, reviewedUnstagedCount } +} diff --git a/mobile/src/session/use-mobile-diff-review-controller.ts b/mobile/src/session/use-mobile-diff-review-controller.ts index 03f4b5711a9..5d74bccf156 100644 --- a/mobile/src/session/use-mobile-diff-review-controller.ts +++ b/mobile/src/session/use-mobile-diff-review-controller.ts @@ -9,6 +9,7 @@ import { buildMobileDiffReviewQueue, filterMobileDiffReviewQueue, mobileDiffReviewCommentMatchesItem, + summarizeMobileDiffReviewQueue, type MobileDiffReviewQueueFilter, type MobileDiffReviewQueueItem } from './mobile-diff-review-queue' @@ -133,12 +134,12 @@ export function useMobileDiffReviewController(input: ControllerInput) { const filteredQueue = useMemo(() => filterMobileDiffReviewQueue(queue, filter), [filter, queue]) const currentItem = filteredQueue[currentIndex] ?? null - const reviewedCount = queue.filter((item) => item.isReviewed).length + const { reviewedCount, reviewedUnstagedCount } = useMemo( + () => summarizeMobileDiffReviewQueue(queue), + [queue] + ) const unsentComments = screenState.kind === 'ready' ? getUnsentMobileDiffComments(screenState.comments) : [] - const reviewedUnstagedCount = queue.filter( - (item) => item.scope === 'unstaged' && item.isReviewed && item.canStage - ).length useEffect(() => { seededInitialTargetRef.current = false