@@ -268,7 +268,7 @@ export function FileViewer({
268268 }
269269
270270 if ( category === 'image-previewable' ) {
271- return < ImagePreview file = { file } />
271+ return < ImagePreview file = { file } workspaceId = { workspaceId } />
272272 }
273273
274274 if ( category === 'docx-previewable' ) {
@@ -997,8 +997,20 @@ const ZOOM_BUTTON_FACTOR = 1.2
997997
998998const clampZoom = ( z : number ) => Math . min ( Math . max ( z , ZOOM_MIN ) , ZOOM_MAX )
999999
1000- const ImagePreview = memo ( function ImagePreview ( { file } : { file : WorkspaceFileRecord } ) {
1001- const serveUrl = `/api/files/serve/${ encodeURIComponent ( file . key ) } ?context=workspace&t=${ file . size } `
1000+ const ImagePreview = memo ( function ImagePreview ( {
1001+ file,
1002+ workspaceId,
1003+ } : {
1004+ file : WorkspaceFileRecord
1005+ workspaceId : string
1006+ } ) {
1007+ const {
1008+ data : fileData ,
1009+ isLoading,
1010+ error : fetchError ,
1011+ } = useWorkspaceFileBinary ( workspaceId , file . id , file . key )
1012+ const [ blobUrl , setBlobUrl ] = useState < string | null > ( null )
1013+ const blobUrlRef = useRef < string | null > ( null )
10021014 const [ zoom , setZoom ] = useState ( 1 )
10031015 const [ offset , setOffset ] = useState ( { x : 0 , y : 0 } )
10041016 const isDragging = useRef ( false )
@@ -1009,6 +1021,15 @@ const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileR
10091021
10101022 const containerRef = useRef < HTMLDivElement > ( null )
10111023
1024+ const replaceBlobUrl = useCallback ( ( nextUrl : string | null ) => {
1025+ const previousUrl = blobUrlRef . current
1026+ blobUrlRef . current = nextUrl
1027+ setBlobUrl ( nextUrl )
1028+ if ( previousUrl && previousUrl !== nextUrl ) {
1029+ URL . revokeObjectURL ( previousUrl )
1030+ }
1031+ } , [ ] )
1032+
10121033 const zoomIn = useCallback ( ( ) => setZoom ( ( z ) => clampZoom ( z * ZOOM_BUTTON_FACTOR ) ) , [ ] )
10131034 const zoomOut = useCallback ( ( ) => setZoom ( ( z ) => clampZoom ( z / ZOOM_BUTTON_FACTOR ) ) , [ ] )
10141035
@@ -1027,6 +1048,24 @@ const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileR
10271048 return ( ) => el . removeEventListener ( 'wheel' , onWheel )
10281049 } , [ ] )
10291050
1051+ useEffect ( ( ) => {
1052+ replaceBlobUrl ( null )
1053+ } , [ file . id , file . key , replaceBlobUrl ] )
1054+
1055+ useEffect ( ( ) => {
1056+ return ( ) => {
1057+ if ( blobUrlRef . current ) {
1058+ URL . revokeObjectURL ( blobUrlRef . current )
1059+ blobUrlRef . current = null
1060+ }
1061+ }
1062+ } , [ ] )
1063+
1064+ useEffect ( ( ) => {
1065+ if ( ! fileData ) return
1066+ replaceBlobUrl ( URL . createObjectURL ( new Blob ( [ fileData ] , { type : file . type || 'image/png' } ) ) )
1067+ } , [ file . type , fileData , replaceBlobUrl ] )
1068+
10301069 const handleMouseDown = useCallback ( ( e : React . MouseEvent ) => {
10311070 if ( e . button !== 0 ) return
10321071 isDragging . current = true
@@ -1052,7 +1091,21 @@ const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileR
10521091 useEffect ( ( ) => {
10531092 setZoom ( 1 )
10541093 setOffset ( { x : 0 , y : 0 } )
1055- } , [ file . key ] )
1094+ } , [ blobUrl ] )
1095+
1096+ const error = blobUrl !== null ? null : resolvePreviewError ( fetchError , null )
1097+
1098+ if ( error ) {
1099+ return < PreviewError label = 'Image' error = { error } />
1100+ }
1101+
1102+ if ( isLoading && ! blobUrl ) {
1103+ return (
1104+ < div className = 'flex h-full items-center justify-center' >
1105+ < Skeleton className = 'h-[200px] w-[80%]' />
1106+ </ div >
1107+ )
1108+ }
10561109
10571110 return (
10581111 < div
@@ -1071,7 +1124,7 @@ const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileR
10711124 } }
10721125 >
10731126 < img
1074- src = { serveUrl }
1127+ src = { blobUrl ?? undefined }
10751128 alt = { file . name }
10761129 className = 'max-h-full max-w-full select-none rounded-md object-contain'
10771130 draggable = { false }
0 commit comments