42917806e9
removed unnecessary components, combined where necessary added each component to a folder, added individual css style modules optimized some component rendering flows removed functional components in favor of pure components linted and formatted all of the files
333 lines
9.8 KiB
JavaScript
333 lines
9.8 KiB
JavaScript
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
import { is } from 'immutable';
|
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
import classNames from 'classnames';
|
|
import { decode } from 'blurhash';
|
|
import IconButton from '../icon_button';
|
|
import { isIOS } from '../../utils/is_mobile';
|
|
import { autoPlayGif, displayMedia } from '../../initial_state';
|
|
|
|
import './index.scss';
|
|
|
|
const messages = defineMessages({
|
|
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
|
});
|
|
|
|
class Item extends PureComponent {
|
|
|
|
static propTypes = {
|
|
attachment: ImmutablePropTypes.map.isRequired,
|
|
standalone: PropTypes.bool,
|
|
index: PropTypes.number.isRequired,
|
|
size: PropTypes.number.isRequired,
|
|
onClick: PropTypes.func.isRequired,
|
|
displayWidth: PropTypes.number,
|
|
visible: PropTypes.bool.isRequired,
|
|
};
|
|
|
|
static defaultProps = {
|
|
standalone: false,
|
|
index: 0,
|
|
size: 1,
|
|
};
|
|
|
|
state = {
|
|
loaded: false,
|
|
};
|
|
|
|
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.ctrlKey || e.metaKey)) {
|
|
if (this.hoverToPlay()) {
|
|
e.target.pause();
|
|
e.target.currentTime = 0;
|
|
}
|
|
|
|
e.preventDefault();
|
|
onClick(index);
|
|
}
|
|
|
|
e.stopPropagation();
|
|
}
|
|
|
|
componentDidMount () {
|
|
if (this.props.attachment.get('blurhash')) {
|
|
this._decode();
|
|
}
|
|
}
|
|
|
|
componentDidUpdate (prevProps) {
|
|
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
|
|
this._decode();
|
|
}
|
|
}
|
|
|
|
_decode () {
|
|
const hash = this.props.attachment.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({ loaded: true });
|
|
}
|
|
|
|
render () {
|
|
const { attachment, index, size, standalone, displayWidth, visible } = this.props;
|
|
|
|
const width = (size === 1) ? 100 : 50;
|
|
const height = (size === 4 || (size === 3 && index > 0)) ? 50 : 100;
|
|
|
|
let top = 'auto';
|
|
let left = 'auto';
|
|
let bottom = 'auto';
|
|
let right = 'auto';
|
|
|
|
switch(size) {
|
|
case 2:
|
|
if (index === 0) right = '2px';
|
|
else left = '2px';
|
|
break;
|
|
case 3:
|
|
if (index === 0) right = '2px';
|
|
else if (index > 0) left = '2px';
|
|
|
|
if (index === 1) bottom = '2px';
|
|
else if (index > 1) top = '2px';
|
|
break;
|
|
case 4:
|
|
if (index === 0 || index === 2) right = '2px';
|
|
if (index === 1 || index === 3) left = '2px';
|
|
|
|
if (index < 2) bottom = '2px';
|
|
else top = '2px';
|
|
break;
|
|
}
|
|
|
|
let thumbnail = '';
|
|
|
|
if (attachment.get('type') === 'unknown') {
|
|
return (
|
|
<div className={classNames('media-item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
|
<a className='media-item__thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
|
|
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
|
</a>
|
|
</div>
|
|
);
|
|
} else 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 && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : 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-item__thumbnail'
|
|
href={attachment.get('remote_url') || originalUrl}
|
|
onClick={this.handleClick}
|
|
target='_blank'
|
|
>
|
|
<img
|
|
src={previewUrl}
|
|
srcSet={srcSet}
|
|
sizes={sizes}
|
|
alt={attachment.get('description')}
|
|
title={attachment.get('description')}
|
|
style={{ objectPosition: `${x}% ${y}%` }}
|
|
onLoad={this.handleImageLoad}
|
|
/>
|
|
</a>
|
|
);
|
|
} else if (attachment.get('type') === 'gifv') {
|
|
const autoPlay = !isIOS() && autoPlayGif;
|
|
|
|
thumbnail = (
|
|
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
|
<video
|
|
className='media-item__gifv'
|
|
aria-label={attachment.get('description')}
|
|
title={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-item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
|
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
|
|
{visible && thumbnail}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
export default @injectIntl
|
|
class MediaGallery extends PureComponent {
|
|
|
|
static propTypes = {
|
|
sensitive: PropTypes.bool,
|
|
standalone: PropTypes.bool,
|
|
media: ImmutablePropTypes.list.isRequired,
|
|
size: PropTypes.object,
|
|
height: PropTypes.number.isRequired,
|
|
onOpenMedia: PropTypes.func.isRequired,
|
|
intl: PropTypes.object.isRequired,
|
|
defaultWidth: PropTypes.number,
|
|
cacheWidth: PropTypes.func,
|
|
visible: PropTypes.bool,
|
|
onToggleVisibility: PropTypes.func,
|
|
};
|
|
|
|
static defaultProps = {
|
|
standalone: false,
|
|
};
|
|
|
|
state = {
|
|
visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
|
width: this.props.defaultWidth,
|
|
};
|
|
|
|
componentWillReceiveProps (nextProps) {
|
|
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
|
|
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
|
|
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
|
this.setState({ visible: nextProps.visible });
|
|
}
|
|
}
|
|
|
|
handleOpen = () => {
|
|
if (this.props.onToggleVisibility) {
|
|
this.props.onToggleVisibility();
|
|
} else {
|
|
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
|
|
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
|
|
|
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, height, defaultWidth } = this.props;
|
|
const { visible } = this.state;
|
|
|
|
const width = this.state.width || defaultWidth;
|
|
|
|
let children, spoilerButton;
|
|
|
|
const style = {};
|
|
|
|
if (this.isStandaloneEligible()) {
|
|
if (width) {
|
|
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
|
|
}
|
|
} else if (width) {
|
|
style.height = width / (16/9);
|
|
} else {
|
|
style.height = height;
|
|
}
|
|
|
|
const size = media.take(4).size;
|
|
|
|
if (this.isStandaloneEligible()) {
|
|
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
|
} else {
|
|
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />);
|
|
}
|
|
|
|
if (visible) {
|
|
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
|
} else {
|
|
spoilerButton = (
|
|
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
|
<span className='spoiler-button__overlay__label'>
|
|
{
|
|
sensitive
|
|
? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />
|
|
: <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />
|
|
}
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className='media-gallery' style={style} ref={this.handleRef}>
|
|
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
|
|
{spoilerButton}
|
|
</div>
|
|
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
}
|