import { forwardRef, memo, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { ICON_CACHE, YT_ICON_CACHE } from "utils/constants"; import { cleanUpBufferUrl, imageSrc, imageSrcs } from "utils/functions"; export type IconProps = { $eager?: boolean; $moving?: boolean; displaySize?: number; imgSize: number; }; type StyledIconProps = Pick & { $height: number; $loaded: boolean; $offset: number | string; $width: number; }; const StyledIcon = styled.img .withConfig({ shouldForwardProp: (prop, defaultValidatorFn) => ["fetchpriority"].includes(prop) || defaultValidatorFn(prop), }) .attrs(({ $eager = false, $height, $width }) => ({ decoding: "async", draggable: false, fetchpriority: $eager ? "high" : undefined, height: $height, loading: $eager ? "eager" : "lazy", width: $width, }))` aspect-ratio: 1; left: ${({ $offset }) => $offset || undefined}; max-height: ${({ $height }) => $height}px; max-width: ${({ $width }) => $width}px; min-height: ${({ $height }) => $height}px; min-width: ${({ $width }) => $width}px; object-fit: contain; opacity: ${({ $moving }) => ($moving ? 0.5 : 1)}; top: ${({ $offset }) => $offset || undefined}; visibility: ${({ $loaded }) => ($loaded ? "visible" : "hidden")}; `; const StyledPicture = styled.picture` pointer-events: none; `; const SUPPORTED_PIXEL_RATIOS = [3, 2, 1]; const Icon = forwardRef< HTMLImageElement, IconProps & React.ImgHTMLAttributes >((props, ref) => { const [loaded, setLoaded] = useState(false); const { displaySize = 0, imgSize = 0, src = "", ...componentProps } = props; const isStaticIcon = !src || src.startsWith("blob:") || src.startsWith("http:") || src.startsWith("https:") || src.startsWith("data:") || src.startsWith(ICON_CACHE) || src.startsWith(YT_ICON_CACHE) || src.endsWith(".ico"); const dimensionProps = useMemo(() => { const size = displaySize > imgSize ? imgSize : displaySize || imgSize; const $offset = displaySize > imgSize ? `${displaySize - imgSize}px` : 0; return { $height: size, $offset, $width: size, }; }, [displaySize, imgSize]); const [failedUrls, setFailedUrls] = useState([]); useEffect( () => () => { if (loaded && src.startsWith("blob:")) cleanUpBufferUrl(src); }, [loaded, src] ); const RenderedIcon = ( { const { currentSrc = "" } = (target || {}) as HTMLImageElement; if (currentSrc && !failedUrls.includes(currentSrc)) { const { pathname } = new URL(currentSrc); setFailedUrls((currentFailedUrls) => [ ...currentFailedUrls, pathname, ]); } }} onLoad={() => setLoaded(true)} src={isStaticIcon ? src : imageSrc(src, imgSize, 1, ".png")} srcSet={ isStaticIcon ? undefined : imageSrcs(src, imgSize, ".png", failedUrls) } {...componentProps} {...dimensionProps} /> ); return ( {!isStaticIcon && SUPPORTED_PIXEL_RATIOS.map((ratio) => { const srcSet = imageSrc(src, imgSize, ratio, ".webp"); const mediaRatio = ratio - 0.99; if ( failedUrls.length > 0 && failedUrls.includes(srcSet.split(" ")[0]) ) { // eslint-disable-next-line unicorn/no-null return null; } return ( 1 ? `(min-resolution: ${mediaRatio}x), (-webkit-min-device-pixel-ratio: ${mediaRatio})` : undefined } srcSet={srcSet} type="image/webp" /> ); })} {RenderedIcon} ); }); export default memo(Icon);