[Glitch] Hide sensitive preview cards with blurhash

Port 8e96510b25 to glitch-soc

Signed-off-by: Thibaut Girka <thib@sitedethib.com>
This commit is contained in:
ThibG 2020-06-06 17:41:56 +02:00 committed by Thibaut Girka
parent 2da2f7f7c2
commit 776aff9656
4 changed files with 103 additions and 11 deletions

View File

@ -656,6 +656,7 @@ class Status extends ImmutablePureComponent {
compact compact
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')}
/> />
); );
mediaIcon = 'link'; mediaIcon = 'link';

View File

@ -2,10 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Immutable from 'immutable'; import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import punycode from 'punycode'; import punycode from 'punycode';
import classnames from 'classnames'; import classnames from 'classnames';
import { decode as decodeIDNA } from 'flavours/glitch/util/idna'; import { decode as decodeIDNA } from 'flavours/glitch/util/idna';
import Icon from 'flavours/glitch/components/icon'; import Icon from 'flavours/glitch/components/icon';
import classNames from 'classnames';
import { useBlurhash } from 'flavours/glitch/util/initial_state';
import { decode } from 'blurhash';
const getHostname = url => { const getHostname = url => {
const parser = document.createElement('a'); const parser = document.createElement('a');
@ -55,6 +59,7 @@ export default class Card extends React.PureComponent {
compact: PropTypes.bool, compact: PropTypes.bool,
defaultWidth: PropTypes.number, defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
sensitive: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -64,12 +69,44 @@ export default class Card extends React.PureComponent {
state = { state = {
width: this.props.defaultWidth || 280, width: this.props.defaultWidth || 280,
previewLoaded: false,
embedded: false, embedded: false,
revealed: !this.props.sensitive,
}; };
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (!Immutable.is(this.props.card, nextProps.card)) { 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);
} }
} }
@ -111,6 +148,18 @@ export default class Card extends React.PureComponent {
} }
} }
setCanvasRef = c => {
this.canvas = c;
}
handleImageLoad = () => {
this.setState({ previewLoaded: true });
}
handleReveal = () => {
this.setState({ revealed: true });
}
renderVideo () { renderVideo () {
const { card } = this.props; const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) }; const content = { __html: addAutoPlay(card.get('html')) };
@ -130,7 +179,7 @@ export default class Card extends React.PureComponent {
render () { render () {
const { card, maxDescription, compact, defaultWidth } = this.props; const { card, maxDescription, compact, defaultWidth } = this.props;
const { width, embedded } = this.state; const { width, embedded, revealed } = this.state;
if (card === null) { if (card === null) {
return null; return null;
@ -145,7 +194,7 @@ export default class Card extends React.PureComponent {
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
const description = ( const description = (
<div className='status-card__content'> <div className={classNames('status-card__content', { 'status-card__content--blurred': !revealed })}>
{title} {title}
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} {!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
<span className='status-card__host'>{provider}</span> <span className='status-card__host'>{provider}</span>
@ -153,7 +202,18 @@ export default class Card extends React.PureComponent {
); );
let embed = ''; 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 (interactive) {
if (embedded) { if (embedded) {
@ -167,14 +227,18 @@ export default class Card extends React.PureComponent {
embed = ( embed = (
<div className='status-card__image'> <div className='status-card__image'>
{canvas}
{thumbnail} {thumbnail}
{revealed && (
<div className='status-card__actions'> <div className='status-card__actions'>
<div> <div>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>} {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
</div> </div>
</div> </div>
)}
{!revealed && spoilerButton}
</div> </div>
); );
} }
@ -188,13 +252,16 @@ export default class Card extends React.PureComponent {
} else if (card.get('image')) { } else if (card.get('image')) {
embed = ( embed = (
<div className='status-card__image'> <div className='status-card__image'>
{canvas}
{thumbnail} {thumbnail}
{!revealed && spoilerButton}
</div> </div>
); );
} else { } else {
embed = ( embed = (
<div className='status-card__image'> <div className='status-card__image'>
<Icon id='file-text' /> <Icon id='file-text' />
{!revealed && spoilerButton}
</div> </div>
); );
} }

View File

@ -184,7 +184,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
mediaIcon = 'picture-o'; mediaIcon = 'picture-o';
} }
} else if (status.get('card')) { } else if (status.get('card')) {
media = <Card onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />; media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />;
mediaIcon = 'link'; mediaIcon = 'link';
} }

View File

@ -874,6 +874,11 @@ a.status-card {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
padding: 14px 14px 14px 8px; padding: 14px 14px 14px 8px;
&--blurred {
filter: blur(2px);
pointer-events: none;
}
} }
.status-card__description { .status-card__description {
@ -911,7 +916,8 @@ a.status-card {
width: 100%; width: 100%;
} }
.status-card__image-image { .status-card__image-image,
.status-card__image-preview {
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
} }
@ -956,6 +962,24 @@ a.status-card.compact:hover {
background-position: center center; 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;
}
}
.attachment-list { .attachment-list {
display: flex; display: flex;
font-size: 14px; font-size: 14px;