From 77872f5f67c59fe1f40bc11abdc9b6755fc34c03 Mon Sep 17 00:00:00 2001 From: AtAFork Date: Wed, 11 Nov 2026 09:30:54 -0600 Subject: [PATCH 1/2] feat(mobile): hide viewed files in pull request file views Reviewing a big pull request on a phone meant scrolling past every file you had already read. The changed-files list, the file picker sheet, and the diff screen now have a "Hide viewed files" filter beside "Hide test files", saved as a mobile preference. The filter shows only where viewed state exists (not for single-commit diffs) and only once a file is viewed or while it is on, so it can always be turned off. A file you mark viewed on the diff screen stays until you leave, as does the file a link or guide jumped to. When every file is hidden, an empty state offers "Show viewed files". Opus 5.5 via Claude Code in T3 Code. --- .../pull-requests/PullRequestFiles.tsx | 52 ++++++++++++-- .../pull-requests/PullRequestRouteScreen.tsx | 67 ++++++++++++++++--- .../pullRequestFileFilters.test.ts | 31 ++++++++- .../pull-requests/pullRequestFileFilters.ts | 39 ++++++++--- .../pull-requests/usePullRequestFileFilter.ts | 49 ++++++++++++-- .../usePullRequestFilesViewed.ts | 1 + .../src/persistence/mobile-preferences.ts | 6 ++ 7 files changed, 211 insertions(+), 34 deletions(-) diff --git a/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx b/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx index 7244945878861..cd38f41b9fed6 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx @@ -54,7 +54,7 @@ function useChangedFiles(params: PullRequestDiffParams) { ).changeRequest; const diff = usePullRequestDiff(params, terminology); const viewed = usePullRequestFilesViewed(params, pr); - const filter = usePullRequestFileFilter(diff.parsed); + const filter = usePullRequestFileFilter(diff.parsed, viewed); const parsed = filter.parsed; return { diff, @@ -78,11 +78,14 @@ function describeFiles( { filter, viewed }: ChangedFiles, ) { const noun = (count: number) => (count === 1 ? "file" : "files"); + const hiding = filter.hiddenCount > 0 || filter.hiddenViewedCount > 0; return [ - `${parsed.fileCount} ${filter.hiddenCount ? "visible" : "changed"} ${noun(parsed.fileCount)}`, - viewed.supported - ? `${parsed.files.filter((file) => viewed.isViewed(file.path)).length} viewed` - : null, + `${parsed.fileCount} ${hiding ? "visible" : "changed"} ${noun(parsed.fileCount)}`, + !viewed.supported + ? null + : filter.hideViewedFiles + ? `${filter.hiddenViewedCount} viewed ${noun(filter.hiddenViewedCount)} hidden` + : `${filter.viewedCount} viewed`, filter.hideTestFiles ? `${filter.hiddenCount} test ${noun(filter.hiddenCount)} hidden` : "Test files included", @@ -166,14 +169,26 @@ function FileFilterMenu({ filter }: { readonly filter: ChangedFiles["filter"] }) image: "line.3.horizontal.decrease", state: filter.hideTestFiles ? "on" : "off", }, + ...(filter.canHideViewedFiles + ? [ + { + id: "hide-viewed", + title: "Hide viewed files", + image: "eye.slash", + state: filter.hideViewedFiles ? ("on" as const) : ("off" as const), + }, + ] + : []), ]} - onPressAction={filter.toggle} + onPressAction={({ nativeEvent }) => + nativeEvent.event === "hide-viewed" ? filter.toggleViewedFiles() : filter.toggle() + } > @@ -182,6 +197,18 @@ function FileFilterMenu({ filter }: { readonly filter: ChangedFiles["filter"] }) function NoVisibleFiles({ files }: { readonly files: ChangedFiles }) { const { diff, filter, parsed } = files; + if (parsed.kind === "files" && parsed.files.length === 0 && filter.hiddenViewedCount > 0) { + return ( + + } + variant="plain" + /> + ); + } if (parsed.kind === "files") { return parsed.files.length === 0 && filter.hiddenCount > 0 ? ( pullRequestDiffPathMatches(linkAnchor.digest, file.path)) ?.path : undefined; + const viewed = usePullRequestFilesViewed(route.params, commit ? null : detail.data); + const reviewDraft = usePullRequestReviewDraft(route.params); const filter = usePullRequestFileFilter( result.parsed, - linkPath ?? (route.params.focus ? route.params.path : undefined), + viewed, + linkPath ?? (route.params.path || undefined), + reviewDraft.draft.comments.map((comment) => comment.path), ); const { refresh: refreshDetail } = detail; useFocusEffect(refreshDetail); const { loadMore } = result; const parsed = filter.parsed; - const reviewDraft = usePullRequestReviewDraft(route.params); - const viewed = usePullRequestFilesViewed(route.params, commit ? null : detail.data); const [commitViewedPaths, setCommitViewedPaths] = useState>(() => new Set()); const permissions = usePullRequestPermissions(route.params, detail.data); const scopeKey = pullRequestReviewKey(route.params); @@ -1089,18 +1091,24 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { const [collapsedIds, setCollapsedIds] = useState(EMPTY_IDS); const [visibleFileId, setVisibleFileId] = useState(null); const nativeView = useRef(null); - // The filter the current jump was made under. Hiding or showing test files replaces the rows, - // so a jump made under the other filter has to run again. Native applies the entry + // The filters the current jump was made under. Hiding or showing files replaces the rows, + // so a jump made under other filters has to run again. Native applies the entry // destination before its first viewport, so the first one counts as made. - const jumpedUnder = useRef(targetFile ? filter.hideTestFiles : null); + const filterChoice = { + hideTestFiles: filter.hideTestFiles, + hideViewedFiles: filter.hideViewedFiles, + }; + const jumpedUnder = useRef(targetFile ? filterChoice : null); const hasTestFiles = result.parsed.kind === "files" && result.parsed.files.some((file) => isTestChangedFile(file.path)); const jumpStarted = () => - jumpedUnder.current !== null && (!hasTestFiles || jumpedUnder.current === filter.hideTestFiles); + jumpedUnder.current !== null && + (!hasTestFiles || jumpedUnder.current.hideTestFiles === filter.hideTestFiles) && + (!filter.viewedCount || jumpedUnder.current.hideViewedFiles === filter.hideViewedFiles); const scrollToFile = (fileId: string) => { if (!nativeView.current) return; - jumpedUnder.current = filter.hideTestFiles; + jumpedUnder.current = filterChoice; void nativeView.current.scrollToFile(fileId, false).catch(() => { jumpedUnder.current = null; }); @@ -1145,7 +1153,7 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { nativeView, jumpStarted, setJumpStarted: (started) => { - jumpedUnder.current = started ? filter.hideTestFiles : null; + jumpedUnder.current = started ? filterChoice : null; }, onDebug: bridge.onDebug, loadMore, @@ -1158,11 +1166,18 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { ); }; const visibleFile = data.files.find((file) => file.id === visibleFileId) ?? targetFile; - const toggleTestFiles = () => { + const keepVisibleFile = () => { setJumpPath(visibleFile?.path ?? route.params.path); setSelectedRowId(null); + }; + const toggleTestFiles = () => { + keepVisibleFile(); filter.toggle(); }; + const toggleViewedFiles = () => { + keepVisibleFile(); + filter.toggleViewedFiles(); + }; const title = commit ? "Commit changes" : "Changes"; const seekingFile = !targetFile && result.nextCursor !== null; const loadingDiff = (result.isPending && !data.rows.length) || (seekingFile && !result.error); @@ -1170,7 +1185,7 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { navigation.goBack()} menus={[ { @@ -1219,6 +1234,17 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { selected: filter.hideTestFiles, onPress: toggleTestFiles, }, + ...(filter.canHideViewedFiles + ? [ + { + id: "hide-viewed", + title: "Hide viewed files", + icon: "eye.slash", + selected: filter.hideViewedFiles, + onPress: toggleViewedFiles, + }, + ] + : []), ...(selectedRow && selectedFile && canCommentOnDiff ? [ { @@ -1357,6 +1383,25 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { contentResetKey={`pull-request:${route.params.environmentId}:${route.params.repository}:${route.params.number}:${commit ?? "all"}`} /> + ) : filter.hiddenViewedCount > 0 && + !data.rows.length && + !result.isPending && + !result.error && + !seekingFile ? ( + + + } + variant="plain" + /> + ) : filter.hiddenCount > 0 && !data.rows.length && !result.isPending && diff --git a/apps/mobile/src/features/pull-requests/pullRequestFileFilters.test.ts b/apps/mobile/src/features/pull-requests/pullRequestFileFilters.test.ts index a7a76abc6e9e4..c5116e8451c8c 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestFileFilters.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestFileFilters.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it } from "vite-plus/test"; import { buildNativeReviewDiffData } from "../review/nativeReviewDiffAdapter"; import { buildReviewParsedDiff } from "../review/reviewModel"; -import { filterPullRequestTestFiles } from "./pullRequestFileFilters"; +import { + filterPullRequestTestFiles, + filterPullRequestViewedFiles, + viewedFilePaths, +} from "./pullRequestFileFilters"; function patch(path: string) { return `diff --git a/${path} b/${path}\n--- a/${path}\n+++ b/${path}\n@@ -1 +1 @@\n-old\n+new\n`; @@ -42,6 +46,31 @@ describe("mobile PR test file filter", () => { expect(visible.files[1]).toBe(parsed.files[2]); }); + it("hides viewed files except those kept on screen", () => { + const parsed = buildReviewParsedDiff( + ["src/read.ts", "src/opened.ts", "src/drafted.ts", "src/unread.ts"].map(patch).join("\n"), + "pr", + ); + const paths = viewedFilePaths( + parsed, + (path) => path !== "src/unread.ts", + (path) => path === "src/opened.ts" || path === "src/drafted.ts", + ); + expect(paths).toEqual({ + viewed: ["src/read.ts", "src/opened.ts", "src/drafted.ts"], + hidden: ["src/read.ts"], + }); + const visible = filterPullRequestViewedFiles(parsed, new Set(paths.hidden)); + if (parsed.kind !== "files" || visible.kind !== "files") throw new Error("Expected files"); + expect(visible.files.map((file) => file.path)).toEqual([ + "src/opened.ts", + "src/drafted.ts", + "src/unread.ts", + ]); + expect(visible.fileCount).toBe(3); + expect(filterPullRequestViewedFiles(parsed, new Set())).toBe(parsed); + }); + it("uses the current path when a file is renamed out of a test directory", () => { const parsed = buildReviewParsedDiff( "diff --git a/tests/helper.ts b/src/helper.ts\nsimilarity index 100%\nrename from tests/helper.ts\nrename to src/helper.ts\n", diff --git a/apps/mobile/src/features/pull-requests/pullRequestFileFilters.ts b/apps/mobile/src/features/pull-requests/pullRequestFileFilters.ts index 2e0424ddbbaed..8395424692a41 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestFileFilters.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestFileFilters.ts @@ -1,15 +1,9 @@ import { isTestChangedFile } from "@t3tools/shared/changedFileKind"; import type { ReviewParsedDiff } from "../review/reviewModel"; -export function filterPullRequestTestFiles( - parsed: ReviewParsedDiff, - hideTestFiles: boolean, - includePath?: string, -): ReviewParsedDiff { - if (!hideTestFiles || parsed.kind !== "files") return parsed; - const files = parsed.files.filter( - (file) => file.path === includePath || !isTestChangedFile(file.path), - ); +function keepFiles(parsed: ReviewParsedDiff, keep: (path: string) => boolean): ReviewParsedDiff { + if (parsed.kind !== "files") return parsed; + const files = parsed.files.filter((file) => keep(file.path)); if (files.length === parsed.files.length) return parsed; return { ...parsed, @@ -19,3 +13,30 @@ export function filterPullRequestTestFiles( deletions: files.reduce((total, file) => total + file.deletions, 0), }; } + +export function filterPullRequestTestFiles( + parsed: ReviewParsedDiff, + hideTestFiles: boolean, + includePath?: string, +): ReviewParsedDiff { + if (!hideTestFiles) return parsed; + return keepFiles(parsed, (path) => path === includePath || !isTestChangedFile(path)); +} + +/** Viewed files among `parsed`, and those the filter hides: every one not kept on screen. */ +export function viewedFilePaths( + parsed: ReviewParsedDiff, + isViewed: (path: string) => boolean, + isKept: (path: string) => boolean, +) { + const viewed = + parsed.kind === "files" ? parsed.files.map((file) => file.path).filter(isViewed) : []; + return { viewed, hidden: viewed.filter((path) => !isKept(path)) }; +} + +export function filterPullRequestViewedFiles( + parsed: ReviewParsedDiff, + hiddenPaths: ReadonlySet, +): ReviewParsedDiff { + return keepFiles(parsed, (path) => !hiddenPaths.has(path)); +} diff --git a/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts b/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts index d8d824c1ae5fc..f19438a6938d1 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts @@ -3,24 +3,61 @@ import { AsyncResult } from "effect/reactivity"; import { useMemo } from "react"; import type { ReviewParsedDiff } from "../review/reviewModel"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; -import { filterPullRequestTestFiles } from "./pullRequestFileFilters"; +import { + filterPullRequestTestFiles, + filterPullRequestViewedFiles, + viewedFilePaths, +} from "./pullRequestFileFilters"; +import type { usePullRequestFilesViewed } from "./usePullRequestFilesViewed"; -export function usePullRequestFileFilter(parsed: ReviewParsedDiff, includePath?: string) { +const PATH_SEPARATOR = "\0"; + +export function usePullRequestFileFilter( + parsed: ReviewParsedDiff, + viewed: ReturnType, + includePath?: string, + draftPaths: readonly string[] = [], +) { const preferences = useAtomValue(mobilePreferencesAtom); const savePreferences = useAtomSet(updateMobilePreferencesAtom); - const hideTestFiles = - AsyncResult.isSuccess(preferences) && preferences.value.pullRequestHideTestFiles === true; - const visible = useMemo( + const saved = AsyncResult.isSuccess(preferences) ? preferences.value : null; + const hideTestFiles = saved?.pullRequestHideTestFiles === true; + const hideViewedFiles = viewed.supported && saved?.pullRequestHideViewedFiles === true; + const withoutTests = useMemo( () => filterPullRequestTestFiles(parsed, hideTestFiles, includePath), [parsed, hideTestFiles, includePath], ); + const { viewed: viewedPaths, hidden } = viewedFilePaths( + withoutTests, + (path) => viewed.supported && viewed.isViewed(path), + // Files marked here stay until the screen closes, so marking one never pulls it out from under you. + (path) => path === includePath || draftPaths.includes(path) || viewed.toggledHere(path), + ); + const hiddenViewedPaths = hideViewedFiles ? hidden : []; + // A string key keeps the same diff object while the hidden set holds, so native rows aren't rebuilt. + const hiddenViewedKey = hiddenViewedPaths.join(PATH_SEPARATOR); + const visible = useMemo( + () => + hiddenViewedKey + ? filterPullRequestViewedFiles(withoutTests, new Set(hiddenViewedKey.split(PATH_SEPARATOR))) + : withoutTests, + [withoutTests, hiddenViewedKey], + ); const hiddenCount = - parsed.kind === "files" && visible.kind === "files" ? parsed.fileCount - visible.fileCount : 0; + parsed.kind === "files" && withoutTests.kind === "files" + ? parsed.fileCount - withoutTests.fileCount + : 0; return { parsed: visible, hideTestFiles, hiddenCount, toggle: () => savePreferences({ pullRequestHideTestFiles: !hideTestFiles }), showAll: () => savePreferences({ pullRequestHideTestFiles: false }), + hideViewedFiles, + viewedCount: viewedPaths.length, + hiddenViewedCount: hiddenViewedPaths.length, + canHideViewedFiles: viewed.supported && (viewedPaths.length > 0 || hideViewedFiles), + toggleViewedFiles: () => savePreferences({ pullRequestHideViewedFiles: !hideViewedFiles }), + showViewedFiles: () => savePreferences({ pullRequestHideViewedFiles: false }), }; } diff --git a/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts b/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts index 7467c73fbe403..5ef32102cbd63 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts @@ -78,6 +78,7 @@ export function usePullRequestFilesViewed(params: PullRequestParams, pr: PullReq supported, canToggle: supported && allowed && Boolean(query.data), isViewed, + toggledHere: (path: string) => Object.hasOwn(presses, path), toggle, error: error ?? query.error, truncated: query.data?.truncated, diff --git a/apps/mobile/src/persistence/mobile-preferences.ts b/apps/mobile/src/persistence/mobile-preferences.ts index ed2d0b063c767..190b33e3c5318 100644 --- a/apps/mobile/src/persistence/mobile-preferences.ts +++ b/apps/mobile/src/persistence/mobile-preferences.ts @@ -92,6 +92,7 @@ export interface Preferences { readonly navigationTabOrder?: readonly string[]; readonly hiddenNavigationTabs?: readonly string[]; readonly pullRequestHideTestFiles?: boolean; + readonly pullRequestHideViewedFiles?: boolean; readonly pullRequestMergeMethod?: PullRequestMergeMethod; readonly liveActivitiesEnabled?: boolean; readonly themeId?: MobileThemeId; @@ -178,6 +179,7 @@ function sanitizePreferences(parsed: Preferences): Preferences { navigationTabOrder?: readonly string[]; hiddenNavigationTabs?: readonly string[]; pullRequestHideTestFiles?: boolean; + pullRequestHideViewedFiles?: boolean; pullRequestMergeMethod?: PullRequestMergeMethod; liveActivitiesEnabled?: boolean; themeId?: MobileThemeId; @@ -229,6 +231,10 @@ function sanitizePreferences(parsed: Preferences): Preferences { preferences.pullRequestHideTestFiles = parsed.pullRequestHideTestFiles; } + if (typeof parsed.pullRequestHideViewedFiles === "boolean") { + preferences.pullRequestHideViewedFiles = parsed.pullRequestHideViewedFiles; + } + if ( parsed.pullRequestMergeMethod === "squash" || parsed.pullRequestMergeMethod === "merge" || From 2662fe2c27f13c6d3e19f78e871919754e067bf3 Mon Sep 17 00:00:00 2001 From: AtAFork Date: Wed, 11 Nov 2026 10:09:30 -0600 Subject: [PATCH 2/2] fix(mobile): keep drafted, picked, and marked files visible under Hide viewed files --- .../src/features/pull-requests/PullRequestFiles.tsx | 9 ++++++++- .../features/pull-requests/PullRequestRouteScreen.tsx | 7 +++++-- .../features/pull-requests/usePullRequestFileFilter.ts | 4 ++-- .../features/pull-requests/usePullRequestFilesViewed.ts | 9 ++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx b/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx index cd38f41b9fed6..613ffdf1b11c6 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestFiles.tsx @@ -30,6 +30,7 @@ import { } from "./usePullRequestDiff"; import { usePullRequestFileFilter } from "./usePullRequestFileFilter"; import { usePullRequestFilesViewed } from "./usePullRequestFilesViewed"; +import { usePullRequestReviewDraft } from "./usePullRequestReviewDraft"; import { pullRequestFileJumpScope, requestPullRequestFileJump } from "./pullRequestFileJump"; const BREAKDOWN_LABELS = { @@ -54,7 +55,13 @@ function useChangedFiles(params: PullRequestDiffParams) { ).changeRequest; const diff = usePullRequestDiff(params, terminology); const viewed = usePullRequestFilesViewed(params, pr); - const filter = usePullRequestFileFilter(diff.parsed, viewed); + const reviewDraft = usePullRequestReviewDraft(params); + const filter = usePullRequestFileFilter( + diff.parsed, + viewed, + undefined, + reviewDraft.draft.comments.map((comment) => comment.path), + ); const parsed = filter.parsed; return { diff, diff --git a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx index d18cb9ee1316a..7d3f861f561a5 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx @@ -986,11 +986,15 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { : undefined; const viewed = usePullRequestFilesViewed(route.params, commit ? null : detail.data); const reviewDraft = usePullRequestReviewDraft(route.params); + const [pickedPath, setJumpPath] = useState(null); const filter = usePullRequestFileFilter( result.parsed, viewed, linkPath ?? (route.params.path || undefined), - reviewDraft.draft.comments.map((comment) => comment.path), + [ + ...reviewDraft.draft.comments.map((comment) => comment.path), + ...(pickedPath ? [pickedPath] : []), + ], ); const { refresh: refreshDetail } = detail; useFocusEffect(refreshDetail); @@ -1060,7 +1064,6 @@ function PullRequestFileRouteContent({ route }: PullRequestFileRouteProps) { code: row.content ?? "", }); }; - const [pickedPath, setJumpPath] = useState(null); const jumpPath = pickedPath ?? linkPath ?? route.params.path; const requestedFile = result.parsed.kind === "files" diff --git a/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts b/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts index f19438a6938d1..5651df8679644 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestFileFilter.ts @@ -16,7 +16,7 @@ export function usePullRequestFileFilter( parsed: ReviewParsedDiff, viewed: ReturnType, includePath?: string, - draftPaths: readonly string[] = [], + keptPaths: readonly string[] = [], ) { const preferences = useAtomValue(mobilePreferencesAtom); const savePreferences = useAtomSet(updateMobilePreferencesAtom); @@ -31,7 +31,7 @@ export function usePullRequestFileFilter( withoutTests, (path) => viewed.supported && viewed.isViewed(path), // Files marked here stay until the screen closes, so marking one never pulls it out from under you. - (path) => path === includePath || draftPaths.includes(path) || viewed.toggledHere(path), + (path) => path === includePath || keptPaths.includes(path) || viewed.toggledHere(path), ); const hiddenViewedPaths = hideViewedFiles ? hidden : []; // A string key keeps the same diff object while the hidden set holds, so native rows aren't rebuilt. diff --git a/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts b/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts index 5ef32102cbd63..ceac02dccea36 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestFilesViewed.ts @@ -25,6 +25,9 @@ export function usePullRequestFilesViewed(params: PullRequestParams, pr: PullReq const refresh = useAtomRefresh(composerPullRequests.filesViewed(target)); const write = useAtomCommand(composerPullRequests.setFilesViewed, { reportFailure: false }); const [state, setState] = useState({ key: scopeKey, presses: {}, error: null }); + // Kept apart from `presses`, which a failed write clears, so a file marked here stays listed. + const [marked, setMarked] = useState({ key: scopeKey, paths: new Set() }); + const markedPaths = marked.key === scopeKey ? marked.paths : null; const presses = state.key === scopeKey ? state.presses : {}; const error = state.key === scopeKey ? state.error : null; const overlay = viewedOverlay(presses, query.data); @@ -39,6 +42,10 @@ export function usePullRequestFilesViewed(params: PullRequestParams, pr: PullReq if (!supported || !allowed || !query.data) return; const viewed = !isViewed(path); const version = (presses[path]?.version ?? 0) + 1; + setMarked((current) => ({ + key: scopeKey, + paths: new Set(current.key === scopeKey ? current.paths : []).add(path), + })); update((current) => ({ ...current, error: null, @@ -78,7 +85,7 @@ export function usePullRequestFilesViewed(params: PullRequestParams, pr: PullReq supported, canToggle: supported && allowed && Boolean(query.data), isViewed, - toggledHere: (path: string) => Object.hasOwn(presses, path), + toggledHere: (path: string) => markedPaths?.has(path) === true, toggle, error: error ?? query.error, truncated: query.data?.truncated,