Merge pull request #1531 from ClearlyClaire/glitch-soc/features/upstream-media-modal

Port upstream's new media modal
pull/1533/head
Claire 2021-05-08 00:37:51 +02:00 committed by GitHub
commit 5233e99106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 411 additions and 203 deletions

View File

@ -0,0 +1,112 @@
const DIGIT_CHARACTERS = [
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'#',
'$',
'%',
'*',
'+',
',',
'-',
'.',
':',
';',
'=',
'?',
'@',
'[',
']',
'^',
'_',
'{',
'|',
'}',
'~',
];
export const decode83 = (str) => {
let value = 0;
let c, digit;
for (let i = 0; i < str.length; i++) {
c = str[i];
digit = DIGIT_CHARACTERS.indexOf(c);
value = value * 83 + digit;
}
return value;
};
export const intToRGB = int => ({
r: Math.max(0, (int >> 16)),
g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, (int & 255)),
});
export const getAverageFromBlurhash = blurhash => {
if (!blurhash) {
return null;
}
return intToRGB(decode83(blurhash.slice(2, 6)));
};

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import 'wicg-inert'; import 'wicg-inert';
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history';
import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -11,6 +12,11 @@ export default class ModalRoot extends React.PureComponent {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
backgroundColor: PropTypes.shape({
r: PropTypes.number,
g: PropTypes.number,
b: PropTypes.number,
}),
noEsc: PropTypes.bool, noEsc: PropTypes.bool,
}; };
@ -68,9 +74,7 @@ export default class ModalRoot extends React.PureComponent {
Promise.resolve().then(() => { Promise.resolve().then(() => {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
this.activeElement = null; this.activeElement = null;
}).catch((error) => { }).catch(console.error);
console.error(error);
});
this.handleModalClose(); this.handleModalClose();
} }
@ -120,10 +124,16 @@ export default class ModalRoot extends React.PureComponent {
); );
} }
let backgroundColor = null;
if (this.props.backgroundColor) {
backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
}
return ( return (
<div className='modal-root' ref={this.setRef}> <div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} /> <div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} />
<div role='dialog' className='modal-root__container'>{children}</div> <div role='dialog' className='modal-root__container'>{children}</div>
</div> </div>
</div> </div>

View File

@ -378,22 +378,26 @@ class Status extends ImmutablePureComponent {
} }
}; };
handleOpenVideo = (media, options) => { handleOpenVideo = (options) => {
this.props.onOpenVideo(media, options); const { status } = this.props;
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options);
}
handleOpenMedia = (media, index) => {
this.props.onOpenMedia(this.props.status.get('id'), media, index);
} }
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
const { status, onOpenMedia, onOpenVideo } = this.props; const { status, onOpenMedia, onOpenVideo } = this.props;
const statusId = status.get('id');
e.preventDefault(); e.preventDefault();
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
// TODO: toggle play/paused? onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 });
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else { } else {
onOpenMedia(status.get('media_attachments'), 0); onOpenMedia(statusId, status.get('media_attachments'), 0);
} }
} }
} }
@ -657,7 +661,7 @@ class Status extends ImmutablePureComponent {
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])} fullwidth={settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded} hidden={isCollapsed || !isExpanded}
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia} visible={this.state.showMedia}
@ -675,7 +679,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('card') && settings.get('inline_preview_cards')) { } else if (status.get('card') && settings.get('inline_preview_cards')) {
media = ( media = (
<Card <Card
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.handleOpenMedia}
card={status.get('card')} card={status.get('card')}
compact compact
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}

View File

@ -2,7 +2,7 @@ import React, { PureComponent, Fragment } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { List as ImmutableList, fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { getLocale } from 'mastodon/locales'; import { getLocale } from 'mastodon/locales';
import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
import MediaGallery from 'flavours/glitch/components/media_gallery'; import MediaGallery from 'flavours/glitch/components/media_gallery';
@ -30,6 +30,8 @@ export default class MediaContainer extends PureComponent {
media: null, media: null,
index: null, index: null,
time: null, time: null,
backgroundColor: null,
options: null,
}; };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
@ -39,20 +41,32 @@ export default class MediaContainer extends PureComponent {
this.setState({ media, index }); this.setState({ media, index });
} }
handleOpenVideo = (video, time) => { handleOpenVideo = (options) => {
const media = ImmutableList([video]); const { components } = this.props;
const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props'));
const mediaList = fromJS(media);
document.body.classList.add('with-modals--active'); document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media, time }); this.setState({ media: mediaList, options });
} }
handleCloseMedia = () => { handleCloseMedia = () => {
document.body.classList.remove('with-modals--active'); document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = 0; document.documentElement.style.marginRight = 0;
this.setState({ media: null, index: null, time: null }); this.setState({
media: null,
index: null,
time: null,
backgroundColor: null,
options: null,
});
}
setBackgroundColor = color => {
this.setState({ backgroundColor: color });
} }
render () { render () {
@ -73,6 +87,7 @@ export default class MediaContainer extends PureComponent {
...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
...(componentName === 'Video' ? { ...(componentName === 'Video' ? {
componetIndex: i,
onOpenVideo: this.handleOpenVideo, onOpenVideo: this.handleOpenVideo,
} : { } : {
onOpenMedia: this.handleOpenMedia, onOpenMedia: this.handleOpenMedia,
@ -85,13 +100,16 @@ export default class MediaContainer extends PureComponent {
); );
})} })}
<ModalRoot onClose={this.handleCloseMedia}> <ModalRoot backgroundColor={this.state.backgroundColor} onClose={this.handleCloseMedia}>
{this.state.media && ( {this.state.media && (
<MediaModal <MediaModal
media={this.state.media} media={this.state.media}
index={this.state.index || 0} index={this.state.index || 0}
time={this.state.time} currentTime={this.state.options?.startTime}
autoPlay={this.state.options?.autoPlay}
volume={this.state.options?.defaultVolume}
onClose={this.handleCloseMedia} onClose={this.handleCloseMedia}
onChangeBackgroundColor={this.setBackgroundColor}
/> />
)} )}
</ModalRoot> </ModalRoot>

View File

@ -177,12 +177,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(mentionCompose(account, router)); dispatch(mentionCompose(account, router));
}, },
onOpenMedia (media, index) { onOpenMedia (statusId, media, index) {
dispatch(openModal('MEDIA', { media, index })); dispatch(openModal('MEDIA', { statusId, media, index }));
}, },
onOpenVideo (media, options) { onOpenVideo (statusId, media, options) {
dispatch(openModal('VIDEO', { media, options })); dispatch(openModal('VIDEO', { statusId, media, options }));
}, },
onBlock (status) { onBlock (status) {

View File

@ -114,15 +114,18 @@ class AccountGallery extends ImmutablePureComponent {
} }
handleOpenMedia = attachment => { handleOpenMedia = attachment => {
const { dispatch } = this.props;
const statusId = attachment.getIn(['status', 'id']);
if (attachment.get('type') === 'video') { if (attachment.get('type') === 'video') {
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } })); dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } }));
} else if (attachment.get('type') === 'audio') { } else if (attachment.get('type') === 'audio') {
this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } })); dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } }));
} else { } else {
const media = attachment.getIn(['status', 'media_attachments']); const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id')); const index = media.findIndex(x => x.get('id') === attachment.get('id'));
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') })); dispatch(openModal('MEDIA', { media, index, statusId }));
} }
} }

View File

@ -23,6 +23,7 @@ const messages = defineMessages({
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -52,11 +53,19 @@ class Footer extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
showReplyCount: PropTypes.bool, showReplyCount: PropTypes.bool,
withOpenButton: PropTypes.bool,
onClose: PropTypes.func,
}; };
_performReply = () => { _performReply = () => {
const { dispatch, status } = this.props; const { dispatch, status, onClose } = this.props;
dispatch(replyCompose(status, this.context.router.history)); const { router } = this.context;
if (onClose) {
onClose();
}
dispatch(replyCompose(status, router.history));
}; };
handleReplyClick = () => { handleReplyClick = () => {
@ -100,8 +109,20 @@ class Footer extends ImmutablePureComponent {
} }
}; };
handleOpenClick = e => {
const { router } = this.context;
if (e.button !== 0 || !router) {
return;
}
const { status } = this.props;
router.history.push(`/statuses/${status.get('id')}`);
}
render () { render () {
const { status, intl, showReplyCount } = this.props; const { status, intl, showReplyCount, withOpenButton } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
@ -156,6 +177,7 @@ class Footer extends ImmutablePureComponent {
{replyButton} {replyButton}
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} /> <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} />}
</div> </div>
); );
} }

View File

@ -68,8 +68,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
e.stopPropagation(); e.stopPropagation();
} }
handleOpenVideo = (media, options) => { handleOpenVideo = (options) => {
this.props.onOpenVideo(media, options); this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
} }
_measureHeight (heightJustChanged) { _measureHeight (heightJustChanged) {

View File

@ -316,11 +316,11 @@ class Status extends ImmutablePureComponent {
} }
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { media, index })); this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index }));
} }
handleOpenVideo = (media, options) => { handleOpenVideo = (media, options) => {
this.props.dispatch(openModal('VIDEO', { media, options })); this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options }));
} }
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
@ -329,9 +329,7 @@ class Status extends ImmutablePureComponent {
e.preventDefault(); e.preventDefault();
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 }); this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else { } else {
this.handleOpenMedia(status.get('media_attachments'), 0); this.handleOpenMedia(status.get('media_attachments'), 0);

View File

@ -4,12 +4,10 @@ import PropTypes from 'prop-types';
import Audio from 'flavours/glitch/features/audio'; import Audio from 'flavours/glitch/features/audio';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl'; import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
const mapStateToProps = (state, { status }) => ({ const mapStateToProps = (state, { statusId }) => ({
account: state.getIn(['accounts', status.get('account')]), accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -17,27 +15,21 @@ class AudioModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map, statusId: PropTypes.string.isRequired,
accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({ options: PropTypes.shape({
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
}), }),
account: ImmutablePropTypes.map,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
}; };
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
}; };
handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
}
render () { render () {
const { media, status, account } = this.props; const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -48,7 +40,7 @@ class AudioModal extends ImmutablePureComponent {
alt={media.get('description')} alt={media.get('description')}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150} height={150}
poster={media.get('preview_url') || account.get('avatar_static')} poster={media.get('preview_url') || accountStaticAvatar}
backgroundColor={media.getIn(['meta', 'colors', 'background'])} backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])} foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])} accentColor={media.getIn(['meta', 'colors', 'accent'])}
@ -56,11 +48,9 @@ class AudioModal extends ImmutablePureComponent {
/> />
</div> </div>
{status && ( <div className='media-modal__overlay'>
<div className={classNames('media-modal__meta')}> {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div> </div>
)}
</div> </div>
); );
} }

View File

@ -4,12 +4,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'flavours/glitch/features/video'; import Video from 'flavours/glitch/features/video';
import classNames from 'classnames'; import classNames from 'classnames';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button'; import IconButton from 'flavours/glitch/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 'flavours/glitch/components/icon'; import Icon from 'flavours/glitch/components/icon';
import GIFV from 'flavours/glitch/components/gifv'; import GIFV from 'flavours/glitch/components/gifv';
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -26,10 +28,14 @@ class MediaModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
status: ImmutablePropTypes.map, statusId: PropTypes.string,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
currentTime: PropTypes.number,
autoPlay: PropTypes.bool,
volume: PropTypes.number,
}; };
state = { state = {
@ -64,6 +70,7 @@ class MediaModal extends ImmutablePureComponent {
handleChangeIndex = (e) => { handleChangeIndex = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index')); const index = Number(e.currentTarget.getAttribute('data-index'));
this.setState({ this.setState({
index: index % this.props.media.size, index: index % this.props.media.size,
zoomButtonHidden: true, zoomButtonHidden: true,
@ -87,10 +94,12 @@ class MediaModal extends ImmutablePureComponent {
componentDidMount () { componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false); window.addEventListener('keydown', this.handleKeyDown, false);
this._sendBackgroundColor();
} }
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('keydown', this.handleKeyDown); window.removeEventListener('keydown', this.handleKeyDown);
this.props.onChangeBackgroundColor(null);
} }
getIndex () { getIndex () {
@ -106,30 +115,38 @@ class MediaModal extends ImmutablePureComponent {
handleStatusClick = e => { handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); this.context.router.history.push(`/statuses/${this.props.statusId}`);
}
this._sendBackgroundColor();
}
componentDidUpdate (prevProps, prevState) {
if (prevState.index !== this.state.index) {
this._sendBackgroundColor();
}
}
_sendBackgroundColor () {
const { media, onChangeBackgroundColor } = this.props;
const index = this.getIndex();
const blurhash = media.getIn([index, 'blurhash']);
if (blurhash) {
const backgroundColor = getAverageFromBlurhash(blurhash);
onChangeBackgroundColor(backgroundColor);
} }
} }
render () { render () {
const { media, status, intl, onClose } = this.props; const { media, statusId, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden } = this.state;
const index = this.getIndex(); const index = this.getIndex();
let pagination = [];
const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>; const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>; const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
if (media.size > 1) {
pagination = media.map((item, i) => {
const classes = ['media-modal__button'];
if (i === index) {
classes.push('media-modal__button--active');
}
return (<li className='media-modal__page-dot' key={i}><button tabIndex='0' className={classes.join(' ')} onClick={this.handleChangeIndex} data-index={i}>{i + 1}</button></li>);
});
}
const content = media.map((image) => { const content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null; const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null; const height = image.getIn(['meta', 'original', 'height']) || null;
@ -148,7 +165,7 @@ class MediaModal extends ImmutablePureComponent {
/> />
); );
} else if (image.get('type') === 'video') { } else if (image.get('type') === 'video') {
const { time } = this.props; const { currentTime, autoPlay, volume } = this.props;
return ( return (
<Video <Video
@ -157,7 +174,10 @@ class MediaModal extends ImmutablePureComponent {
src={image.get('url')} src={image.get('url')}
width={image.get('width')} width={image.get('width')}
height={image.get('height')} height={image.get('height')}
currentTime={time || 0} frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
currentTime={currentTime || 0}
autoPlay={autoPlay || false}
volume={volume || 1}
onCloseVideo={onClose} onCloseVideo={onClose}
detailed detailed
alt={image.get('description')} alt={image.get('description')}
@ -197,13 +217,19 @@ class MediaModal extends ImmutablePureComponent {
'media-modal__navigation--hidden': navigationHidden, 'media-modal__navigation--hidden': navigationHidden,
}); });
let pagination;
if (media.size > 1) {
pagination = media.map((item, i) => (
<button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
{i + 1}
</button>
));
}
return ( return (
<div className='modal-root__modal media-modal'> <div className='modal-root__modal media-modal'>
<div <div className='media-modal__closer' role='presentation' onClick={onClose} >
className='media-modal__closer'
role='presentation'
onClick={onClose}
>
<ReactSwipeableViews <ReactSwipeableViews
style={swipeableViewsStyle} style={swipeableViewsStyle}
containerStyle={containerStyle} containerStyle={containerStyle}
@ -221,15 +247,10 @@ class MediaModal extends ImmutablePureComponent {
{leftNav} {leftNav}
{rightNav} {rightNav}
{status && ( <div className='media-modal__overlay'>
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}> {pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
</div> </div>
)}
<ul className='media-modal__pagination'>
{pagination}
</ul>
</div> </div>
</div> </div>
); );

View File

@ -55,6 +55,10 @@ export default class ModalRoot extends React.PureComponent {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
state = {
backgroundColor: null,
};
getSnapshotBeforeUpdate () { getSnapshotBeforeUpdate () {
return { visible: !!this.props.type }; return { visible: !!this.props.type };
} }
@ -69,6 +73,10 @@ export default class ModalRoot extends React.PureComponent {
} }
} }
setBackgroundColor = color => {
this.setState({ backgroundColor: color });
}
renderLoading = modalId => () => { renderLoading = modalId => () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
} }
@ -81,13 +89,14 @@ export default class ModalRoot extends React.PureComponent {
render () { render () {
const { type, props, onClose } = this.props; const { type, props, onClose } = this.props;
const { backgroundColor } = this.state;
const visible = !!type; const visible = !!type;
return ( return (
<Base onClose={onClose} noEsc={props ? props.noEsc : false}> <Base backgroundColor={backgroundColor} onClose={onClose} noEsc={props ? props.noEsc : false}>
{visible && ( {visible && (
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={onClose} />}
</BundleContainer> </BundleContainer>
)} )}
</Base> </Base>

View File

@ -3,9 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'flavours/glitch/features/video'; import Video from 'flavours/glitch/features/video';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl'; import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
import classNames from 'classnames'; import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
import Icon from 'flavours/glitch/components/icon';
export default class VideoModal extends ImmutablePureComponent { export default class VideoModal extends ImmutablePureComponent {
@ -15,24 +14,28 @@ export default class VideoModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map, statusId: PropTypes.string,
options: PropTypes.shape({ options: PropTypes.shape({
startTime: PropTypes.number, startTime: PropTypes.number,
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
defaultVolume: PropTypes.number, defaultVolume: PropTypes.number,
}), }),
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
}; };
handleStatusClick = e => { componentDidMount () {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { const { media, onChangeBackgroundColor, onClose } = this.props;
e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
if (backgroundColor) {
onChangeBackgroundColor(backgroundColor);
} }
} }
render () { render () {
const { media, status, onClose } = this.props; const { media, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -52,11 +55,9 @@ export default class VideoModal extends ImmutablePureComponent {
/> />
</div> </div>
{status && ( <div className='media-modal__overlay'>
<div className={classNames('media-modal__meta')}> {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div> </div>
)}
</div> </div>
); );
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS, is } from 'immutable'; import { is } from 'immutable';
import { throttle, debounce } from 'lodash'; import { throttle, debounce } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen';
@ -120,10 +120,10 @@ class Video extends React.PureComponent {
deployPictureInPicture: PropTypes.func, deployPictureInPicture: PropTypes.func,
preventPlayback: PropTypes.bool, preventPlayback: PropTypes.bool,
blurhash: PropTypes.string, blurhash: PropTypes.string,
link: PropTypes.node,
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
volume: PropTypes.number, volume: PropTypes.number,
muted: PropTypes.bool, muted: PropTypes.bool,
componetIndex: PropTypes.number,
}; };
static defaultProps = { static defaultProps = {
@ -510,25 +510,14 @@ class Video extends React.PureComponent {
} }
handleOpenVideo = () => { handleOpenVideo = () => {
const { src, preview, width, height, alt } = this.props; this.video.pause();
const media = fromJS({ this.props.onOpenVideo({
type: 'video',
url: src,
preview_url: preview,
description: alt,
width,
height,
});
const options = {
startTime: this.video.currentTime, startTime: this.video.currentTime,
autoPlay: !this.state.paused, autoPlay: !this.state.paused,
defaultVolume: this.state.volume, defaultVolume: this.state.volume,
}; componetIndex: this.props.componetIndex,
});
this.video.pause();
this.props.onOpenVideo(media, options);
} }
handleCloseVideo = () => { handleCloseVideo = () => {
@ -548,7 +537,7 @@ class Video extends React.PureComponent {
} }
render () { render () {
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link, editable, blurhash } = this.props; const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
const playerStyle = {}; const playerStyle = {};
@ -666,8 +655,6 @@ class Video extends React.PureComponent {
<span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span> <span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span>
</span> </span>
)} )}
{link && <span className='video-player__link'>{link}</span>}
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>

File diff suppressed because one or more lines are too long

View File

@ -334,11 +334,11 @@
} }
} }
.star-icon.active { .icon-button.star-icon.active {
color: $gold-star; color: $gold-star;
} }
.bookmark-icon.active { .icon-button.bookmark-icon.active {
color: $red-bookmark; color: $red-bookmark;
} }

View File

@ -187,16 +187,19 @@
height: 100%; height: 100%;
position: relative; position: relative;
.extended-video-player { &__close,
width: 100%; &__zoom-button {
height: 100%; color: rgba($white, 0.7);
display: flex;
align-items: center;
justify-content: center;
video { &:hover,
max-width: $media-modal-media-max-width; &:focus,
max-height: $media-modal-media-max-height; &:active {
color: $white;
background-color: rgba($white, 0.15);
}
&:focus {
background-color: rgba($white, 0.3);
} }
} }
} }
@ -233,10 +236,10 @@
} }
.media-modal__nav { .media-modal__nav {
background: rgba($base-overlay-background, 0.5); background: transparent;
box-sizing: border-box; box-sizing: border-box;
border: 0; border: 0;
color: $primary-text-color; color: rgba($primary-text-color, 0.7);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@ -247,6 +250,12 @@
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
&:hover,
&:focus,
&:active {
color: $primary-text-color;
}
} }
.media-modal__nav--left { .media-modal__nav--left {
@ -257,58 +266,86 @@
right: 0; right: 0;
} }
.media-modal__pagination { .media-modal__overlay {
width: 100%; max-width: 600px;
text-align: center;
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 20px; right: 0;
pointer-events: none; bottom: 0;
} margin: 0 auto;
.media-modal__meta { .picture-in-picture__footer {
text-align: center; border-radius: 0;
position: absolute; background: transparent;
left: 0; padding: 20px 0;
bottom: 20px;
width: 100%;
pointer-events: none;
&--shifted { .icon-button {
bottom: 62px; color: $white;
}
a {
pointer-events: auto;
text-decoration: none;
font-weight: 500;
color: $ui-secondary-color;
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
text-decoration: underline; color: $white;
background-color: rgba($white, 0.15);
}
&:focus {
background-color: rgba($white, 0.3);
}
&.active {
color: $highlight-text-color;
&:hover,
&:focus,
&:active {
background: rgba($highlight-text-color, 0.15);
}
&:focus {
background: rgba($highlight-text-color, 0.3);
} }
} }
&.star-icon.active {
color: $gold-star;
&:hover,
&:focus,
&:active {
background: rgba($gold-star, 0.15);
}
&:focus {
background: rgba($gold-star, 0.3);
}
}
}
}
}
.media-modal__pagination {
display: flex;
justify-content: center;
margin-bottom: 20px;
} }
.media-modal__page-dot { .media-modal__page-dot {
display: inline-block; flex: 0 0 auto;
}
.media-modal__button {
background-color: $white; background-color: $white;
height: 12px; opacity: 0.4;
width: 12px; height: 6px;
border-radius: 6px; width: 6px;
margin: 10px; border-radius: 50%;
margin: 0 4px;
padding: 0; padding: 0;
border: 0; border: 0;
font-size: 0; font-size: 0;
} transition: opacity .2s ease-in-out;
.media-modal__button--active { &.active {
background-color: $ui-highlight-color; opacity: 1;
}
} }
.media-modal__close { .media-modal__close {

View File

@ -14,6 +14,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba($base-overlay-background, 0.7); background: rgba($base-overlay-background, 0.7);
transition: background 0.5s;
} }
.modal-root__container { .modal-root__container {

View File

@ -1122,21 +1122,6 @@ a.status-card.compact:hover {
.audio-player { .audio-player {
border-radius: 0; border-radius: 0;
} }
@media screen and (max-width: 415px) {
width: 210px;
bottom: 10px;
right: 10px;
&__footer {
display: none;
}
.video-player,
.audio-player {
border-radius: 0 0 4px 4px;
}
}
} }
.picture-in-picture-placeholder { .picture-in-picture-placeholder {