mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-14 23:24:55 +01:00
658ac4396c
The rationale behind this is that if the user wants to report violent media, they might not want to see it repeatedly. The “sensitive” property is still kept, displaying different messages for hidden media depending on whether they are marked as sensitive.
307 lines
8.3 KiB
JavaScript
307 lines
8.3 KiB
JavaScript
import React from 'react';
|
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
import PropTypes from 'prop-types';
|
|
import { is } from 'immutable';
|
|
import IconButton from './icon_button';
|
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
import { isIOS } from 'flavours/glitch/util/is_mobile';
|
|
import classNames from 'classnames';
|
|
import { autoPlayGif, displaySensitiveMedia } from 'flavours/glitch/util/initial_state';
|
|
|
|
const messages = defineMessages({
|
|
hidden: {
|
|
defaultMessage: 'Media hidden',
|
|
id: 'status.media_hidden',
|
|
},
|
|
sensitive: {
|
|
defaultMessage: 'Sensitive',
|
|
id: 'media_gallery.sensitive',
|
|
},
|
|
toggle: {
|
|
defaultMessage: 'Click to view',
|
|
id: 'status.sensitive_toggle',
|
|
},
|
|
toggle_visible: {
|
|
defaultMessage: 'Toggle visibility',
|
|
id: 'media_gallery.toggle_visible',
|
|
},
|
|
warning: {
|
|
defaultMessage: 'Sensitive content',
|
|
id: 'status.sensitive_warning',
|
|
},
|
|
});
|
|
|
|
class Item extends React.PureComponent {
|
|
|
|
static propTypes = {
|
|
attachment: ImmutablePropTypes.map.isRequired,
|
|
standalone: PropTypes.bool,
|
|
index: PropTypes.number.isRequired,
|
|
size: PropTypes.number.isRequired,
|
|
letterbox: PropTypes.bool,
|
|
onClick: PropTypes.func.isRequired,
|
|
};
|
|
|
|
static defaultProps = {
|
|
standalone: false,
|
|
index: 0,
|
|
size: 1,
|
|
};
|
|
|
|
handleMouseEnter = (e) => {
|
|
if (this.hoverToPlay()) {
|
|
e.target.play();
|
|
}
|
|
}
|
|
|
|
handleMouseLeave = (e) => {
|
|
if (this.hoverToPlay()) {
|
|
e.target.pause();
|
|
e.target.currentTime = 0;
|
|
}
|
|
}
|
|
|
|
hoverToPlay () {
|
|
const { attachment } = this.props;
|
|
return !autoPlayGif && attachment.get('type') === 'gifv';
|
|
}
|
|
|
|
handleClick = (e) => {
|
|
const { index, onClick } = this.props;
|
|
|
|
if (e.button === 0) {
|
|
e.preventDefault();
|
|
onClick(index);
|
|
}
|
|
|
|
e.stopPropagation();
|
|
}
|
|
|
|
render () {
|
|
const { attachment, index, size, standalone, letterbox } = this.props;
|
|
|
|
let width = 50;
|
|
let height = 100;
|
|
let top = 'auto';
|
|
let left = 'auto';
|
|
let bottom = 'auto';
|
|
let right = 'auto';
|
|
|
|
if (size === 1) {
|
|
width = 100;
|
|
}
|
|
|
|
if (size === 4 || (size === 3 && index > 0)) {
|
|
height = 50;
|
|
}
|
|
|
|
if (size === 2) {
|
|
if (index === 0) {
|
|
right = '2px';
|
|
} else {
|
|
left = '2px';
|
|
}
|
|
} else if (size === 3) {
|
|
if (index === 0) {
|
|
right = '2px';
|
|
} else if (index > 0) {
|
|
left = '2px';
|
|
}
|
|
|
|
if (index === 1) {
|
|
bottom = '2px';
|
|
} else if (index > 1) {
|
|
top = '2px';
|
|
}
|
|
} else if (size === 4) {
|
|
if (index === 0 || index === 2) {
|
|
right = '2px';
|
|
}
|
|
|
|
if (index === 1 || index === 3) {
|
|
left = '2px';
|
|
}
|
|
|
|
if (index < 2) {
|
|
bottom = '2px';
|
|
} else {
|
|
top = '2px';
|
|
}
|
|
}
|
|
|
|
let thumbnail = '';
|
|
|
|
if (attachment.get('type') === 'image') {
|
|
const previewUrl = attachment.get('preview_url');
|
|
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
|
|
|
const originalUrl = attachment.get('url');
|
|
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
|
|
|
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
|
|
|
|
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
|
|
const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
|
|
|
|
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
|
|
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
|
|
const x = ((focusX / 2) + .5) * 100;
|
|
const y = ((focusY / -2) + .5) * 100;
|
|
|
|
thumbnail = (
|
|
<a
|
|
className='media-gallery__item-thumbnail'
|
|
href={attachment.get('remote_url') || originalUrl}
|
|
onClick={this.handleClick}
|
|
target='_blank'
|
|
>
|
|
<img
|
|
className={letterbox ? 'letterbox' : null}
|
|
src={previewUrl}
|
|
srcSet={srcSet}
|
|
sizes={sizes}
|
|
alt={attachment.get('description')}
|
|
title={attachment.get('description')}
|
|
style={{ objectPosition: `${x}% ${y}%` }} />
|
|
</a>
|
|
);
|
|
} else if (attachment.get('type') === 'gifv') {
|
|
const autoPlay = !isIOS() && autoPlayGif;
|
|
|
|
thumbnail = (
|
|
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
|
<video
|
|
className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
|
|
aria-label={attachment.get('description')}
|
|
role='application'
|
|
src={attachment.get('url')}
|
|
onClick={this.handleClick}
|
|
onMouseEnter={this.handleMouseEnter}
|
|
onMouseLeave={this.handleMouseLeave}
|
|
autoPlay={autoPlay}
|
|
loop
|
|
muted
|
|
/>
|
|
|
|
<span className='media-gallery__gifv__label'>GIF</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
|
{thumbnail}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
@injectIntl
|
|
export default class MediaGallery extends React.PureComponent {
|
|
|
|
static propTypes = {
|
|
sensitive: PropTypes.bool,
|
|
revealed: PropTypes.bool,
|
|
standalone: PropTypes.bool,
|
|
letterbox: PropTypes.bool,
|
|
fullwidth: PropTypes.bool,
|
|
media: ImmutablePropTypes.list.isRequired,
|
|
size: PropTypes.object,
|
|
onOpenMedia: PropTypes.func.isRequired,
|
|
intl: PropTypes.object.isRequired,
|
|
};
|
|
|
|
static defaultProps = {
|
|
standalone: false,
|
|
};
|
|
|
|
state = {
|
|
visible: this.props.revealed === undefined ? (!this.props.sensitive || displaySensitiveMedia) : this.props.revealed,
|
|
};
|
|
|
|
componentWillReceiveProps (nextProps) {
|
|
if (!is(nextProps.media, this.props.media)) {
|
|
this.setState({ visible: !nextProps.sensitive });
|
|
}
|
|
}
|
|
|
|
handleOpen = () => {
|
|
this.setState({ visible: !this.state.visible });
|
|
}
|
|
|
|
handleClick = (index) => {
|
|
this.props.onOpenMedia(this.props.media, index);
|
|
}
|
|
|
|
handleRef = (node) => {
|
|
if (node && this.isStandaloneEligible()) {
|
|
// offsetWidth triggers a layout, so only calculate when we need to
|
|
this.setState({
|
|
width: node.offsetWidth,
|
|
});
|
|
}
|
|
}
|
|
|
|
isStandaloneEligible() {
|
|
const { media, standalone } = this.props;
|
|
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
|
}
|
|
|
|
render () {
|
|
const { media, intl, sensitive, letterbox, fullwidth } = this.props;
|
|
const { width, visible } = this.state;
|
|
const size = media.take(4).size;
|
|
|
|
let children;
|
|
|
|
const style = {};
|
|
|
|
if (this.isStandaloneEligible() && width) {
|
|
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
|
|
}
|
|
|
|
if (!visible) {
|
|
let warning = <FormattedMessage {...(sensitive ? messages.warning : messages.hidden)} />;
|
|
|
|
children = (
|
|
<button className='media-spoiler' type='button' onClick={this.handleOpen}>
|
|
<span className='media-spoiler__warning'>{warning}</span>
|
|
<span className='media-spoiler__trigger'><FormattedMessage {...messages.toggle} /></span>
|
|
</button>
|
|
);
|
|
} else {
|
|
if (this.isStandaloneEligible()) {
|
|
children = <Item standalone attachment={media.get(0)} onClick={this.handleClick} />;
|
|
} else {
|
|
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} />);
|
|
}
|
|
}
|
|
|
|
const computedClass = classNames('media-gallery', `size-${size}`, { 'full-width': fullwidth });
|
|
|
|
return (
|
|
<div className={computedClass} style={style} ref={this.handleRef}>
|
|
{visible ? (
|
|
<div className='sensitive-info'>
|
|
<IconButton
|
|
icon='eye'
|
|
onClick={this.handleOpen}
|
|
overlay
|
|
title={intl.formatMessage(messages.toggle_visible)}
|
|
/>
|
|
{sensitive ? (
|
|
<span className='sensitive-marker'>
|
|
<FormattedMessage {...messages.sensitive} />
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
}
|