Hide sensitive preview cards with blurhash (#13985)
* Use preview card blurhash in WebUI * Handle sensitive preview cards
This commit is contained in:
		
							parent
							
								
									a3f22bd4ca
								
							
						
					
					
						commit
						8e96510b25
					
				
					 6 changed files with 105 additions and 13 deletions
				
			
		| 
						 | 
				
			
			@ -401,6 +401,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
          compact
 | 
			
		||||
          cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
          defaultWidth={this.props.cachedMediaWidth}
 | 
			
		||||
          sensitive={status.get('sensitive')}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,9 +2,13 @@ import React from 'react';
 | 
			
		|||
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--';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +67,7 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
    compact: PropTypes.bool,
 | 
			
		||||
    defaultWidth: PropTypes.number,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
    sensitive: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,12 +77,44 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +156,18 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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')) };
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +187,7 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { card, maxDescription, compact } = this.props;
 | 
			
		||||
    const { width, embedded } = this.state;
 | 
			
		||||
    const { width, embedded, revealed } = this.state;
 | 
			
		||||
 | 
			
		||||
    if (card === null) {
 | 
			
		||||
      return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +202,7 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
    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>
 | 
			
		||||
| 
						 | 
				
			
			@ -161,7 +210,18 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
    );
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -175,14 +235,18 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
        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>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -196,13 +260,16 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
    } 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>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -153,7 +153,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
 | 
			
		|||
        );
 | 
			
		||||
      }
 | 
			
		||||
    } else if (status.get('spoiler_text').length === 0) {
 | 
			
		||||
      media = <Card onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
 | 
			
		||||
      media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (status.get('application')) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3097,6 +3097,11 @@ a.status-card {
 | 
			
		|||
  flex: 1 1 auto;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding: 14px 14px 14px 8px;
 | 
			
		||||
 | 
			
		||||
  &--blurred {
 | 
			
		||||
    filter: blur(2px);
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-card__description {
 | 
			
		||||
| 
						 | 
				
			
			@ -3134,7 +3139,8 @@ a.status-card {
 | 
			
		|||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .status-card__image-image {
 | 
			
		||||
  .status-card__image-image,
 | 
			
		||||
  .status-card__image-preview {
 | 
			
		||||
    border-radius: 4px 4px 0 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3179,6 +3185,24 @@ a.status-card.compact:hover {
 | 
			
		|||
  background-position: center center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-card__image-preview {
 | 
			
		||||
  border-radius: 4px 0 0 4px;
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: fill;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  z-index: 0;
 | 
			
		||||
  background: $base-overlay-background;
 | 
			
		||||
 | 
			
		||||
  &--hidden {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.load-more {
 | 
			
		||||
  display: block;
 | 
			
		||||
  color: $dark-text-color;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,7 @@
 | 
			
		|||
      = react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, 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
 | 
			
		||||
 | 
			
		||||
  .detailed-status__meta
 | 
			
		||||
    %data.dt-published{ value: status.created_at.to_time.iso8601 }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@
 | 
			
		|||
      = 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue