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
					
				| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user