From bf7a1a2b6c3705c95a7e7bee6316baa4b4cf6233 Mon Sep 17 00:00:00 2001 From: sabraman Date: Tue, 8 Sep 2026 05:39:59 +0300 Subject: [PATCH] feat(mobile): add markdown preview toggle to the file header --- .../src/components/AndroidScreenHeader.tsx | 12 ++++- .../features/files/ThreadFilesRouteScreen.tsx | 47 +++++++++++++++++-- .../src/persistence/mobile-preferences.ts | 6 +++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/components/AndroidScreenHeader.tsx b/apps/mobile/src/components/AndroidScreenHeader.tsx index 46bc2c7c0912..f672830d3645 100644 --- a/apps/mobile/src/components/AndroidScreenHeader.tsx +++ b/apps/mobile/src/components/AndroidScreenHeader.tsx @@ -18,6 +18,7 @@ export function AndroidHeaderIconButton(props: { readonly icon: AppSymbolName; readonly onPress?: () => void; readonly disabled?: boolean; + readonly filled?: boolean; }) { return ( diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 29bbdb49c890..e2ecbfe304e1 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -49,6 +49,9 @@ import { } from "../layout/native-mail-search-toolbar"; import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; +import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; +import { useAtomSet, useAtomValue } from "@effect/atom-react"; +import { AsyncResult } from "effect/unstable/reactivity"; import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; import { FileMarkdownPreview } from "./FileMarkdownPreview"; import { FileTreeBrowser } from "./FileTreeBrowser"; @@ -547,6 +550,12 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { readonly mode: FileViewMode; } | null>(null); const [previewRevision, setPreviewRevision] = useState(0); + const preferencesResult = useAtomValue(mobilePreferencesAtom); + const savePreferences = useAtomSet(updateMobilePreferencesAtom); + const markdownPreviewEnabled = + AsyncResult.isSuccess(preferencesResult) && + preferencesResult.value.markdownPreviewEnabled === true; + const primaryColor = useUniwindTheme()["--color-primary"]; const previewKey = JSON.stringify([environmentId, cwd, relativePath, previewRevision]); const [fullScreenPreview, setFullScreenPreview] = useState(null); const isVideoFile = relativePath !== null && isVideoPreviewFile(relativePath); @@ -554,14 +563,29 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { relativePath !== null && !isVideoFile && isWorkspaceBrowserPreviewPath(relativePath); const isImageFile = relativePath !== null && !isVideoFile && isWorkspaceImagePreviewPath(relativePath); + const isMarkdownFile = relativePath !== null && isMarkdownPreviewFile(relativePath); const canPreview = relativePath !== null && (isMarkdownPreviewFile(relativePath) || isBrowserFile || isImageFile || isVideoFile); const activeMode = - relativePath !== null && modeOverride?.path === relativePath - ? modeOverride.mode - : defaultViewMode(relativePath); + relativePath !== null && targetLine !== null + ? "source" + : relativePath !== null && modeOverride?.path === relativePath + ? modeOverride.mode + : isMarkdownFile && markdownPreviewEnabled + ? "preview" + : defaultViewMode(relativePath); const resolvedActiveMode = isVideoFile ? "preview" : canPreview ? activeMode : "source"; + const handleToggleMarkdownMode = useCallback(() => { + if (relativePath === null || targetLine !== null) return; + const next: FileViewMode = resolvedActiveMode === "preview" ? "source" : "preview"; + setModeOverride({ path: relativePath, mode: next }); + void savePreferences({ markdownPreviewEnabled: next === "preview" }); + }, [relativePath, resolvedActiveMode, savePreferences, targetLine]); + const markdownToggleLabel = + isMarkdownFile && resolvedActiveMode === "preview" + ? "Show markdown source" + : "Show rendered markdown"; const assetPreviewPath = isBrowserFile || isImageFile || isVideoFile ? relativePath : null; const assetPreview = useWorkspaceFileAssetUrlState({ cwd, @@ -843,6 +867,14 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { onBack={handleBack} trailing={ <> + {isMarkdownFile && targetLine === null ? ( + + ) : null} {fileInspector.supported ? ( ) : null} + {isMarkdownFile && targetLine === null ? ( + + ) : null} {fileMenuActions.some(({ inline }) => inline) ? ( diff --git a/apps/mobile/src/persistence/mobile-preferences.ts b/apps/mobile/src/persistence/mobile-preferences.ts index f14a73f15cb2..a27cdc8aaab9 100644 --- a/apps/mobile/src/persistence/mobile-preferences.ts +++ b/apps/mobile/src/persistence/mobile-preferences.ts @@ -26,6 +26,8 @@ export interface Preferences { readonly markdownFontSize?: number; readonly codeFontSize?: number | null; readonly codeWordBreak?: boolean; + /** Device-local mirror of web's `renderMarkdown` preference for markdown file previews. */ + readonly markdownPreviewEnabled?: boolean; readonly connectOnboardingOptOutAccounts?: ReadonlyArray; readonly collapsedProjectGroups?: readonly string[]; /** @deprecated Kept temporarily so older OTA bundles retain the selected mode. */ @@ -95,6 +97,7 @@ function sanitizePreferences(parsed: Preferences): Preferences { markdownFontSize?: number; codeFontSize?: number | null; codeWordBreak?: boolean; + markdownPreviewEnabled?: boolean; connectOnboardingOptOutAccounts?: ReadonlyArray; collapsedProjectGroups?: readonly string[]; projectGroupingEnabled?: boolean; @@ -144,6 +147,9 @@ function sanitizePreferences(parsed: Preferences): Preferences { preferences.codeFontSize = parsed.codeFontSize; } if (typeof parsed.codeWordBreak === "boolean") preferences.codeWordBreak = parsed.codeWordBreak; + if (typeof parsed.markdownPreviewEnabled === "boolean") { + preferences.markdownPreviewEnabled = parsed.markdownPreviewEnabled; + } if (Array.isArray(parsed.connectOnboardingOptOutAccounts)) { preferences.connectOnboardingOptOutAccounts = parsed.connectOnboardingOptOutAccounts.filter( (account): account is string => typeof account === "string",