import punycode from 'punycode'; import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import Immutable from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Blurhash } from 'mastodon/components/blurhash'; import { Icon } from 'mastodon/components/icon'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { useBlurhash } from 'mastodon/initial_state'; const IDNA_PREFIX = 'xn--'; const decodeIDNA = domain => { return domain .split('.') .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) .join('.'); }; const getHostname = url => { const parser = document.createElement('a'); parser.href = url; return parser.hostname; }; const domParser = new DOMParser(); const addAutoPlay = html => { const document = domParser.parseFromString(html, 'text/html').documentElement; const iframe = document.querySelector('iframe'); if (iframe) { if (iframe.src.indexOf('?') !== -1) { iframe.src += '&'; } else { iframe.src += '?'; } iframe.src += 'autoplay=1&auto_play=1'; // DOM parser creates html/body elements around original HTML fragment, // so we need to get innerHTML out of the body and not the entire document return document.querySelector('body').innerHTML; } return html; }; export default class Card extends PureComponent { static propTypes = { card: ImmutablePropTypes.map, onOpenMedia: PropTypes.func.isRequired, sensitive: PropTypes.bool, }; state = { previewLoaded: false, embedded: false, revealed: !this.props.sensitive, }; UNSAFE_componentWillReceiveProps (nextProps) { if (!Immutable.is(this.props.card, nextProps.card)) { this.setState({ embedded: false, previewLoaded: false }); } if (this.props.sensitive !== nextProps.sensitive) { this.setState({ revealed: !nextProps.sensitive }); } } componentDidMount () { window.addEventListener('resize', this.handleResize, { passive: true }); } componentWillUnmount () { window.removeEventListener('resize', this.handleResize); } handleEmbedClick = () => { this.setState({ embedded: true }); }; setRef = c => { this.node = c; }; handleImageLoad = () => { this.setState({ previewLoaded: true }); }; handleReveal = e => { e.preventDefault(); e.stopPropagation(); this.setState({ revealed: true }); }; renderVideo () { const { card } = this.props; const content = { __html: addAutoPlay(card.get('html')) }; return (
); } render () { const { card } = this.props; const { embedded, revealed } = this.state; if (card === null) { return null; } const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); const interactive = card.get('type') === 'video'; const language = card.get('language') || ''; const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive; const description = (
{provider} {card.get('published_at') && <> ยท } {card.get('title')} {card.get('author_name').length > 0 ? {card.get('author_name')} }} /> : {card.get('description')}}
); const thumbnailStyle = { visibility: revealed ? null : 'hidden', }; if (largeImage && card.get('type') === 'video') { thumbnailStyle.aspectRatio = `16 / 9`; } else if (largeImage) { thumbnailStyle.aspectRatio = '1.91 / 1'; } else { thumbnailStyle.aspectRatio = 1; } let embed; let canvas = ( ); const thumbnailDescription = card.get('image_description'); const thumbnail = {thumbnailDescription}; let spoilerButton = ( ); spoilerButton = (
{spoilerButton}
); if (interactive) { if (embedded) { embed = this.renderVideo(); } else { embed = (
{canvas} {thumbnail} {revealed ? (
) : spoilerButton}
); } return (
{embed} {description}
); } else if (card.get('image')) { embed = (
{canvas} {thumbnail}
); } else { embed = (
); } return ( {embed} {description} ); } }