From c792e1f4f4bc040a0c499f8cbc98a207fc82fbb8 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 29 Jul 2026 10:11:35 +0300 Subject: [PATCH] fix: load mobile image files via runtime fetch (fix for relay) Fetch image blobs directly for mobile file previews Create and clean up object URLs for loaded images Remove the separate image auth token flow --- packages/ui/src/apps/MobileFilesSurface.tsx | 72 ++++++++++----------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/apps/MobileFilesSurface.tsx b/packages/ui/src/apps/MobileFilesSurface.tsx index e4080749..5ec4e314 100644 --- a/packages/ui/src/apps/MobileFilesSurface.tsx +++ b/packages/ui/src/apps/MobileFilesSurface.tsx @@ -30,9 +30,7 @@ import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry'; import { getDefaultTheme } from '@/lib/theme/themes'; import { getImageMimeType, getLanguageFromExtension, isImageFile } from '@/lib/toolHelpers'; import type { FileListEntry, FileSearchResult } from '@/lib/api/types'; -import { getRuntimeUrlResolver } from '@/lib/runtime-url'; -import { refreshRuntimeUrlAuthToken } from '@/lib/runtime-auth'; -import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; +import { runtimeFetch } from '@/lib/runtime-fetch'; import { cn } from '@/lib/utils'; type MobileFilesRoute = @@ -77,13 +75,6 @@ const formatFileSize = (size?: number): string => { return ''; }; -const getImageSrc = (path: string): string => { - if (path.toLowerCase().endsWith('.svg')) { - return ''; - } - return getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', { path }); -}; - const isMarkdownFile = (path: string): boolean => /\.(md|mdx|markdown)$/i.test(path); const isJsonFile = (path: string): boolean => /\.(json|jsonc)$/i.test(path); @@ -104,6 +95,7 @@ export const MobileFilesSurface: React.FC = ({ onClose const [searchResults, setSearchResults] = React.useState([]); const [isSearching, setIsSearching] = React.useState(false); const [fileContent, setFileContent] = React.useState(''); + const [imageSrc, setImageSrc] = React.useState(''); const [fileError, setFileError] = React.useState(null); const [isLoadingFile, setIsLoadingFile] = React.useState(false); const directoryLoadRequestIdRef = React.useRef(0); @@ -180,11 +172,35 @@ export const MobileFilesSurface: React.FC = ({ onClose React.useEffect(() => { if (route.type !== 'file') return; setFileContent(''); + setImageSrc(''); setFileError(null); if (isImageFile(route.path) && !route.path.toLowerCase().endsWith('.svg')) { - setIsLoadingFile(false); - return; + let cancelled = false; + let objectUrl = ''; + setIsLoadingFile(true); + void runtimeFetch('/api/fs/raw', { query: { path: route.path, directory: root || undefined } }) + .then(async (response) => { + if (!response.ok) throw new Error(t('filesView.error.readFileFailed')); + objectUrl = URL.createObjectURL(await response.blob()); + if (cancelled) { + URL.revokeObjectURL(objectUrl); + objectUrl = ''; + return; + } + setImageSrc(objectUrl); + }) + .catch((error) => { + if (!cancelled) setFileError(error instanceof Error ? error.message : t('filesView.error.readFileFailed')); + }) + .finally(() => { + if (!cancelled) setIsLoadingFile(false); + }); + + return () => { + cancelled = true; + if (objectUrl) URL.revokeObjectURL(objectUrl); + }; } if (!files.readFile) { @@ -212,7 +228,7 @@ export const MobileFilesSurface: React.FC = ({ onClose return () => { cancelled = true; }; - }, [files, route, t]); + }, [files, root, route, t]); const openDirectory = (directory: string) => { setQuery(''); @@ -244,6 +260,7 @@ export const MobileFilesSurface: React.FC = ({ onClose setRoute({ type: 'browser', directory: route.returnDirectory })} @@ -393,37 +410,14 @@ const MobileSearchResults: React.FC<{ const MobileFileDetail: React.FC<{ path: string; content: string; + imageSrc: string; error: string | null; isLoading: boolean; onBack: () => void; onCopyPath: () => void; onCopyContent: () => void; -}> = ({ path, content, error, isLoading, onBack, onCopyPath, onCopyContent }) => { +}> = ({ path, content, imageSrc, error, isLoading, onBack, onCopyPath, onCopyContent }) => { const { t } = useI18n(); - const imageAuthKey = isImageFile(path) && !path.toLowerCase().endsWith('.svg') ? path : ''; - const [imageAuthReadyKey, setImageAuthReadyKey] = React.useState(''); - - React.useEffect(() => { - if (!imageAuthKey) { - setImageAuthReadyKey(''); - return; - } - - let cancelled = false; - setImageAuthReadyKey(''); - void refreshRuntimeUrlAuthToken(getRuntimeApiBaseUrl()) - .then((token) => { - if (!cancelled && token) setImageAuthReadyKey(imageAuthKey); - }) - .catch(() => {}); - - return () => { - cancelled = true; - }; - }, [imageAuthKey]); - - const imageAuthLoading = Boolean(imageAuthKey && imageAuthReadyKey !== imageAuthKey); - const imageSrc = imageAuthLoading ? '' : getImageSrc(path); return (
@@ -449,7 +443,7 @@ const MobileFileDetail: React.FC<{
- {isLoading || imageAuthLoading ? ( + {isLoading ? ( ) : error ? (