diff --git a/cspell.json b/cspell.json index e57c3333e581..f00cc37bc863 100644 --- a/cspell.json +++ b/cspell.json @@ -257,6 +257,7 @@ "WAVAX", "webextension", "webm", + "webp", "withdrawed", "wmatic", "wnative", diff --git a/packages/mask/src/components/shared/Image.tsx b/packages/mask/src/components/shared/Image.tsx index 4521818e0382..00da7a77d999 100644 --- a/packages/mask/src/components/shared/Image.tsx +++ b/packages/mask/src/components/shared/Image.tsx @@ -35,6 +35,7 @@ export interface ImageProps { className?: string style?: React.CSSProperties onClick?: React.MouseEventHandler + onError?(event: Event): void onURL?(url: string): void } @@ -48,7 +49,18 @@ export type ImageRef = { * This React Component is used to render images in the content script to bypass the CSP restriction. */ export const Image = forwardRef(function Image(props, outgoingRef) { - const { src, loading: propsLoading, canvasProps, imgProps, style, className, SkeletonProps, onClick, onURL } = props + const { + src, + loading: propsLoading, + canvasProps, + imgProps, + style, + className, + SkeletonProps, + onClick, + onURL, + onError, + } = props // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#Maximum_canvas_size const [height, width] = [Math.min(32767, props.height || 500), Math.min(32767, props.width || 500)] const [hasCSPBan, setHasCSPBan] = useState(false) @@ -112,7 +124,7 @@ export const Image = forwardRef(function Image(props, outg return ( setHasCSPBan(true) : undefined} + onError={blobURL ? () => setHasCSPBan(true) : (event) => onError?.(event as unknown as Event)} width={width} height={height} className={className} diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx index 7386acd3af68..16c604a76954 100644 --- a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx +++ b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx @@ -1,9 +1,9 @@ -import { Card, Link } from '@mui/material' +import { Card, Link, useTheme } from '@mui/material' import { makeStyles } from '@masknet/theme' import { Wallet, ERC721TokenDetailed, resolveCollectibleLink, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' -import { MaskSharpIconOfSize } from '../../../../resources/MaskIcon' import { NFTCardStyledAssetPlayer } from '@masknet/shared' import { ActionsBarNFT } from '../ActionsBarNFT' +import { Image } from '../../../../components/shared/Image' const useStyles = makeStyles()((theme) => ({ root: { @@ -64,6 +64,12 @@ export interface CollectibleCardProps { export function CollectibleCard(props: CollectibleCardProps) { const { wallet, token, provider, readonly, renderOrder } = props const { classes } = useStyles() + const isImage = /\.(gif|svg|png|webp|jpg)$/.test(token.info.mediaUrl ?? '') + const theme = useTheme() + const fallbackImageURL = + theme.palette.mode === 'dark' + ? new URL('./nft_token_fallback_dark.png', import.meta.url) + : new URL('./nft_token_fallback.png', import.meta.url) return ( )} {token.info.mediaUrl ? ( - + isImage ? ( + { + const target = event.currentTarget as HTMLImageElement + target.src = fallbackImageURL.toString() + target.classList.add(classes.loadingFailImage ?? '') + }} + /> + ) : ( + + ) ) : ( - + )} diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/nft_token_fallback.png b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/nft_token_fallback.png new file mode 100644 index 000000000000..80895f895a33 Binary files /dev/null and b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/nft_token_fallback.png differ diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/nft_token_fallback_dark.png b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/nft_token_fallback_dark.png new file mode 100644 index 000000000000..c71580a52ef0 Binary files /dev/null and b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/nft_token_fallback_dark.png differ diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx index df48bd8d7e25..b557f5a5e5f9 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx @@ -58,6 +58,7 @@ export function ArticleTab(props: ArticleTabProps) { playsInline: true, }} classes={classes} + isFixedIframeSize={false} /> diff --git a/packages/shared/src/UI/components/AssetPlayer/index.tsx b/packages/shared/src/UI/components/AssetPlayer/index.tsx index bc1c0de39947..00ec0bc5deed 100644 --- a/packages/shared/src/UI/components/AssetPlayer/index.tsx +++ b/packages/shared/src/UI/components/AssetPlayer/index.tsx @@ -32,6 +32,7 @@ interface AssetPlayerProps renderTimeout?: number iconProps?: SvgIconProps fallbackImage?: URL + isFixedIframeSize?: boolean showIframeFromInit?: boolean setERC721TokenName?: (name: string) => void setSourceType?: (type: string) => void @@ -50,8 +51,9 @@ enum AssetPlayerState { ERROR = 3, } -export const AssetPlayer = memo(({ url, type, options, iconProps, ...props }) => { +export const AssetPlayer = memo((props) => { const ref = useRef(null) + const { url, type, options, iconProps, isFixedIframeSize = true } = props const classes = useStylesExtends(useStyles(), props) const [hidden, setHidden] = useState(Boolean(props.renderTimeout)) const { RPC: RPC_Entries } = getRPCConstants(props.erc721Token?.chainId) @@ -121,6 +123,28 @@ export const AssetPlayer = memo(({ url, type, options, iconPro setIframe() }, [setIframe]) + // Workaround for a bug of `iframe-resizer-react`: + // When the content of iframe loaded, `IframeResizer` triggers a `size` event, + // but the `height` and `width` value of that `size` event isn't equal to the content. + // (Sometimes it doesn't matter, if the size of iframe has been set fixed already) + // Meanwhile `IframeResizer` triggers a `resize` event when the size of + // parent of iframe changed, this time the returned `height` and `width` is right. + // So resize the parent manually. + useEffect(() => { + if (playerState !== AssetPlayerState.NORMAL && isFixedIframeSize) return + const resize = (height: string) => () => { + if (!ref.current?.parentElement) return + ref.current.parentElement.style.height = height + } + const noSenseHeight = '100px' + const timerOne = setTimeout(resize(noSenseHeight), 100) + const timerTwo = setTimeout(resize(''), 150) + return () => { + clearTimeout(timerOne) + clearTimeout(timerTwo) + } + }, [playerState, ref.current]) + const IframeResizerMemo = useMemo( () => hidden ? null : ( @@ -150,6 +174,7 @@ export const AssetPlayer = memo(({ url, type, options, iconPro checkOrigin={false} onMessage={onMessage} frameBorder="0" + resizeFrom="child" allow="autoplay" allowFullScreen />