@@ -7,119 +7,103 @@ const resolveImageUrl = (url: string): string => {
77 return url . startsWith ( 'http' ) ? url : `${ getConfigValue ( 'APPFLOWY_BASE_URL' , '' ) } ${ url } ` ;
88} ;
99
10+
11+ interface CheckImageResult {
12+ ok : boolean ;
13+ status : number ;
14+ statusText : string ;
15+ error ?: string ;
16+ validatedUrl ?: string ;
17+ }
18+
1019// Helper function to check image using Image() approach
11- const checkImageWithImageElement = (
12- imageUrl : string ,
13- resolve : ( data : {
14- ok : boolean ,
15- status : number ,
16- statusText : string ,
17- error ?: string ,
18- validatedUrl ?: string ,
19- } ) => void
20- ) => {
21- const img = new Image ( ) ;
22-
23- // Set a timeout to handle very slow loads
24- const timeoutId = setTimeout ( ( ) => {
25- resolve ( {
26- ok : false ,
27- status : 408 ,
28- statusText : 'Request Timeout' ,
29- error : 'Image loading timed out' ,
30- } ) ;
31- } , 10000 ) ; // 10 second timeout
32-
33- img . onload = ( ) => {
34- clearTimeout ( timeoutId ) ;
35- resolve ( {
36- ok : true ,
37- status : 200 ,
38- statusText : 'OK' ,
39- validatedUrl : imageUrl ,
40- } ) ;
41- } ;
42-
43- img . onerror = ( ) => {
44- clearTimeout ( timeoutId ) ;
45- resolve ( {
46- ok : false ,
47- status : 404 ,
48- statusText : 'Image Not Found' ,
49- error : 'Failed to load image' ,
50- } ) ;
51- } ;
52-
53- img . src = imageUrl ;
54- } ;
20+ const validateImageLoad = ( imageUrl : string ) : Promise < CheckImageResult > => {
21+ return new Promise ( ( resolve ) => {
22+ const img = new Image ( ) ;
23+
24+ // Set a timeout to handle very slow loads
25+ const timeoutId = setTimeout ( ( ) => {
26+ resolve ( {
27+ ok : false ,
28+ status : 408 ,
29+ statusText : 'Request Timeout' ,
30+ error : 'Image loading timed out' ,
31+ } ) ;
32+ } , 10000 ) ; // 10 second timeout
33+
34+ img . onload = ( ) => {
35+ clearTimeout ( timeoutId ) ;
36+ resolve ( {
37+ ok : true ,
38+ status : 200 ,
39+ statusText : 'OK' ,
40+ validatedUrl : imageUrl ,
41+ } ) ;
42+ } ;
43+
44+ img . onerror = ( ) => {
45+ clearTimeout ( timeoutId ) ;
46+ resolve ( {
47+ ok : false ,
48+ status : 404 ,
49+ statusText : 'Image Not Found' ,
50+ error : 'Failed to load image' ,
51+ } ) ;
52+ } ;
5553
56- export const checkImage = async ( url : string ) => {
57- return new Promise ( ( resolve : ( data : {
58- ok : boolean ,
59- status : number ,
60- statusText : string ,
61- error ?: string ,
62- validatedUrl ?: string ,
63- } ) => void ) => {
64- // If it's an AppFlowy file storage URL, try authenticated fetch first
65- if ( isAppFlowyFileStorageUrl ( url ) ) {
66- const token = getTokenParsed ( ) ;
67-
68- if ( ! token ) {
69- // Allow browser to load publicly-accessible URLs without authentication
70- // Fall through to Image() approach with resolved URL
71- const resolvedUrl = resolveImageUrl ( url ) ;
72-
73- checkImageWithImageElement ( resolvedUrl , resolve ) ;
74- return ;
75- }
54+ img . src = imageUrl ;
55+ } ) ;
56+ } ;
7657
77- const fullUrl = resolveImageUrl ( url ) ;
58+ export const checkImage = async ( url : string ) : Promise < CheckImageResult > => {
59+ // If it's an AppFlowy file storage URL, try authenticated fetch first
60+ if ( isAppFlowyFileStorageUrl ( url ) ) {
61+ const token = getTokenParsed ( ) ;
62+ const fullUrl = resolveImageUrl ( url ) ;
7863
79- fetch ( fullUrl , {
80- headers : {
81- Authorization : `Bearer ${ token . access_token } ` ,
82- } ,
83- } )
84- . then ( ( response ) => {
85- console . debug ( "fetchImageBlob response" , response ) ;
86- if ( response . ok ) {
87- // Convert to blob URL for use in img tag
88- return response . blob ( ) . then ( ( blob ) => {
89- const blobUrl = URL . createObjectURL ( blob ) ;
90-
91- resolve ( {
92- ok : true ,
93- status : 200 ,
94- statusText : 'OK' ,
95- validatedUrl : blobUrl ,
96- } ) ;
97- } ) ;
98- } else {
99- console . error ( 'Authenticated image fetch failed' , response . status , response . statusText ) ;
100- // If authenticated fetch fails, fall back to Image() approach
101- // This allows publicly-accessible URLs to still work
102- checkImageWithImageElement ( fullUrl , resolve ) ;
103- }
104- } )
105- . catch ( ( error ) => {
106- console . error ( 'Failed to fetch authenticated image' , error ) ;
107- // If fetch throws an error (CORS, network, etc.), fall back to Image() approach
108- checkImageWithImageElement ( fullUrl , resolve ) ;
64+ if ( token ) {
65+ try {
66+ const response = await fetch ( fullUrl , {
67+ headers : {
68+ Authorization : `Bearer ${ token . access_token } ` ,
69+ } ,
10970 } ) ;
110- return ;
71+
72+ if ( response . ok ) {
73+ const blob = await response . blob ( ) ;
74+ const blobUrl = URL . createObjectURL ( blob ) ;
75+
76+ return {
77+ ok : true ,
78+ status : 200 ,
79+ statusText : 'OK' ,
80+ validatedUrl : blobUrl ,
81+ } ;
82+ }
83+
84+ console . error ( 'Authenticated image fetch failed' , response . status , response . statusText ) ;
85+ } catch ( error ) {
86+ console . error ( 'Failed to fetch authenticated image' , error ) ;
87+ }
11188 }
11289
113- // For non-AppFlowy URLs, use the original Image() approach
114- checkImageWithImageElement ( url , resolve ) ;
115- } ) ;
90+ // Fallback for no token or failed fetch
91+ return validateImageLoad ( fullUrl ) ;
92+ }
93+
94+ // For non-AppFlowy URLs, use the original Image() approach
95+ return validateImageLoad ( url ) ;
11696} ;
11797
11898export const fetchImageBlob = async ( url : string ) : Promise < Blob | null > => {
11999 if ( isAppFlowyFileStorageUrl ( url ) ) {
100+ console . debug ( "fetch appflowy image blob" , url ) ;
120101 const token = getTokenParsed ( ) ;
121102
122- if ( ! token ) return null ;
103+ if ( ! token ) {
104+ console . error ( 'No authentication token available for image fetch' ) ;
105+ return null ;
106+ }
123107
124108 const fullUrl = resolveImageUrl ( url ) ;
125109
@@ -130,8 +114,21 @@ export const fetchImageBlob = async (url: string): Promise<Blob | null> => {
130114 } ,
131115 } ) ;
132116
117+ console . debug ( "fetch image blob response" , response ) ;
118+
133119 if ( response . ok ) {
134- return await response . blob ( ) ;
120+ const blob = await response . blob ( ) ;
121+
122+ // If the blob type is generic or missing, try to infer from URL
123+ if ( ( ! blob . type || blob . type === 'application/octet-stream' ) && url ) {
124+ const inferredType = getMimeTypeFromUrl ( url ) ;
125+
126+ if ( inferredType ) {
127+ return blob . slice ( 0 , blob . size , inferredType ) ;
128+ }
129+ }
130+
131+ return blob ;
135132 }
136133 } catch ( error ) {
137134 return null ;
@@ -141,12 +138,88 @@ export const fetchImageBlob = async (url: string): Promise<Blob | null> => {
141138 const response = await fetch ( url ) ;
142139
143140 if ( response . ok ) {
144- return await response . blob ( ) ;
141+ const blob = await response . blob ( ) ;
142+
143+ // If the blob type is generic or missing, try to infer from URL
144+ if ( ( ! blob . type || blob . type === 'application/octet-stream' ) && url ) {
145+ const inferredType = getMimeTypeFromUrl ( url ) ;
146+
147+ if ( inferredType ) {
148+ return blob . slice ( 0 , blob . size , inferredType ) ;
149+ }
150+ }
151+
152+ return blob ;
145153 }
146154 } catch ( error ) {
147155 return null ;
148156 }
149157 }
150158
151159 return null ;
160+ } ;
161+
162+ export const convertBlobToPng = async ( blob : Blob ) : Promise < Blob > => {
163+ return new Promise ( ( resolve , reject ) => {
164+ const img = new Image ( ) ;
165+ const url = URL . createObjectURL ( blob ) ;
166+
167+ img . onload = ( ) => {
168+ const canvas = document . createElement ( 'canvas' ) ;
169+
170+ canvas . width = img . width ;
171+ canvas . height = img . height ;
172+
173+ const ctx = canvas . getContext ( '2d' ) ;
174+
175+ if ( ! ctx ) {
176+ reject ( new Error ( 'Failed to get canvas context' ) ) ;
177+ return ;
178+ }
179+
180+ ctx . drawImage ( img , 0 , 0 ) ;
181+ canvas . toBlob ( ( pngBlob ) => {
182+ if ( pngBlob ) {
183+ resolve ( pngBlob ) ;
184+ } else {
185+ reject ( new Error ( 'Failed to convert to PNG' ) ) ;
186+ }
187+
188+ URL . revokeObjectURL ( url ) ;
189+ } , 'image/png' ) ;
190+ } ;
191+
192+ img . onerror = ( ) => {
193+ URL . revokeObjectURL ( url ) ;
194+ reject ( new Error ( 'Failed to load image for conversion' ) ) ;
195+ } ;
196+
197+ img . src = url ;
198+ } ) ;
199+ } ;
200+
201+ const getMimeTypeFromUrl = ( url : string ) : string | null => {
202+ // Handle data URLs
203+ if ( url . startsWith ( 'data:' ) ) {
204+ return url . split ( ';' ) [ 0 ] . split ( ':' ) [ 1 ] ;
205+ }
206+
207+ const cleanUrl = url . split ( '?' ) [ 0 ] ;
208+ const ext = cleanUrl . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ;
209+
210+ switch ( ext ) {
211+ case 'jpg' :
212+ case 'jpeg' :
213+ return 'image/jpeg' ;
214+ case 'png' :
215+ return 'image/png' ;
216+ case 'gif' :
217+ return 'image/gif' ;
218+ case 'webp' :
219+ return 'image/webp' ;
220+ case 'svg' :
221+ return 'image/svg+xml' ;
222+ default :
223+ return null ;
224+ }
152225} ;
0 commit comments