@@ -23,6 +23,7 @@ import VideoSmallIcon from '../../assets/img/video-small.svg';
2323import VideoPngIcon from '../../assets/img/video.png' ;
2424import VideoPngDarkIcon from '../../assets/img/video_dark.png' ;
2525import FileType from '../../constants/FileType' ;
26+ import { isSettledHashMismatch } from '../../hooks/selectNFTPreviewState' ;
2627import useCache from '../../hooks/useCache' ;
2728import useFileType from '../../hooks/useFileType' ;
2829import useHideObjectionableContent from '../../hooks/useHideObjectionableContent' ;
@@ -141,7 +142,7 @@ export default function NFTPreview(props: NFTPreviewProps) {
141142 const nftId = useMemo ( ( ) => getNFTId ( id ) , [ id ] ) ;
142143 const iframeRef = useRef < any > ( null ) ;
143144 const { isDarkMode } = useDarkMode ( ) ;
144- const [ , setError ] = useStateAbort < Error | undefined > ( undefined ) ;
145+ const [ prepareError , setPrepareError ] = useStateAbort < Error | undefined > ( undefined ) ;
145146 const [ previewContent , setPreviewContent ] = useStateAbort < ReactNode | undefined > ( undefined ) ;
146147 const abortControllerRef = useRef ( new AbortController ( ) ) ;
147148 const [ hideObjectionableContent ] = useHideObjectionableContent ( ) ;
@@ -172,15 +173,20 @@ export default function NFTPreview(props: NFTPreviewProps) {
172173
173174 const { isLoading : isLoadingNFT } = useNFT ( nftId ) ;
174175 const { metadata, isLoading : isLoadingMetadata } = useNFTMetadata ( nftId ) ;
175- const isLoading = isLoadingVerifyHash || isLoadingMetadata || isLoadingNFT || isLoadingFileType ;
176+ // hash verification downloads the full data file, which can take a long
177+ // time for large media, and the metadata host can be slow or dead — either
178+ // one only blocks the tile while there is no verified preview uri to show
179+ const isLoading = isLoadingNFT || isLoadingFileType || ( ( isLoadingVerifyHash || isLoadingMetadata ) && ! preview ) ;
176180
177181 const blurPreview = useMemo ( ( ) => {
178182 if ( ! hideObjectionableContent ) {
179183 return false ;
180184 }
181185
182- if ( isLoading ) {
183- return false ;
186+ // a verified preview can render before the metadata fetch settles — keep
187+ // it covered until the sensitive-content flag can actually be read
188+ if ( isLoadingMetadata ) {
189+ return true ;
184190 }
185191
186192 if ( ! metadata ) {
@@ -192,16 +198,22 @@ export default function NFTPreview(props: NFTPreviewProps) {
192198 }
193199
194200 return false ;
195- } , [ hideObjectionableContent , isLoading , metadata ] ) ;
201+ } , [ hideObjectionableContent , isLoadingMetadata , metadata ] ) ;
196202
197203 const previewExtension = useMemo ( ( ) => getFileExtension ( preview ?. uri ) , [ preview ] ) ;
198204
205+ // The cached bytes of a settled mismatch must never reach the iframe, even
206+ // though the state still carries the uri so the hash badge can report it.
207+ const isHashMismatch = isSettledHashMismatch ( preview ) ;
208+
209+ const previewUri = isHashMismatch ? undefined : preview ?. uri ;
210+
199211 const preparePreview = useCallback (
200212 async ( signal : AbortSignal ) => {
201213 try {
202- setError ( undefined , signal ) ;
214+ setPrepareError ( undefined , signal ) ;
203215
204- if ( ! preview ?. uri ) {
216+ if ( ! previewUri ) {
205217 setPreviewContent ( undefined , signal ) ;
206218 return ;
207219 }
@@ -232,9 +244,10 @@ export default function NFTPreview(props: NFTPreviewProps) {
232244 }
233245 ` ;
234246
235- const cachedURI = await getURI ( preview . uri , { maxSize : ignoreSizeLimit ? - 1 : undefined } ) ;
247+ const cachedURI = await getURI ( previewUri , { maxSize : ignoreSizeLimit ? - 1 : undefined } ) ;
236248 if ( ! cachedURI || ! cachedURI . startsWith ( 'cache://' ) ) {
237249 setPreviewContent ( undefined , signal ) ;
250+ setPrepareError ( new Error ( t `File is not available` ) , signal ) ;
238251 return ;
239252 }
240253
@@ -276,10 +289,21 @@ export default function NFTPreview(props: NFTPreviewProps) {
276289 signal ,
277290 ) ;
278291 } catch ( e ) {
279- setError ( e as Error , signal ) ;
292+ setPreviewContent ( undefined , signal ) ;
293+ setPrepareError ( e as Error , signal ) ;
280294 }
281295 } ,
282- [ preview , fit , getURI , ignoreSizeLimit , previewFileType , loopVideo , isDarkMode , setPreviewContent , setError ] ,
296+ [
297+ previewUri ,
298+ fit ,
299+ getURI ,
300+ ignoreSizeLimit ,
301+ previewFileType ,
302+ loopVideo ,
303+ isDarkMode ,
304+ setPreviewContent ,
305+ setPrepareError ,
306+ ] ,
283307 ) ;
284308
285309 useEffect ( ( ) => {
@@ -482,6 +506,13 @@ export default function NFTPreview(props: NFTPreviewProps) {
482506
483507 const hasFile = ! ! preview ;
484508
509+ // icon, model, document and compact non-image previews do not render the
510+ // iframe, so they never wait for the preview file to download
511+ const usesIframe =
512+ ! ( isCompact && previewFileType !== FileType . IMAGE ) &&
513+ ! icon &&
514+ ! [ FileType . MODEL , FileType . DOCUMENT ] . includes ( previewFileType ) ;
515+
485516 return (
486517 < StyledCardPreview width = { width } height = { height } sx = { { aspectRatio : ratio . toString ( ) } } >
487518 { isLoading ? (
@@ -494,6 +525,22 @@ export default function NFTPreview(props: NFTPreviewProps) {
494525 < Trans > No file available</ Trans >
495526 </ IconMessage >
496527 </ Background >
528+ ) : isHashMismatch ? (
529+ < Background >
530+ < IconMessage icon = { < NotInterested fontSize = "large" /> } >
531+ < Trans > File does not match the expected hash</ Trans >
532+ </ IconMessage >
533+ </ Background >
534+ ) : usesIframe && prepareError ? (
535+ < Background >
536+ < IconMessage icon = { < NotInterested fontSize = "large" /> } >
537+ < Trans > Preview is not available</ Trans >
538+ </ IconMessage >
539+ </ Background >
540+ ) : usesIframe && ! previewContent ? (
541+ < Flex position = "absolute" left = "0" top = "0" bottom = "0" right = "0" justifyContent = "center" alignItems = "center" >
542+ < Loading center > { ! isCompact && t `Loading preview...` } </ Loading >
543+ </ Flex >
497544 ) : (
498545 previewIframe
499546 ) }
0 commit comments