import PropTypes from 'prop-types';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
import punycode from 'punycode';
import classnames from 'classnames';
import Icon from 'mastodon/components/icon';
+import classNames from 'classnames';
+import { useBlurhash } from 'mastodon/initial_state';
+import { decode } from 'blurhash';
const IDNA_PREFIX = 'xn--';
compact: PropTypes.bool,
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
+ sensitive: PropTypes.bool,
};
static defaultProps = {
state = {
width: this.props.defaultWidth || 280,
+ previewLoaded: false,
embedded: false,
+ revealed: !this.props.sensitive,
};
componentWillReceiveProps (nextProps) {
if (!Immutable.is(this.props.card, nextProps.card)) {
- this.setState({ embedded: false });
+ this.setState({ embedded: false, previewLoaded: false });
+ }
+ if (this.props.sensitive !== nextProps.sensitive) {
+ this.setState({ revealed: !nextProps.sensitive });
+ }
+ }
+
+ componentDidMount () {
+ if (this.props.card && this.props.card.get('blurhash')) {
+ this._decode();
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ const { card } = this.props;
+ if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash'))) {
+ this._decode();
+ }
+ }
+
+ _decode () {
+ if (!useBlurhash) return;
+
+ const hash = this.props.card.get('blurhash');
+ const pixels = decode(hash, 32, 32);
+
+ if (pixels) {
+ const ctx = this.canvas.getContext('2d');
+ const imageData = new ImageData(pixels, 32, 32);
+
+ ctx.putImageData(imageData, 0, 0);
}
}
}
}
+ setCanvasRef = c => {
+ this.canvas = c;
+ }
+
+ handleImageLoad = () => {
+ this.setState({ previewLoaded: true });
+ }
+
+ handleReveal = () => {
+ this.setState({ revealed: true });
+ }
+
renderVideo () {
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };
render () {
const { card, maxDescription, compact } = this.props;
- const { width, embedded } = this.state;
+ const { width, embedded, revealed } = this.state;
if (card === null) {
return null;
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
const description = (
- <div className='status-card__content'>
+ <div className={classNames('status-card__content', { 'status-card__content--blurred': !revealed })}>
{title}
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
<span className='status-card__host'>{provider}</span>
);
let embed = '';
- let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
+ let canvas = <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('status-card__image-preview', { 'status-card__image-preview--hidden' : revealed && this.state.previewLoaded })} />;
+ let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
+ let spoilerButton = (
+ <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
+ <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
+ </button>
+ );
+ spoilerButton = (
+ <div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
+ {spoilerButton}
+ </div>
+ );
if (interactive) {
if (embedded) {
embed = (
<div className='status-card__image'>
+ {canvas}
{thumbnail}
- <div className='status-card__actions'>
- <div>
- <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
- {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
+ {revealed && (
+ <div className='status-card__actions'>
+ <div>
+ <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
+ {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
+ </div>
</div>
- </div>
+ )}
+ {!revealed && spoilerButton}
</div>
);
}
} else if (card.get('image')) {
embed = (
<div className='status-card__image'>
+ {canvas}
{thumbnail}
+ {!revealed && spoilerButton}
</div>
);
} else {
embed = (
<div className='status-card__image'>
<Icon id='file-text' />
+ {!revealed && spoilerButton}
</div>
);
}
= react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.preview_card
- = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
+ = react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
- if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do