@@ -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 CharacterCounter from 'mastodon/features/compose/components/character_counter'; | ||||
import { length } from 'stringz'; | import { length } from 'stringz'; | ||||
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; | import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; | ||||
import GIFV from 'mastodon/components/gifv'; | |||||
const messages = defineMessages({ | const messages = defineMessages({ | ||||
close: { id: 'lightbox.close', defaultMessage: 'Close' }, | close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||
@@ -41,6 +42,36 @@ const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') | |||||
const assetHost = process.env.CDN_HOST || ''; | 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) | export default @connect(mapStateToProps, mapDispatchToProps) | ||||
@injectIntl | @injectIntl | ||||
class FocalPointModal extends ImmutablePureComponent { | class FocalPointModal extends ImmutablePureComponent { | ||||
@@ -60,6 +91,7 @@ class FocalPointModal extends ImmutablePureComponent { | |||||
description: '', | description: '', | ||||
dirty: false, | dirty: false, | ||||
progress: 0, | progress: 0, | ||||
loading: true, | |||||
}; | }; | ||||
componentWillMount () { | componentWillMount () { | ||||
@@ -242,8 +274,8 @@ class FocalPointModal extends ImmutablePureComponent { | |||||
<div className='focal-point-modal__content'> | <div className='focal-point-modal__content'> | ||||
{focals && ( | {focals && ( | ||||
<div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}> | <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'> | <div className='focal-point__preview'> | ||||
<strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong> | <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 ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
import Video from 'mastodon/features/video'; | import Video from 'mastodon/features/video'; | ||||
import ExtendedVideoPlayer from 'mastodon/components/extended_video_player'; | |||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
import IconButton from 'mastodon/components/icon_button'; | import IconButton from 'mastodon/components/icon_button'; | ||||
import ImmutablePureComponent from 'react-immutable-pure-component'; | import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
import ImageLoader from './image_loader'; | import ImageLoader from './image_loader'; | ||||
import Icon from 'mastodon/components/icon'; | import Icon from 'mastodon/components/icon'; | ||||
import GIFV from 'mastodon/components/gifv'; | |||||
const messages = defineMessages({ | const messages = defineMessages({ | ||||
close: { id: 'lightbox.close', defaultMessage: 'Close' }, | close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||
@@ -169,10 +169,8 @@ class MediaModal extends ImmutablePureComponent { | |||||
); | ); | ||||
} else if (image.get('type') === 'gifv') { | } else if (image.get('type') === 'gifv') { | ||||
return ( | return ( | ||||
<ExtendedVideoPlayer | |||||
<GIFV | |||||
src={image.get('url')} | src={image.get('url')} | ||||
muted | |||||
controls={false} | |||||
width={width} | width={width} | ||||
height={height} | height={height} | ||||
key={image.get('preview_url')} | key={image.get('preview_url')} | ||||
@@ -6092,7 +6092,8 @@ noscript { | |||||
background: $base-shadow-color; | background: $base-shadow-color; | ||||
img, | img, | ||||
video { | |||||
video, | |||||
canvas { | |||||
display: block; | display: block; | ||||
max-height: 80vh; | max-height: 80vh; | ||||
width: 100%; | width: 100%; | ||||