import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils'; import { getHostname } from '../../lib/utils/url-utils'; import useExpandedMediaStore from '../../stores/use-expanded-media-store'; import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import useIsMobile from '../../hooks/use-is-mobile'; import styles from './comment-media.module.css'; import Embed, { canEmbed } from '../embed'; interface MediaProps { commentMediaInfo?: CommentMediaInfo; deleted?: boolean; disableToggle?: boolean; displayHeight?: string; displayWidth?: string; isFloatingEmbed?: boolean; isOutOfFeed?: boolean; isReply?: boolean; linkHeight?: number; linkWidth?: number; parentCid?: string; removed?: boolean; spoiler?: boolean; showThumbnail?: boolean; setShowThumbnail: (showThumbnail: boolean) => void; } const Thumbnail = ({ commentMediaInfo, deleted, displayHeight, displayWidth, isFloatingEmbed, isOutOfFeed, isReply, removed, spoiler, setShowThumbnail }: MediaProps) => { const isMobile = useIsMobile(); const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {}; let thumbnailComponent: React.ReactNode = null; const iframeThumbnail = patternThumbnailUrl || thumbnail; const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined); const hasThumbnail = getHasThumbnail(commentMediaInfo, url); if (type === 'gif') { thumbnailComponent = setShowThumbnail(false)} />; } else if (type === 'video') { thumbnailComponent = thumbnail ? ( ) : ( // show first frame of the video, as a workaround for Safari not loading thumbnails setShowThumbnail(false)} /> ); } else if (type === 'webpage') { thumbnailComponent = setShowThumbnail(false)} />; } else if (type === 'iframe') { thumbnailComponent = iframeThumbnail ? setShowThumbnail(false)} /> : null; } else if (type === 'audio') { thumbnailComponent = ; } const thumbnailSmallPadding = isMobile ? styles.thumbnailMobile : styles.thumbnailReplyDesktop; const thumbnailDimensions = { '--width': displayWidth, '--height': displayHeight } as React.CSSProperties; const linkWithoutThumbnail = url && new URL(url); return deleted || removed ? ( ) : spoiler ? ( setShowThumbnail(false)} /> ) : isOutOfFeed ? ( {thumbnailComponent} ) : isMobile || isReply ? ( {thumbnailComponent} {isMobile && !hasThumbnail && linkWithoutThumbnail && (canEmbed(linkWithoutThumbnail) ? ( setShowThumbnail(false)}>{getHostname(url)} ) : ( {getHostname(url) || (url.length > 30 ? url.slice(0, 30) + '...' : url)} ))} ) : ( {thumbnailComponent} ); }; const Media = ({ commentMediaInfo, disableToggle, isReply, setShowThumbnail }: MediaProps) => { const { t } = useTranslation(); const { thumbnail, type, url } = commentMediaInfo || {}; const isMobile = useIsMobile(); const { fitExpandedImagesToScreen } = useExpandedMediaStore(); const mediaClass = `${isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp} ${ fitExpandedImagesToScreen ? styles.fitToScreen : '' }`; const mediaDimensions = getMediaDimensions(commentMediaInfo); return ( {type === 'iframe' && url ? ( ) : type === 'gif' ? ( setShowThumbnail(true)} /> ) : type === 'video' ? ( ) : type === 'webpage' ? ( setShowThumbnail(true)} /> ) : null} {isMobile && type && ( {url && url.length > 30 ? url.slice(0, 30) + '...' : url} {' '} ({getDisplayMediaInfoType(type, t)} {mediaDimensions && `, ${mediaDimensions}`}) )} {isMobile && (type === 'iframe' || type === 'video' || type === 'audio') && ( setShowThumbnail(true)}> {t('close')} )} ); }; interface ImageProps { commentMediaInfo: CommentMediaInfo; disableToggle?: boolean; displayHeight: string; displayWidth: string; initialExpanded?: boolean; isOutOfFeed: boolean; parentCid?: string; spoiler?: boolean; } const Image = ({ commentMediaInfo, disableToggle = false, displayHeight, displayWidth, initialExpanded = false, isOutOfFeed, parentCid, spoiler }: ImageProps) => { const { t } = useTranslation(); const { type, url } = commentMediaInfo || {}; const isReply = parentCid; const isMobile = useIsMobile(); const [isImageExpanded, setIsImageExpanded] = useState(initialExpanded); const { fitExpandedImagesToScreen } = useExpandedMediaStore(); const mediaDimensions = getMediaDimensions(commentMediaInfo); const mediaClass = `${isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp} ${ fitExpandedImagesToScreen ? styles.fitToScreen : '' }`; const thumbnailSmallPadding = isMobile ? styles.thumbnailMobile : styles.thumbnailReplyDesktop; const thumbnailDimensions = { '--width': displayWidth, '--height': displayHeight } as React.CSSProperties; const [hasError, setHasError] = useState(false); const handleError = () => setHasError(true); if (spoiler && !isImageExpanded) { const spoilerDimensions = { '--width': '150px', '--height': '150px' } as React.CSSProperties; return ( setIsImageExpanded(true)} /> ); } return isMobile ? ( {hasError ? ( ) : ( setIsImageExpanded(!isImageExpanded)} /> )} {isImageExpanded && type && ( {url && url.length > 30 ? url.slice(0, 30) + '...' : url} {' '} ({getDisplayMediaInfoType(type, t)} {mediaDimensions && `, ${mediaDimensions}`}) )} {type && !isImageExpanded && {`${spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}} ) : ( {hasError ? ( ) : ( setIsImageExpanded(!isImageExpanded)} /> )} ); }; const CommentMedia = ({ commentMediaInfo, deleted, disableToggle, isFloatingEmbed, linkHeight, linkWidth, parentCid, removed, showThumbnail, setShowThumbnail, spoiler, }: MediaProps) => { const isReply = parentCid; const { t } = useTranslation(); const isMobile = useIsMobile(); const { thumbnailHeight, thumbnailWidth, url } = commentMediaInfo || {}; let type = commentMediaInfo?.type; const gifFrameUrl = useFetchGifFirstFrame(url); if (type === 'gif' && gifFrameUrl) { type = 'animated gif'; } else if (type === 'gif' && !gifFrameUrl) { type = 'static gif'; } let displayWidth, displayHeight; const maxThumbnailSize = isMobile || isReply ? 125 : 250; if (linkWidth && linkHeight) { // use the dimensions from the plebbit-js api let scale = Math.min(1, maxThumbnailSize / Math.max(linkWidth, linkHeight)); displayWidth = `${linkWidth * scale}px`; displayHeight = `${linkHeight * scale}px`; } else if (thumbnailHeight && thumbnailWidth) { // use the dimensions from the thumbnail fetched by useCommentMediaInfo let scale = Math.min(1, maxThumbnailSize / Math.max(thumbnailWidth, thumbnailHeight)); displayWidth = `${thumbnailWidth * scale}px`; displayHeight = `${thumbnailHeight * scale}px`; } else { // use the default size displayWidth = `${maxThumbnailSize}px`; displayHeight = `${maxThumbnailSize}px`; } if (type === 'audio') { displayWidth = '100%'; displayHeight = '100%'; } const isOutOfFeed = isFloatingEmbed || spoiler || false; // virtuoso wrapper unneeded return ( {commentMediaInfo?.type === 'image' ? ( // images just enlarge when clicked, so they don't need two separate components // when showThumbnail is explicitly false (e.g., from embed button), start expanded ) : ( <> {url && ( )} {isMobile && type && {`${spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}} {!showThumbnail && } > )} ); }; export default CommentMedia;