@@ -1,63 +0,0 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
export default class ExtendedVideoPlayer extends React.PureComponent { | |||
static propTypes = { | |||
src: PropTypes.string.isRequired, | |||
alt: PropTypes.string, | |||
width: PropTypes.number, | |||
height: PropTypes.number, | |||
time: PropTypes.number, | |||
controls: PropTypes.bool.isRequired, | |||
muted: PropTypes.bool.isRequired, | |||
onClick: PropTypes.func, | |||
}; | |||
handleLoadedData = () => { | |||
if (this.props.time) { | |||
this.video.currentTime = this.props.time; | |||
} | |||
} | |||
componentDidMount () { | |||
this.video.addEventListener('loadeddata', this.handleLoadedData); | |||
} | |||
componentWillUnmount () { | |||
this.video.removeEventListener('loadeddata', this.handleLoadedData); | |||
} | |||
setRef = (c) => { | |||
this.video = c; | |||
} | |||
handleClick = e => { | |||
e.stopPropagation(); | |||
const handler = this.props.onClick; | |||
if (handler) handler(); | |||
} | |||
render () { | |||
const { src, muted, controls, alt } = this.props; | |||
return ( | |||
<div className='extended-video-player'> | |||
<video | |||
ref={this.setRef} | |||
src={src} | |||
autoPlay | |||
role='button' | |||
tabIndex='0' | |||
aria-label={alt} | |||
title={alt} | |||
muted={muted} | |||
controls={controls} | |||
loop={!controls} | |||
onClick={this.handleClick} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,75 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
export default class GIFV extends React.PureComponent { | |||
static propTypes = { | |||
src: PropTypes.string.isRequired, | |||
alt: PropTypes.string, | |||
width: PropTypes.number, | |||
height: PropTypes.number, | |||
onClick: PropTypes.func, | |||
}; | |||
state = { | |||
loading: true, | |||
}; | |||
handleLoadedData = () => { | |||
this.setState({ loading: false }); | |||
} | |||
componentWillReceiveProps (nextProps) { | |||
if (nextProps.src !== this.props.src) { | |||
this.setState({ loading: true }); | |||
} | |||
} | |||
handleClick = e => { | |||
const { onClick } = this.props; | |||
if (onClick) { | |||
e.stopPropagation(); | |||
onClick(); | |||
} | |||
} | |||
render () { | |||
const { src, width, height, alt } = this.props; | |||
const { loading } = this.state; | |||
return ( | |||
<div className='gifv' style={{ position: 'relative' }}> | |||
{loading && ( | |||
<canvas | |||
width={width} | |||
height={height} | |||
role='button' | |||
tabIndex='0' | |||
aria-label={alt} | |||
title={alt} | |||
onClick={this.handleClick} | |||
/> | |||
)} | |||
<video | |||
src={src} | |||
width={width} | |||
height={height} | |||
role='button' | |||
tabIndex='0' | |||
aria-label={alt} | |||
title={alt} | |||
muted | |||
loop | |||
autoPlay | |||
playsInline | |||
onClick={this.handleClick} | |||
onLoadedData={this.handleLoadedData} | |||
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -16,6 +16,7 @@ import UploadProgress from 'mastodon/features/compose/components/upload_progress | |||
import CharacterCounter from 'mastodon/features/compose/components/character_counter'; | |||
import { length } from 'stringz'; | |||
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; | |||
import GIFV from 'mastodon/components/gifv'; | |||
const messages = defineMessages({ | |||
close: { id: 'lightbox.close', defaultMessage: 'Close' }, | |||
@@ -41,6 +42,36 @@ const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') | |||
const assetHost = process.env.CDN_HOST || ''; | |||
class ImageLoader extends React.PureComponent { | |||
static propTypes = { | |||
src: PropTypes.string.isRequired, | |||
width: PropTypes.number, | |||
height: PropTypes.number, | |||
}; | |||
state = { | |||
loading: true, | |||
}; | |||
componentDidMount() { | |||
const image = new Image(); | |||
image.addEventListener('load', () => this.setState({ loading: false })); | |||
image.src = this.props.src; | |||
} | |||
render () { | |||
const { loading } = this.state; | |||
if (loading) { | |||
return <canvas width={this.props.width} height={this.props.height} />; | |||
} else { | |||
return <img {...this.props} alt='' />; | |||
} | |||
} | |||
} | |||
export default @connect(mapStateToProps, mapDispatchToProps) | |||
@injectIntl | |||
class FocalPointModal extends ImmutablePureComponent { | |||
@@ -60,6 +91,7 @@ class FocalPointModal extends ImmutablePureComponent { | |||
description: '', | |||
dirty: false, | |||
progress: 0, | |||
loading: true, | |||
}; | |||
componentWillMount () { | |||
@@ -242,8 +274,8 @@ class FocalPointModal extends ImmutablePureComponent { | |||
<div className='focal-point-modal__content'> | |||
{focals && ( | |||
<div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}> | |||
{media.get('type') === 'image' && <img src={media.get('url')} width={width} height={height} alt='' />} | |||
{media.get('type') === 'gifv' && <video src={media.get('url')} width={width} height={height} loop muted autoPlay />} | |||
{media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />} | |||
{media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />} | |||
<div className='focal-point__preview'> | |||
<strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong> | |||
@@ -3,13 +3,13 @@ import ReactSwipeableViews from 'react-swipeable-views'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import Video from 'mastodon/features/video'; | |||
import ExtendedVideoPlayer from 'mastodon/components/extended_video_player'; | |||
import classNames from 'classnames'; | |||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | |||
import IconButton from 'mastodon/components/icon_button'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import ImageLoader from './image_loader'; | |||
import Icon from 'mastodon/components/icon'; | |||
import GIFV from 'mastodon/components/gifv'; | |||
const messages = defineMessages({ | |||
close: { id: 'lightbox.close', defaultMessage: 'Close' }, | |||
@@ -169,10 +169,8 @@ class MediaModal extends ImmutablePureComponent { | |||
); | |||
} else if (image.get('type') === 'gifv') { | |||
return ( | |||
<ExtendedVideoPlayer | |||
<GIFV | |||
src={image.get('url')} | |||
muted | |||
controls={false} | |||
width={width} | |||
height={height} | |||
key={image.get('preview_url')} | |||
@@ -6092,7 +6092,8 @@ noscript { | |||
background: $base-shadow-color; | |||
img, | |||
video { | |||
video, | |||
canvas { | |||
display: block; | |||
max-height: 80vh; | |||
width: 100%; | |||