import styles from './text.module.css' import { useState, useEffect, useMemo, useCallback, memo } from 'react' import { decodeProxyUrl, IMGPROXY_URL_REGEXP, MEDIA_DOMAIN_REGEXP } from '@/lib/url' import { useMe } from './me' import { UNKNOWN_LINK_REL } from '@/lib/constants' import classNames from 'classnames' import { useCarousel } from './carousel' function LinkRaw ({ href, children, src, rel }) { const isRawURL = /^https?:\/\//.test(children?.[0]) return ( // eslint-disable-next-line {isRawURL || !children ? src : children} ) } const Media = memo(function Media ({ src, bestResSrc, srcSet, sizes, width, height, onClick, onError, style, className, video }) { return (
{video ?
) }) export default function MediaOrLink ({ linkFallback = true, ...props }) { const media = useMediaHelper(props) const [error, setError] = useState(false) const { showCarousel, addMedia, removeMedia } = useCarousel() useEffect(() => { if (!media.image) return addMedia({ src: media.bestResSrc, originalSrc: media.originalSrc, rel: props.rel }) }, [media.image]) const handleClick = useCallback(() => showCarousel({ src: media.bestResSrc }), [showCarousel, media.bestResSrc]) const handleError = useCallback((err) => { console.error('Error loading media', err) removeMedia(media.bestResSrc) setError(true) }, [setError, removeMedia, media.bestResSrc]) if (!media.src) return null if (!error) { if (media.image || media.video) { return ( ) } } if (linkFallback) { return } return null } // determines how the media should be displayed given the params, me settings, and editor tab export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) => { const { me } = useMe() const trusted = useMemo(() => !!srcSetIntital || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetIntital, src]) const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {} const [isImage, setIsImage] = useState(video === false && trusted) const [isVideo, setIsVideo] = useState(video) const showMedia = useMemo(() => tab === 'preview' || me?.privates?.showImagesAndVideos !== false, [tab, me?.privates?.showImagesAndVideos]) useEffect(() => { // don't load the video at all if user doesn't want these if (!showMedia || isVideo || isImage) return // make sure it's not a false negative by trying to load URL as const img = new window.Image() img.onload = () => setIsImage(true) img.src = src const video = document.createElement('video') video.onloadeddata = () => setIsVideo(true) video.src = src return () => { img.onload = null img.src = '' video.onloadeddata = null video.src = '' } }, [src, setIsImage, setIsVideo, showMedia, isVideo]) const srcSet = useMemo(() => { if (Object.keys(srcSetObj).length === 0) return undefined // srcSetObj shape: { [widthDescriptor]: , ... } return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url], i, arr) => { // backwards compatibility: we used to replace image urls with imgproxy urls rather just storing paths if (!url.startsWith('http')) { url = new URL(url, process.env.NEXT_PUBLIC_IMGPROXY_URL).toString() } return acc + `${url} ${wDescriptor}` + (i < arr.length - 1 ? ', ' : '') }, '') }, [srcSetObj]) const sizes = useMemo(() => srcSet ? `${(topLevel ? 100 : 66)}vw` : undefined) // get source url in best resolution const bestResSrc = useMemo(() => { if (Object.keys(srcSetObj).length === 0) return src return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url]) => { if (!url.startsWith('http')) { url = new URL(url, process.env.NEXT_PUBLIC_IMGPROXY_URL).toString() } const w = Number(wDescriptor.replace(/w$/, '')) return w > acc.w ? { w, url } : acc }, { w: 0, url: undefined }).url }, [srcSetObj]) const [style, width, height] = useMemo(() => { if (dimensions) { const { width, height } = dimensions const style = { '--height': `${height}px`, '--width': `${width}px`, '--aspect-ratio': `${width} / ${height}` } return [style, width, height] } return [] }, [dimensions?.width, dimensions?.height]) return { src, srcSet, originalSrc: IMGPROXY_URL_REGEXP.test(src) ? decodeProxyUrl(src) : src, sizes, bestResSrc, style, width, height, image: (!me?.privates?.imgproxyOnly || trusted) && showMedia && isImage && !isVideo, video: !me?.privates?.imgproxyOnly && showMedia && isVideo } }