diff options
author | ThibG <thib@sitedethib.com> | 2019-05-05 20:53:07 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-05-05 20:53:07 +0200 |
commit | e4ca5414a37b7973247a283a364b27052c8f9d1c (patch) | |
tree | dcb7769320c5a35b229ef579a20aa2fff209ea94 /app/javascript/flavours/glitch/features | |
parent | 2c2f649200ba5b742ba9d17ac5ba553c752b59aa (diff) | |
parent | eef8802325d75b2668b131a765c5555f4be8c2ce (diff) |
Merge pull request #1032 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app/javascript/flavours/glitch/features')
11 files changed, 313 insertions, 130 deletions
diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 89778e123..026136b2c 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -1,69 +1,148 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Permalink from 'flavours/glitch/components/permalink'; -import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; +import classNames from 'classnames'; +import { decode } from 'blurhash'; +import { isIOS } from 'flavours/glitch/util/is_mobile'; export default class MediaItem extends ImmutablePureComponent { static propTypes = { - media: ImmutablePropTypes.map.isRequired, + attachment: ImmutablePropTypes.map.isRequired, + displayWidth: PropTypes.number.isRequired, + onOpenMedia: PropTypes.func.isRequired, }; state = { - visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all', + visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all', + loaded: false, }; - handleClick = () => { - if (!this.state.visible) { - this.setState({ visible: true }); - return true; + componentDidMount () { + if (this.props.attachment.get('blurhash')) { + this._decode(); } + } - return false; + componentDidUpdate (prevProps) { + if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) { + this._decode(); + } } - render () { - const { media } = this.props; - const { visible } = this.state; - const status = media.get('status'); - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const style = {}; - - let label, icon, title; - - if (media.get('type') === 'gifv') { - label = <span className='media-gallery__gifv__label'>GIF</span>; + _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 }); + } + + handleMouseEnter = e => { + if (this.hoverToPlay()) { + e.target.play(); } + } + + handleMouseLeave = e => { + if (this.hoverToPlay()) { + e.target.pause(); + e.target.currentTime = 0; + } + } - if (visible) { - style.backgroundImage = `url(${media.get('preview_url')})`; - style.backgroundPosition = `${x}% ${y}%`; - title = media.get('description'); - } else { - icon = ( - <span className='account-gallery__item__icons'> - <i className='fa fa-eye-slash' /> - </span> + hoverToPlay () { + return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1; + } + + handleClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + + if (this.state.visible) { + this.props.onOpenMedia(this.props.attachment); + } else { + this.setState({ visible: true }); + } + } + } + + render () { + const { attachment, displayWidth } = this.props; + const { visible, loaded } = this.state; + + const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; + const height = width; + const status = attachment.get('status'); + const title = status.get('spoiler_text') || attachment.get('description'); + + let thumbnail = ''; + + if (attachment.get('type') === 'unknown') { + // Skip + } else if (attachment.get('type') === 'image') { + 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 = ( + <img + src={attachment.get('preview_url')} + alt={attachment.get('description')} + title={attachment.get('description')} + style={{ objectPosition: `${x}% ${y}%` }} + onLoad={this.handleImageLoad} + /> + ); + } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) { + const autoPlay = !isIOS() && autoPlayGif; + + thumbnail = ( + <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> + <video + className='media-gallery__item-gifv-thumbnail' + aria-label={attachment.get('description')} + title={attachment.get('description')} + role='application' + src={attachment.get('url')} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + autoPlay={autoPlay} + loop + muted + /> + <span className='media-gallery__gifv__label'>GIF</span> + </div> ); - title = status.get('spoiler_text') || media.get('description'); } + const icon = ( + <span className='account-gallery__item__icons'> + <i className='fa fa-eye-slash' /> + </span> + ); + return ( - <div className='account-gallery__item'> - <Permalink - to={`/statuses/${status.get('id')}`} - href={status.get('url')} - style={style} - title={title} - onInterceptClick={this.handleClick} - > - {icon} - {label} - </Permalink> + <div className='account-gallery__item' style={{ width, height }}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}> + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> + {visible ? thumbnail : icon} + </a> </div> ); } diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 3b1af108f..3e4421306 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -14,12 +14,13 @@ import HeaderContainer from 'flavours/glitch/features/account_timeline/container import { ScrollContainer } from 'react-router-scroll-4'; import LoadMore from 'flavours/glitch/components/load_more'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import { openModal } from 'flavours/glitch/actions/modal'; const mapStateToProps = (state, props) => ({ isAccount: !!state.getIn(['accounts', props.params.accountId]), - medias: getAccountGallery(state, props.params.accountId), + attachments: getAccountGallery(state, props.params.accountId), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), - hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), + hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), }); class LoadMoreMedia extends ImmutablePureComponent { @@ -50,12 +51,16 @@ export default class AccountGallery extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, - medias: ImmutablePropTypes.list.isRequired, + attachments: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, }; + state = { + width: 323, + }; + componentDidMount () { this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); @@ -74,11 +79,11 @@ export default class AccountGallery extends ImmutablePureComponent { handleScrollToBottom = () => { if (this.props.hasMore) { - this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined); + this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined); } } - handleScroll = (e) => { + handleScroll = e => { const { scrollTop, scrollHeight, clientHeight } = e.target; const offset = scrollHeight - scrollTop - clientHeight; @@ -91,7 +96,7 @@ export default class AccountGallery extends ImmutablePureComponent { this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); }; - handleLoadOlder = (e) => { + handleLoadOlder = e => { e.preventDefault(); this.handleScrollToBottom(); } @@ -101,12 +106,30 @@ export default class AccountGallery extends ImmutablePureComponent { return !(location.state && location.state.mastodonModalOpen); } - setRef = c => { + setColumnRef = c => { this.column = c; } + handleOpenMedia = attachment => { + if (attachment.get('type') === 'video') { + this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') })); + } else { + const media = attachment.getIn(['status', 'media_attachments']); + const index = media.findIndex(x => x.get('id') === attachment.get('id')); + + this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') })); + } + } + + handleRef = c => { + if (c) { + this.setState({ width: c.offsetWidth }); + } + } + render () { - const { medias, isLoading, hasMore, isAccount } = this.props; + const { attachments, isLoading, hasMore, isAccount } = this.props; + const { width } = this.state; if (!isAccount) { return ( @@ -116,9 +139,7 @@ export default class AccountGallery extends ImmutablePureComponent { ); } - let loadOlder = null; - - if (!medias && isLoading) { + if (!attachments && isLoading) { return ( <Column> <LoadingIndicator /> @@ -126,35 +147,31 @@ export default class AccountGallery extends ImmutablePureComponent { ); } - if (hasMore && !(isLoading && medias.size === 0)) { + let loadOlder = null; + + if (hasMore && !(isLoading && attachments.size === 0)) { loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />; } return ( - <Column ref={this.setRef}> + <Column ref={this.setColumnRef}> <ProfileColumnHeader onClick={this.handleHeaderClick} /> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> <HeaderContainer accountId={this.props.params.accountId} /> - <div role='feed' className='account-gallery__container'> - {medias.map((media, index) => media === null ? ( - <LoadMoreMedia - key={'more:' + medias.getIn(index + 1, 'id')} - maxId={index > 0 ? medias.getIn(index - 1, 'id') : null} - onLoadMore={this.handleLoadMore} - /> + <div role='feed' className='account-gallery__container' ref={this.handleRef}> + {attachments.map((attachment, index) => attachment === null ? ( + <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> ) : ( - <MediaItem - key={media.get('id')} - media={media} - /> + <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} /> ))} + {loadOlder} </div> - {isLoading && medias.size === 0 && ( + {isLoading && attachments.size === 0 && ( <div className='scrollable__append'> <LoadingIndicator /> </div> diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 8a760bd15..ee9730961 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -65,10 +65,6 @@ const messages = defineMessages({ defaultMessage: 'Public', id: 'privacy.public.short', }, - sensitive: { - defaultMessage: 'Mark media as sensitive', - id: 'compose_form.sensitive', - }, spoiler: { defaultMessage: 'Hide text behind warning', id: 'compose_form.spoiler', @@ -116,7 +112,6 @@ class ComposerOptions extends ImmutablePureComponent { hasPoll: PropTypes.bool, intl: PropTypes.object.isRequired, onChangeAdvancedOption: PropTypes.func, - onChangeSensitivity: PropTypes.func, onChangeVisibility: PropTypes.func, onTogglePoll: PropTypes.func, onDoodleOpen: PropTypes.func, @@ -126,7 +121,6 @@ class ComposerOptions extends ImmutablePureComponent { onUpload: PropTypes.func, privacy: PropTypes.string, resetFileKey: PropTypes.number, - sensitive: PropTypes.bool, spoiler: PropTypes.bool, }; @@ -175,7 +169,6 @@ class ComposerOptions extends ImmutablePureComponent { hasPoll, intl, onChangeAdvancedOption, - onChangeSensitivity, onChangeVisibility, onTogglePoll, onModalClose, @@ -183,7 +176,6 @@ class ComposerOptions extends ImmutablePureComponent { onToggleSpoiler, privacy, resetFileKey, - sensitive, spoiler, } = this.props; @@ -264,39 +256,6 @@ class ComposerOptions extends ImmutablePureComponent { title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} /> )} - <Motion - defaultStyle={{ scale: 0.87 }} - style={{ - scale: spring(hasMedia ? 1 : 0.87, { - stiffness: 200, - damping: 3, - }), - }} - > - {({ scale }) => ( - <div - style={{ - display: hasMedia ? null : 'none', - transform: `scale(${scale})`, - }} - > - <IconButton - active={sensitive} - className='sensitive' - disabled={spoiler} - icon={sensitive ? 'eye-slash' : 'eye'} - inverted - onClick={onChangeSensitivity} - size={18} - style={{ - height: null, - lineHeight: null, - }} - title={intl.formatMessage(messages.sensitive)} - /> - </div> - )} - </Motion> <hr /> <Dropdown disabled={disabled} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js index 4864043a8..35880ddcc 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js @@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; +import SensitiveButtonContainer from '../containers/sensitive_button_container'; export default class UploadForm extends ImmutablePureComponent { static propTypes = { @@ -23,6 +24,8 @@ export default class UploadForm extends ImmutablePureComponent { ))} </div> )} + + {!mediaIds.isEmpty() && <SensitiveButtonContainer />} </div> ); } diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index e846cfbd5..2ac7ab8d8 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -2,7 +2,6 @@ import { connect } from 'react-redux'; import Options from '../components/options'; import { changeComposeAdvancedOption, - changeComposeSensitivity, } from 'flavours/glitch/actions/compose'; import { addPoll, removePoll } from 'flavours/glitch/actions/compose'; import { closeModal, openModal } from 'flavours/glitch/actions/modal'; @@ -27,10 +26,6 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeComposeAdvancedOption(option, value)); }, - onChangeSensitivity() { - dispatch(changeComposeSensitivity()); - }, - onTogglePoll() { dispatch((_, getState) => { if (getState().getIn(['compose', 'poll'])) { diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js new file mode 100644 index 000000000..8f163979f --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { changeComposeSensitivity } from 'flavours/glitch/actions/compose'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; + +const messages = defineMessages({ + marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, + unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' }, +}); + +const mapStateToProps = state => { + const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); + const spoilerText = state.getIn(['compose', 'spoiler_text']); + return { + active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0), + disabled: state.getIn(['compose', 'spoiler']), + }; +}; + +const mapDispatchToProps = dispatch => ({ + + onClick () { + dispatch(changeComposeSensitivity()); + }, + +}); + +class SensitiveButton extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + disabled: PropTypes.bool, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + render () { + const { active, disabled, onClick, intl } = this.props; + + return ( + <div className='compose-form__sensitive-button'> + <button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <Icon icon='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> + </button> + </div> + ); + } + +} + +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js index d674eecf9..cc49042fc 100644 --- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js +++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js @@ -35,6 +35,7 @@ export default class StatusCheckBox extends React.PureComponent { {Component => ( <Component preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} width={239} diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 8b35de0d4..0b162ed36 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -134,6 +134,7 @@ export default class DetailedStatus extends ImmutablePureComponent { media = ( <Video preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} inline diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index 1f3ac18ea..ce6660480 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; import ExtendedVideoPlayer from 'flavours/glitch/components/extended_video_player'; import classNames from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from 'flavours/glitch/components/icon_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImageLoader from './image_loader'; @@ -19,8 +19,13 @@ const messages = defineMessages({ @injectIntl export default class MediaModal extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { media: ImmutablePropTypes.list.isRequired, + status: ImmutablePropTypes.map, index: PropTypes.number.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -81,8 +86,15 @@ export default class MediaModal extends ImmutablePureComponent { })); }; + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, intl, onClose } = this.props; + const { media, status, intl, onClose } = this.props; const { navigationHidden } = this.state; const index = this.getIndex(); @@ -123,6 +135,7 @@ export default class MediaModal extends ImmutablePureComponent { return ( <Video preview={image.get('preview_url')} + blurhash={image.get('blurhash')} src={image.get('url')} width={image.get('width')} height={image.get('height')} @@ -185,10 +198,19 @@ export default class MediaModal extends ImmutablePureComponent { {content} </ReactSwipeableViews> </div> + <div className={navigationClassName}> <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} /> + {leftNav} {rightNav} + + {status && ( + <div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}> + <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> + </div> + )} + <ul className='media-modal__pagination'> {pagination} </ul> diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index 69e0ee46e..3f742c260 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -3,26 +3,43 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; export default class VideoModal extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { media: ImmutablePropTypes.map.isRequired, + status: ImmutablePropTypes.map, time: PropTypes.number, onClose: PropTypes.func.isRequired, }; + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, time, onClose } = this.props; + const { media, status, time, onClose } = this.props; + + const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>; return ( <div className='modal-root__modal video-modal'> <div> <Video preview={media.get('preview_url')} + blurhash={media.get('blurhash')} src={media.get('url')} startTime={time} onCloseVideo={onClose} + link={link} detailed alt={media.get('description')} /> diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index e3ed799c7..8291ff3c8 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -6,6 +6,7 @@ import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen'; import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { decode } from 'blurhash'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -104,6 +105,8 @@ export default class Video extends React.PureComponent { preventPlayback: PropTypes.bool, intl: PropTypes.object.isRequired, cacheWidth: PropTypes.func, + blurhash: PropTypes.string, + link: PropTypes.node, }; state = { @@ -147,6 +150,7 @@ export default class Video extends React.PureComponent { setVideoRef = c => { this.video = c; + if (this.video) { this.setState({ volume: this.video.volume, muted: this.video.muted }); } @@ -160,6 +164,10 @@ export default class Video extends React.PureComponent { this.volume = c; } + setCanvasRef = c => { + this.canvas = c; + } + handleMouseDownRoot = e => { e.preventDefault(); e.stopPropagation(); @@ -181,7 +189,6 @@ export default class Video extends React.PureComponent { } handleVolumeMouseDown = e => { - document.addEventListener('mousemove', this.handleMouseVolSlide, true); document.addEventListener('mouseup', this.handleVolumeMouseUp, true); document.addEventListener('touchmove', this.handleMouseVolSlide, true); @@ -201,7 +208,6 @@ export default class Video extends React.PureComponent { } handleMouseVolSlide = throttle(e => { - const rect = this.volume.getBoundingClientRect(); const x = (e.clientX - rect.left) / this.volWidth; //x position within the element. @@ -272,6 +278,10 @@ export default class Video extends React.PureComponent { document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); + + if (this.props.blurhash) { + this._decode(); + } } componentWillUnmount () { @@ -293,6 +303,24 @@ export default class Video extends React.PureComponent { } } + componentDidUpdate (prevProps) { + if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) { + this._decode(); + } + } + + _decode () { + const hash = this.props.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); + } + } + handleFullscreenChange = () => { this.setState({ fullscreen: isFullscreen() }); } @@ -337,6 +365,7 @@ export default class Video extends React.PureComponent { handleOpenVideo = () => { const { src, preview, width, height, alt } = this.props; + const media = fromJS({ type: 'video', url: src, @@ -356,7 +385,7 @@ export default class Video extends React.PureComponent { } render () { - const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive } = this.props; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; const playerStyle = {}; @@ -385,6 +414,7 @@ export default class Video extends React.PureComponent { } let preload; + if (startTime || fullscreen || dragging) { preload = 'auto'; } else if (detailed) { @@ -403,7 +433,9 @@ export default class Video extends React.PureComponent { onMouseDown={this.handleMouseDownRoot} tabIndex={0} > - <video + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} /> + + {revealed && <video ref={this.setVideoRef} src={src} poster={preview} @@ -423,12 +455,13 @@ export default class Video extends React.PureComponent { onLoadedData={this.handleLoadedData} onProgress={this.handleProgress} onVolumeChange={this.handleVolumeChange} - /> + />} - <button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> - <span className='video-player__spoiler__title'>{warning}</span> - <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </button> + <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}> + <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> + <span className='spoiler-button__overlay__label'>{warning}</span> + </button> + </div> <div className={classNames('video-player__controls', { active: paused || hovered })}> <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> @@ -455,17 +488,19 @@ export default class Video extends React.PureComponent { /> </div> - {(detailed || fullscreen) && + {(detailed || fullscreen) && ( <span> <span className='video-player__time-current'>{formatTime(currentTime)}</span> <span className='video-player__time-sep'>/</span> <span className='video-player__time-total'>{formatTime(duration)}</span> </span> - } + )} + + {link && <span className='video-player__link'>{link}</span>} </div> <div className='video-player__buttons right'> - {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} + {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye-slash' /></button>} {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>} <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> |