import React, { useState } from 'react'; import { openModal } from 'soapbox/actions/modals'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card'; import Card from 'soapbox/features/status/components/card'; import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch, useSettings } from 'soapbox/hooks'; import { addAutoPlay } from 'soapbox/utils/media'; import type { List as ImmutableList } from 'immutable'; import type VideoType from 'soapbox/features/video'; import type { Status, Attachment } from 'soapbox/types/entities'; interface IStatusMedia { /** Status entity to render media for. */ status: Status, /** Whether to display compact media. */ muted?: boolean, /** Callback when compact media is clicked. */ onClick?: () => void, /** Whether or not the media is concealed behind a NSFW banner. */ showMedia?: boolean, /** Callback when visibility is toggled (eg clicked through NSFW). */ onToggleVisibility?: () => void, } /** Render media attachments for a status. */ const StatusMedia: React.FC<IStatusMedia> = ({ status, muted = false, onClick, showMedia = true, onToggleVisibility = () => { }, }) => { const dispatch = useAppDispatch(); const settings = useSettings(); const shouldAutoPlayVideo = settings.get('autoPlayVideo'); const [mediaWrapperWidth, setMediaWrapperWidth] = useState<number | undefined>(undefined); const size = status.media_attachments.size; const firstAttachment = status.media_attachments.first(); let media: JSX.Element | null = null; const setRef = (c: HTMLDivElement): void => { if (c) { setMediaWrapperWidth(c.offsetWidth); } }; const renderLoadingMediaGallery = (): JSX.Element => { return <div className='media_gallery' style={{ height: '285px' }} />; }; const renderLoadingVideoPlayer = (): JSX.Element => { return <div className='media-spoiler-video' style={{ height: '285px' }} />; }; const renderLoadingAudioPlayer = (): JSX.Element => { return <div className='media-spoiler-audio' style={{ height: '285px' }} />; }; const openMedia = (media: ImmutableList<Attachment>, index: number) => { dispatch(openModal('MEDIA', { media, status, index })); }; if (size > 0 && firstAttachment) { if (muted) { media = ( <AttachmentThumbs media={status.media_attachments} onClick={onClick} sensitive={status.sensitive} /> ); } else if (size === 1 && firstAttachment.type === 'video') { const video = firstAttachment; if (video.external_video_id && status.card) { const getHeight = (): number => { const width = Number(video.meta.getIn(['original', 'width'])); const height = Number(video.meta.getIn(['original', 'height'])); return Number(mediaWrapperWidth) / (width / height); }; const height = getHeight(); media = ( <div className='status-card horizontal compact interactive status-card--video'> <div ref={setRef} className='status-card__image status-card-video' style={height ? { height } : undefined} dangerouslySetInnerHTML={{ __html: shouldAutoPlayVideo ? addAutoPlay(status.card.html) : status.card.html, }} /> </div> ); } else { media = ( <Bundle fetchComponent={Video} loading={renderLoadingVideoPlayer}> {(Component: typeof VideoType) => ( <Component preview={video.preview_url} blurhash={video.blurhash} src={video.url} alt={video.description} aspectRatio={Number(video.meta.getIn(['original', 'aspect']))} height={285} visible={showMedia} inline /> )} </Bundle> ); } } else if (size === 1 && firstAttachment.type === 'audio') { const attachment = firstAttachment; media = ( <Bundle fetchComponent={Audio} loading={renderLoadingAudioPlayer}> {(Component: any) => ( <Component src={attachment.url} alt={attachment.description} poster={attachment.preview_url !== attachment.url ? attachment.preview_url : status.getIn(['account', 'avatar_static'])} backgroundColor={attachment.meta.getIn(['colors', 'background'])} foregroundColor={attachment.meta.getIn(['colors', 'foreground'])} accentColor={attachment.meta.getIn(['colors', 'accent'])} duration={attachment.meta.getIn(['original', 'duration'], 0)} height={263} /> )} </Bundle> ); } else { media = ( <Bundle fetchComponent={MediaGallery} loading={renderLoadingMediaGallery}> {(Component: any) => ( <Component media={status.media_attachments} sensitive={status.sensitive} height={285} onOpenMedia={openMedia} visible={showMedia} onToggleVisibility={onToggleVisibility} /> )} </Bundle> ); } } else if (status.spoiler_text.length === 0 && !status.quote && status.card) { media = ( <Card onOpenMedia={openMedia} card={status.card} compact /> ); } else if (status.expectsCard) { media = ( <PlaceholderCard /> ); } if (media) { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions <div onClick={e => e.stopPropagation()}> {media} </div> ); } else { return null; } }; export default StatusMedia;