authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-07 22:33:55-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 22:33:55-07:00
loge803242d936af3e052757d59ab32bc7c4fcc2390
tree217b58edfca5ec9d2d7241ca8ddbfc2474fbbb65
parent65404eb75de9233c9d1068ddfd09bacfc3b0465b
signature Signed by PGP key B5690EEEBB952194

fix(mobile): restore file viewer insets and glass header (#17073)


9 files changed, 77 insertions(+), 37 deletions(-)

apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift+8
...@@ -49,6 +49,14 @@ public class T3ReviewDiffModule: Module {...@@ -49,6 +49,14 @@ public class T3ReviewDiffModule: Module {
49 view.setContentWidth(CGFloat(contentWidth))49 view.setContentWidth(CGFloat(contentWidth))
50 }50 }
5151
52 Prop("contentInsetTop") { (view: T3ReviewDiffView, inset: Double) in
53 view.setContentInsetTop(CGFloat(inset))
54 }
55
56 Prop("contentInsetBottom") { (view: T3ReviewDiffView, inset: Double) in
57 view.setContentInsetBottom(CGFloat(inset))
58 }
59
52 Prop("initialRowIndex") { (view: T3ReviewDiffView, initialRowIndex: Double) in60 Prop("initialRowIndex") { (view: T3ReviewDiffView, initialRowIndex: Double) in
53 view.setInitialRowIndex(initialRowIndex)61 view.setInitialRowIndex(initialRowIndex)
54 }62 }
apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift+28-8
...@@ -563,6 +563,24 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {...@@ -563,6 +563,24 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {
563 ])563 ])
564 }564 }
565565
566 // Source files fill the screen so UIKit can sample content below the glass bar.
567 // Keep the same visible row when header or safe-area reservations change.
568 func setContentInsetTop(_ inset: CGFloat) {
569 scrollView.automaticallyAdjustsScrollIndicatorInsets = false
570 let inset = max(0, inset)
571 let delta = inset - scrollView.contentInset.top
572 guard delta != 0 else { return }
573 scrollView.contentInset.top = inset
574 scrollView.verticalScrollIndicatorInsets.top = inset
575 scrollView.contentOffset.y -= delta
576 updateViewportFrame()
577 }
578
579 func setContentInsetBottom(_ inset: CGFloat) {
580 scrollView.contentInset.bottom = max(0, inset)
581 scrollView.verticalScrollIndicatorInsets.bottom = max(0, inset)
582 }
583
566 func setContentResetKey(_ contentResetKey: String) {584 func setContentResetKey(_ contentResetKey: String) {
567 guard contentResetKey != self.contentResetKey else {585 guard contentResetKey != self.contentResetKey else {
568 return586 return
...@@ -577,7 +595,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {...@@ -577,7 +595,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {
577 hasAppliedInitialRowIndex = false595 hasAppliedInitialRowIndex = false
578 pendingScrollFileId = nil596 pendingScrollFileId = nil
579 isProgrammaticScrollActive = false597 isProgrammaticScrollActive = false
580 scrollView.setContentOffset(.zero, animated: false)598 scrollView.setContentOffset(CGPoint(x: 0, y: -scrollView.contentInset.top), animated: false)
581 updateContentMetrics()599 updateContentMetrics()
582 updateViewportFrame()600 updateViewportFrame()
583 lastVisibleFileId = nil601 lastVisibleFileId = nil
...@@ -870,8 +888,12 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {...@@ -870,8 +888,12 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {
870 }888 }
871889
872 private func setVerticalContentOffset(_ targetOffset: CGFloat, animated: Bool) {890 private func setVerticalContentOffset(_ targetOffset: CGFloat, animated: Bool) {
873 let maxOffset = max(scrollView.contentSize.height - scrollView.bounds.height, 0)891 let topInset = scrollView.contentInset.top
874 let clampedOffset = min(max(targetOffset, 0), maxOffset)892 let maxOffset = max(
893 scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom,
894 -topInset
895 )
896 let clampedOffset = min(max(targetOffset - topInset, -topInset), maxOffset)
875 let shouldAnimate = animated && abs(scrollView.contentOffset.y - clampedOffset) > 0.5897 let shouldAnimate = animated && abs(scrollView.contentOffset.y - clampedOffset) > 0.5
876 isProgrammaticScrollActive = shouldAnimate898 isProgrammaticScrollActive = shouldAnimate
877 contentView.isVerticalScrollActive = shouldAnimate899 contentView.isVerticalScrollActive = shouldAnimate
...@@ -889,12 +911,10 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {...@@ -889,12 +911,10 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {
889 return911 return
890 }912 }
891913
892 let targetScreenY = max(0, (bounds.height - rowFrame.height) * 0.3)914 let visibleHeight = bounds.height - scrollView.contentInset.top - scrollView.contentInset.bottom
893 let maxOffset = max(scrollView.contentSize.height - scrollView.bounds.height, 0)915 let targetScreenY = max(0, (visibleHeight - rowFrame.height) * 0.3)
894 let targetOffset = min(max(rowFrame.minY - targetScreenY, 0), maxOffset)
895 hasAppliedInitialRowIndex = true916 hasAppliedInitialRowIndex = true
896 scrollView.setContentOffset(CGPoint(x: 0, y: targetOffset), animated: false)917 setVerticalContentOffset(rowFrame.minY - targetScreenY, animated: false)
897 updateViewportFrame()
898 }918 }
899}919}
900920
apps/mobile/src/Stack.tsx+2-2
...@@ -159,7 +159,7 @@ const GLASS_HEADER_OPTIONS: AppScreenOptions = {...@@ -159,7 +159,7 @@ const GLASS_HEADER_OPTIONS: AppScreenOptions = {
159};159};
160160
161// SOLID: opaque sheet-colored header for surfaces whose content scrolls internally161// SOLID: opaque sheet-colored header for surfaces whose content scrolls internally
162// (file viewer, terminal, review) — there is nothing for glass to sample there.162// (terminal, review) — there is nothing for glass to sample there.
163const SOLID_HEADER_OPTIONS: AppScreenOptions = {163const SOLID_HEADER_OPTIONS: AppScreenOptions = {
164 headerBackButtonDisplayMode: "minimal",164 headerBackButtonDisplayMode: "minimal",
165 headerBackTitle: "",165 headerBackTitle: "",
...@@ -750,7 +750,7 @@ const RootStackConfig = createWorkspaceStackNavigator({...@@ -750,7 +750,7 @@ const RootStackConfig = createWorkspaceStackNavigator({
750 ThreadFile: createNativeStackScreen({750 ThreadFile: createNativeStackScreen({
751 screen: ThreadFileScreen,751 screen: ThreadFileScreen,
752 linking: `${THREAD_LINKING_PREFIX}/files/:path*`,752 linking: `${THREAD_LINKING_PREFIX}/files/:path*`,
753 options: SOLID_HEADER_OPTIONS,753 options: GLASS_HEADER_OPTIONS,
754 }),754 }),
755 ThreadMcpApp: createNativeStackScreen({755 ThreadMcpApp: createNativeStackScreen({
756 screen: McpAppFullscreenScreen,756 screen: McpAppFullscreenScreen,
apps/mobile/src/components/ComposerContextSheet.tsx+1
...@@ -47,6 +47,7 @@ function ContextField(props: { label: string; value: string | null | undefined;...@@ -47,6 +47,7 @@ function ContextField(props: { label: string; value: string | null | undefined;
47 style={{ height: Math.min(260, Math.max(100, props.value.split("\n").length * 22 + 36)) }}47 style={{ height: Math.min(260, Math.max(100, props.value.split("\n").length * 22 + 36)) }}
48 >48 >
49 <SourceFileSurface49 <SourceFileSurface
50 embedded
50 contents={props.value}51 contents={props.value}
51 path={props.label === "HTML" ? "element.html" : "styles.css"}52 path={props.label === "HTML" ? "element.html" : "styles.css"}
52 />53 />
apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts+2
...@@ -121,6 +121,8 @@ export interface NativeReviewDiffViewProps extends ViewProps {...@@ -121,6 +121,8 @@ export interface NativeReviewDiffViewProps extends ViewProps {
121 readonly styleJson?: string;121 readonly styleJson?: string;
122 readonly rowHeight: number;122 readonly rowHeight: number;
123 readonly contentWidth: number;123 readonly contentWidth: number;
124 readonly contentInsetTop?: number;
125 readonly contentInsetBottom?: number;
124 readonly initialRowIndex?: number;126 readonly initialRowIndex?: number;
125 readonly refreshing?: boolean;127 readonly refreshing?: boolean;
126 readonly nativeViewRef?: Ref<NativeReviewDiffViewHandle>;128 readonly nativeViewRef?: Ref<NativeReviewDiffViewHandle>;
apps/mobile/src/features/files/FileTreeBrowser.tsx+6-18
...@@ -8,7 +8,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";...@@ -8,7 +8,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
8import { AppText as Text } from "../../components/AppText";8import { AppText as Text } from "../../components/AppText";
9import { PierreEntryIcon } from "../../components/PierreEntryIcon";9import { PierreEntryIcon } from "../../components/PierreEntryIcon";
10import { cn } from "../../lib/cn";10import { cn } from "../../lib/cn";
11import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";11import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
12import {12import {
13 buildFileTree,13 buildFileTree,
14 flattenFileTree,14 flattenFileTree,
...@@ -42,7 +42,6 @@ const FileTreeRow = memo(function FileTreeRow(props: {...@@ -42,7 +42,6 @@ const FileTreeRow = memo(function FileTreeRow(props: {
42 readonly item: VisibleFileTreeNode;42 readonly item: VisibleFileTreeNode;
43 readonly selected: boolean;43 readonly selected: boolean;
44 readonly expanded: boolean;44 readonly expanded: boolean;
45 readonly loaded: boolean;
46 readonly loading: boolean;45 readonly loading: boolean;
47 readonly onPressDirectory: (path: string, expand: boolean) => void;46 readonly onPressDirectory: (path: string, expand: boolean) => void;
48 readonly onPreviewFile?: (path: string) => void;47 readonly onPreviewFile?: (path: string) => void;
...@@ -98,10 +97,6 @@ const FileTreeRow = memo(function FileTreeRow(props: {...@@ -98,10 +97,6 @@ const FileTreeRow = memo(function FileTreeRow(props: {
98 </Text>97 </Text>
99 {node.kind === "directory" && props.expanded && props.loading ? (98 {node.kind === "directory" && props.expanded && props.loading ? (
100 <ActivityIndicator size="small" accessibilityLabel={`Loading ${node.name}`} />99 <ActivityIndicator size="small" accessibilityLabel={`Loading ${node.name}`} />
101 ) : node.kind === "directory" && props.loaded ? (
102 <Text className="text-2xs font-t3-medium text-foreground-tertiary">
103 {node.children.length}
104 </Text>
105 ) : null}100 ) : null}
106 </Pressable>101 </Pressable>
107 );102 );
...@@ -115,7 +110,6 @@ export function FileTreeBrowser(props: {...@@ -115,7 +110,6 @@ export function FileTreeBrowser(props: {
115 readonly searchQuery: string;110 readonly searchQuery: string;
116 readonly searchTruncated: boolean;111 readonly searchTruncated: boolean;
117 readonly selectedPath: string | null;112 readonly selectedPath: string | null;
118 readonly loadedDirectories: ReadonlySet<string>;
119 readonly loadingDirectories: ReadonlySet<string>;113 readonly loadingDirectories: ReadonlySet<string>;
120 readonly onLoadDirectory: (path: string) => void;114 readonly onLoadDirectory: (path: string) => void;
121 readonly onPreviewFile?: (path: string) => void;115 readonly onPreviewFile?: (path: string) => void;
...@@ -128,14 +122,12 @@ export function FileTreeBrowser(props: {...@@ -128,14 +122,12 @@ export function FileTreeBrowser(props: {
128 readonly selectedPathAtPress: string | null;122 readonly selectedPathAtPress: string | null;
129 } | null>(null);123 } | null>(null);
130 const insets = useSafeAreaInsets();124 const insets = useSafeAreaInsets();
131 // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the125 const columnMetrics = useNativeColumnLayoutMetrics();
132 // observed adjustedContentInset bottom (~102) seen in the native trace.126 const headerInset = Platform.OS === "ios" ? (columnMetrics?.safeArea.top ?? insets.top) : 0;
133 const headerInset = Platform.OS === "ios" ? insets.top + IOS_NAV_BAR_HEIGHT : 0;
134 const {127 const {
135 onLoadDirectory,128 onLoadDirectory,
136 onPreviewFile,129 onPreviewFile,
137 onSelectFile,130 onSelectFile,
138 loadedDirectories,
139 loadingDirectories,131 loadingDirectories,
140 selectedPath: controlledSelectedPath,132 selectedPath: controlledSelectedPath,
141 } = props;133 } = props;
...@@ -226,7 +218,6 @@ export function FileTreeBrowser(props: {...@@ -226,7 +218,6 @@ export function FileTreeBrowser(props: {
226 item={item}218 item={item}
227 selected={item.node.kind === "file" && item.node.path === selectedPath}219 selected={item.node.kind === "file" && item.node.path === selectedPath}
228 expanded={expandedPaths.has(item.node.path)}220 expanded={expandedPaths.has(item.node.path)}
229 loaded={loadedDirectories.has(item.node.path)}
230 loading={loadingDirectories.has(item.node.path)}221 loading={loadingDirectories.has(item.node.path)}
231 onPressDirectory={toggleDirectory}222 onPressDirectory={toggleDirectory}
232 onPreviewFile={onPreviewFile}223 onPreviewFile={onPreviewFile}
...@@ -237,7 +228,6 @@ export function FileTreeBrowser(props: {...@@ -237,7 +228,6 @@ export function FileTreeBrowser(props: {
237 expandedPaths,228 expandedPaths,
238 handleSelectFile,229 handleSelectFile,
239 onPreviewFile,230 onPreviewFile,
240 loadedDirectories,
241 loadingDirectories,231 loadingDirectories,
242 selectedPath,232 selectedPath,
243 toggleDirectory,233 toggleDirectory,
...@@ -245,8 +235,8 @@ export function FileTreeBrowser(props: {...@@ -245,8 +235,8 @@ export function FileTreeBrowser(props: {
245 );235 );
246236
247 const extraData = useMemo(237 const extraData = useMemo(
248 () => ({ expandedPaths, loadedDirectories, loadingDirectories, selectedPath }),238 () => ({ expandedPaths, loadingDirectories, selectedPath }),
249 [expandedPaths, loadedDirectories, loadingDirectories, selectedPath],239 [expandedPaths, loadingDirectories, selectedPath],
250 );240 );
251241
252 // UIKit owns the header inset on every supported iOS version. Keep the242 // UIKit owns the header inset on every supported iOS version. Keep the
...@@ -259,9 +249,7 @@ export function FileTreeBrowser(props: {...@@ -259,9 +249,7 @@ export function FileTreeBrowser(props: {
259 data={visibleNodes}249 data={visibleNodes}
260 keyExtractor={(item) => item.node.path}250 keyExtractor={(item) => item.node.path}
261 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}251 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}
262 scrollIndicatorInsets={252 automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}
263 Platform.OS === "ios" ? { top: headerInset, left: 0, right: 0, bottom: 0 } : undefined
264 }
265 keyboardDismissMode="on-drag"253 keyboardDismissMode="on-drag"
266 keyboardShouldPersistTaps="handled"254 keyboardShouldPersistTaps="handled"
267 estimatedItemSize={42}255 estimatedItemSize={42}
apps/mobile/src/features/files/SourceFileSurface.tsx+23-6
...@@ -11,9 +11,10 @@ import {...@@ -11,9 +11,10 @@ import {
11 useWindowDimensions,11 useWindowDimensions,
12 View,12 View,
13} from "react-native";13} from "react-native";
1414import { useSafeAreaInsets } from "react-native-safe-area-context";
15import { AppText as Text } from "../../components/AppText";15import { AppText as Text } from "../../components/AppText";
16import { LoadingStrip } from "../../components/LoadingStrip";16import { LoadingStrip } from "../../components/LoadingStrip";
17import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
17import {18import {
18 type NativeReviewDiffViewProps,19 type NativeReviewDiffViewProps,
19 resolveNativeReviewDiffView,20 resolveNativeReviewDiffView,
...@@ -39,7 +40,11 @@ import { sourceHighlightAtom } from "./sourceHighlightingState";...@@ -39,7 +40,11 @@ import { sourceHighlightAtom } from "./sourceHighlightingState";
39interface SourceFileSurfaceProps {40interface SourceFileSurfaceProps {
40 readonly contents: string;41 readonly contents: string;
41 readonly path: string;42 readonly path: string;
43 /** The enclosing sheet or card already reserves its own chrome and safe area. */
44 readonly embedded?: boolean;
42 readonly initialLine?: number | null;45 readonly initialLine?: number | null;
46 /** Override when a fixed notice already reserves the native header. */
47 readonly headerInsetTop?: number;
43 /** Keep the entire document in one native text-selection scope. */48 /** Keep the entire document in one native text-selection scope. */
44 readonly selectable?: boolean;49 readonly selectable?: boolean;
45 /** Enables native pull-to-refresh on the source surface. */50 /** Enables native pull-to-refresh on the source surface. */
...@@ -184,6 +189,9 @@ function NativeSourceFileSurface(...@@ -184,6 +189,9 @@ function NativeSourceFileSurface(
184 },189 },
185) {190) {
186 const { NativeView, onRefresh } = props;191 const { NativeView, onRefresh } = props;
192 const insets = useSafeAreaInsets();
193 const columnMetrics = useNativeColumnLayoutMetrics();
194 const safeArea = columnMetrics?.safeArea ?? insets;
187 const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface();195 const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface();
188 const { themeAppearance, themeId } = useAppearancePreferences();196 const { themeAppearance, themeId } = useAppearancePreferences();
189 const appTheme = useUniwindTheme();197 const appTheme = useUniwindTheme();
...@@ -214,6 +222,8 @@ function NativeSourceFileSurface(...@@ -214,6 +222,8 @@ function NativeSourceFileSurface(
214 appearanceScheme={themeAppearance}222 appearanceScheme={themeAppearance}
215 contentResetKey={props.path}223 contentResetKey={props.path}
216 contentWidth={contentWidth}224 contentWidth={contentWidth}
225 contentInsetTop={props.embedded ? 0 : (props.headerInsetTop ?? safeArea.top)}
226 contentInsetBottom={props.embedded ? 0 : safeArea.bottom + 8}
217 initialRowIndex={targetIndex ?? -1}227 initialRowIndex={targetIndex ?? -1}
218 rowHeight={nativeSourceStyle.rowHeight ?? codeSurface.rowHeight}228 rowHeight={nativeSourceStyle.rowHeight ?? codeSurface.rowHeight}
219 rowsJson={rowsJson}229 rowsJson={rowsJson}
...@@ -233,6 +243,7 @@ function NativeSourceFileSurface(...@@ -233,6 +243,7 @@ function NativeSourceFileSurface(
233}243}
234244
235function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {245function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {
246 const usesAutomaticInsets = Platform.OS === "ios" && !props.embedded;
236 const foreground = useUniwindTheme()["--color-foreground"];247 const foreground = useUniwindTheme()["--color-foreground"];
237 const { codeSurface, codeWordBreak } = useAppearanceCodeSurface();248 const { codeSurface, codeWordBreak } = useAppearanceCodeSurface();
238 const { normalizedContents, lines, status, targetIndex, tokens } = useSourceFileModel(props);249 const { normalizedContents, lines, status, targetIndex, tokens } = useSourceFileModel(props);
...@@ -323,6 +334,8 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {...@@ -323,6 +334,8 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {
323 const list = (334 const list = (
324 <FlatList335 <FlatList
325 ref={listRef}336 ref={listRef}
337 contentInsetAdjustmentBehavior={usesAutomaticInsets ? "automatic" : "never"}
338 automaticallyAdjustsScrollIndicatorInsets={usesAutomaticInsets}
326 refreshControl={refreshControl}339 refreshControl={refreshControl}
327 data={lines}340 data={lines}
328 keyExtractor={(_line, index) => String(index)}341 keyExtractor={(_line, index) => String(index)}
...@@ -354,6 +367,8 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {...@@ -354,6 +367,8 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {
354 <ScrollView367 <ScrollView
355 refreshControl={refreshControl}368 refreshControl={refreshControl}
356 className="flex-1"369 className="flex-1"
370 contentInsetAdjustmentBehavior={usesAutomaticInsets ? "automatic" : "never"}
371 automaticallyAdjustsScrollIndicatorInsets={usesAutomaticInsets}
357 contentContainerStyle={{372 contentContainerStyle={{
358 paddingBottom: codeSurface.rowHeight,373 paddingBottom: codeSurface.rowHeight,
359 paddingHorizontal: 12,374 paddingHorizontal: 12,
...@@ -393,9 +408,11 @@ export function SourceFileSurface(props: SourceFileSurfaceProps) {...@@ -393,9 +408,11 @@ export function SourceFileSurface(props: SourceFileSurfaceProps) {
393 // The native canvas draws source lines without text selection or wrapping. Attachments408 // The native canvas draws source lines without text selection or wrapping. Attachments
394 // need one selectable text view in either wrap mode; workspace line navigation can still409 // need one selectable text view in either wrap mode; workspace line navigation can still
395 // use the canvas when wrapping is disabled.410 // use the canvas when wrapping is disabled.
396 return NativeView && !codeWordBreak && !props.selectable ? (411 const surface =
397 <NativeSourceFileSurface {...props} NativeView={NativeView} />412 NativeView && !codeWordBreak && !props.selectable ? (
398 ) : (413 <NativeSourceFileSurface {...props} NativeView={NativeView} />
399 <JavaScriptSourceFileSurface {...props} />414 ) : (
400 );415 <JavaScriptSourceFileSurface {...props} />
416 );
417 return surface;
401}418}
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+7-2
...@@ -48,6 +48,7 @@ import { FileTreeBrowser } from "./FileTreeBrowser";...@@ -48,6 +48,7 @@ import { FileTreeBrowser } from "./FileTreeBrowser";
48import { useFileTreeEntries } from "./useFileTreeEntries";48import { useFileTreeEntries } from "./useFileTreeEntries";
49import { preloadWorkspaceFileContents } from "./preload-workspace-file";49import { preloadWorkspaceFileContents } from "./preload-workspace-file";
50import { SourceFileSurface } from "./SourceFileSurface";50import { SourceFileSurface } from "./SourceFileSurface";
51import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
51import { ThreadFileNavigatorPane } from "./thread-file-navigator-pane";52import { ThreadFileNavigatorPane } from "./thread-file-navigator-pane";
52import { ScreenHeader } from "../../components/ScreenHeader";53import { ScreenHeader } from "../../components/ScreenHeader";
53import { WorkspaceFileImagePreview } from "./WorkspaceFileImagePreview";54import { WorkspaceFileImagePreview } from "./WorkspaceFileImagePreview";
...@@ -229,6 +230,10 @@ function FileContent(props: {...@@ -229,6 +230,10 @@ function FileContent(props: {
229}) {230}) {
230 // Reopening a mutable host file must not reuse a poster from an earlier visit.231 // Reopening a mutable host file must not reuse a poster from an earlier visit.
231 const thumbnailInstanceId = useId();232 const thumbnailInstanceId = useId();
233 const insets = useSafeAreaInsets();
234 const columnMetrics = useNativeColumnLayoutMetrics();
235 const reservedHeaderInset =
236 Platform.OS === "ios" && props.truncated ? (columnMetrics?.safeArea.top ?? insets.top) : 0;
232 const isMarkdown = isMarkdownPreviewFile(props.relativePath);237 const isMarkdown = isMarkdownPreviewFile(props.relativePath);
233 const isBrowserFile = isWorkspaceBrowserPreviewPath(props.relativePath);238 const isBrowserFile = isWorkspaceBrowserPreviewPath(props.relativePath);
234 const isImageFile = isWorkspaceImagePreviewPath(props.relativePath);239 const isImageFile = isWorkspaceImagePreviewPath(props.relativePath);
...@@ -300,7 +305,7 @@ function FileContent(props: {...@@ -300,7 +305,7 @@ function FileContent(props: {
300 }305 }
301306
302 return (307 return (
303 <View className="flex-1 bg-sheet">308 <View className="flex-1 bg-sheet" style={{ paddingTop: reservedHeaderInset }}>
304 {props.truncated ? (309 {props.truncated ? (
305 <FilePreviewNotice title="Partial file">310 <FilePreviewNotice title="Partial file">
306 Preview limited to the first 1 MB of a truncated file.311 Preview limited to the first 1 MB of a truncated file.
...@@ -320,6 +325,7 @@ function FileContent(props: {...@@ -320,6 +325,7 @@ function FileContent(props: {
320 contents={props.fileContents}325 contents={props.fileContents}
321 path={props.relativePath}326 path={props.relativePath}
322 initialLine={props.initialLine}327 initialLine={props.initialLine}
328 headerInsetTop={props.truncated ? 0 : undefined}
323 onRefresh={props.onRefresh}329 onRefresh={props.onRefresh}
324 />330 />
325 )}331 )}
...@@ -561,7 +567,6 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -561,7 +567,6 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
561 <FileTreeBrowser567 <FileTreeBrowser
562 key={JSON.stringify([environmentId, cwd])}568 key={JSON.stringify([environmentId, cwd])}
563 entries={entriesQuery.entries}569 entries={entriesQuery.entries}
564 loadedDirectories={entriesQuery.loadedDirectories}
565 loadingDirectories={entriesQuery.loadingDirectories}570 loadingDirectories={entriesQuery.loadingDirectories}
566 onLoadDirectory={entriesQuery.loadDirectory}571 onLoadDirectory={entriesQuery.loadDirectory}
567 error={entriesQuery.error}572 error={entriesQuery.error}
apps/mobile/src/features/files/thread-file-navigator-pane.tsx-1
...@@ -87,7 +87,6 @@ export function ThreadFileNavigatorPane(props: {...@@ -87,7 +87,6 @@ export function ThreadFileNavigatorPane(props: {
87 <FileTreeBrowser87 <FileTreeBrowser
88 key={JSON.stringify([props.environmentId, props.cwd])}88 key={JSON.stringify([props.environmentId, props.cwd])}
89 entries={entriesQuery.entries}89 entries={entriesQuery.entries}
90 loadedDirectories={entriesQuery.loadedDirectories}
91 loadingDirectories={entriesQuery.loadingDirectories}90 loadingDirectories={entriesQuery.loadingDirectories}
92 onLoadDirectory={entriesQuery.loadDirectory}91 onLoadDirectory={entriesQuery.loadDirectory}
93 error={92 error={