Change media modals look in web UI (#15217)
- Change overlay background to match color of viewed image - Add interactive reply/boost/favourite buttons to footer of modal - Change ugly "View context" link to button among the action bargh/stable
parent
cb7bd8ee03
commit
1e89e2ed98
|
@ -1,12 +1,18 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import 'wicg-inert';
|
||||
import { normal } from 'color-blend';
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
backgroundColor: PropTypes.shape({
|
||||
r: PropTypes.number,
|
||||
g: PropTypes.number,
|
||||
b: PropTypes.number,
|
||||
}),
|
||||
};
|
||||
|
||||
activeElement = this.props.children ? document.activeElement : null;
|
||||
|
@ -62,9 +68,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||
Promise.resolve().then(() => {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
this.activeElement = null;
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,10 +95,16 @@ export default class ModalRoot extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
let backgroundColor = null;
|
||||
|
||||
if (this.props.backgroundColor) {
|
||||
backgroundColor = normal({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.3 });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='modal-root' ref={this.setRef}>
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
@ -193,22 +193,24 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleOpenVideo = (media, options) => {
|
||||
this.props.onOpenVideo(media, options);
|
||||
this.props.onOpenVideo(this._properStatus().get('id'), media, options);
|
||||
}
|
||||
|
||||
handleOpenMedia = (media, index) => {
|
||||
this.props.onOpenMedia(this._properStatus().get('id'), media, index);
|
||||
}
|
||||
|
||||
handleHotkeyOpenMedia = e => {
|
||||
const { onOpenMedia, onOpenVideo } = this.props;
|
||||
const status = this._properStatus();
|
||||
const statusId = this._properStatus().get('id');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
// TODO: toggle play/paused?
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 });
|
||||
} else {
|
||||
onOpenMedia(status.get('media_attachments'), 0);
|
||||
onOpenMedia(statusId, status.get('media_attachments'), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -416,7 +418,7 @@ class Status extends ImmutablePureComponent {
|
|||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
height={110}
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
|
|
|
@ -152,12 +152,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
|
||||
onOpenMedia (media, index) {
|
||||
dispatch(openModal('MEDIA', { media, index }));
|
||||
onOpenMedia (statusId, media, index) {
|
||||
dispatch(openModal('MEDIA', { statusId, media, index }));
|
||||
},
|
||||
|
||||
onOpenVideo (media, options) {
|
||||
dispatch(openModal('VIDEO', { media, options }));
|
||||
onOpenVideo (statusId, media, options) {
|
||||
dispatch(openModal('VIDEO', { statusId, media, options }));
|
||||
},
|
||||
|
||||
onBlock (status) {
|
||||
|
|
|
@ -105,15 +105,18 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleOpenMedia = attachment => {
|
||||
const { dispatch } = this.props;
|
||||
const statusId = attachment.getIn(['status', 'id']);
|
||||
|
||||
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') {
|
||||
this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
|
||||
dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } }));
|
||||
} 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') }));
|
||||
dispatch(openModal('MEDIA', { media, index, statusId }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ const messages = defineMessages({
|
|||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
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?' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -49,11 +50,19 @@ class Footer extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
askReplyConfirmation: PropTypes.bool,
|
||||
withOpenButton: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
_performReply = () => {
|
||||
const { dispatch, status } = this.props;
|
||||
dispatch(replyCompose(status, this.context.router.history));
|
||||
const { dispatch, status, onClose } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
dispatch(replyCompose(status, router.history));
|
||||
};
|
||||
|
||||
handleReplyClick = () => {
|
||||
|
@ -97,8 +106,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 () {
|
||||
const { status, intl } = this.props;
|
||||
const { status, intl, withOpenButton } = this.props;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
||||
|
@ -130,6 +151,7 @@ class Footer extends ImmutablePureComponent {
|
|||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
<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')} />
|
||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -276,22 +276,20 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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) => {
|
||||
this.props.dispatch(openModal('VIDEO', { media, options }));
|
||||
this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options }));
|
||||
}
|
||||
|
||||
handleHotkeyOpenMedia = e => {
|
||||
const status = this._properStatus();
|
||||
const { status } = this.props;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
// TODO: toggle play/paused?
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
|
||||
} else {
|
||||
this.handleOpenMedia(status.get('media_attachments'), 0);
|
||||
|
|
|
@ -4,13 +4,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import PropTypes from 'prop-types';
|
||||
import Video from 'mastodon/features/video';
|
||||
import classNames from 'classnames';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } 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';
|
||||
import { disableSwiping } from 'mastodon/initial_state';
|
||||
import Footer from 'mastodon/features/picture_in_picture/components/footer';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
|
@ -20,15 +21,121 @@ const messages = defineMessages({
|
|||
|
||||
export const previewState = 'previewMediaModal';
|
||||
|
||||
const digitCharacters = [
|
||||
'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',
|
||||
'#',
|
||||
'$',
|
||||
'%',
|
||||
'*',
|
||||
'+',
|
||||
',',
|
||||
'-',
|
||||
'.',
|
||||
':',
|
||||
';',
|
||||
'=',
|
||||
'?',
|
||||
'@',
|
||||
'[',
|
||||
']',
|
||||
'^',
|
||||
'_',
|
||||
'{',
|
||||
'|',
|
||||
'}',
|
||||
'~',
|
||||
];
|
||||
|
||||
const decode83 = (str) => {
|
||||
let value = 0;
|
||||
let c, digit;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
c = str[i];
|
||||
digit = digitCharacters.indexOf(c);
|
||||
value = value * 83 + digit;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const decodeRGB = int => ({
|
||||
r: Math.max(0, (int >> 16)),
|
||||
g: Math.max(0, (int >> 8) & 255),
|
||||
b: Math.max(0, (int & 255)),
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
statusId: PropTypes.string,
|
||||
index: PropTypes.number.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChangeBackgroundColor: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -67,6 +174,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
|
||||
handleChangeIndex = (e) => {
|
||||
const index = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
this.setState({
|
||||
index: index % this.props.media.size,
|
||||
zoomButtonHidden: true,
|
||||
|
@ -100,6 +208,22 @@ class MediaModal extends ImmutablePureComponent {
|
|||
this.props.onClose();
|
||||
});
|
||||
}
|
||||
|
||||
this._sendBackgroundColor();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevState.index !== this.state.index) {
|
||||
this._sendBackgroundColor();
|
||||
}
|
||||
}
|
||||
|
||||
_sendBackgroundColor () {
|
||||
const { media, onChangeBackgroundColor } = this.props;
|
||||
const index = this.getIndex();
|
||||
const backgroundColor = decodeRGB(decode83(media.getIn([index, 'blurhash']).slice(2, 6)));
|
||||
|
||||
onChangeBackgroundColor(backgroundColor);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -112,6 +236,8 @@ class MediaModal extends ImmutablePureComponent {
|
|||
this.context.router.history.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onChangeBackgroundColor(null);
|
||||
}
|
||||
|
||||
getIndex () {
|
||||
|
@ -127,30 +253,19 @@ 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')}`);
|
||||
this.context.router.history.push(`/statuses/${this.props.statusId}`);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, status, intl, onClose } = this.props;
|
||||
const { media, statusId, intl, onClose } = this.props;
|
||||
const { navigationHidden } = this.state;
|
||||
|
||||
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 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 width = image.getIn(['meta', 'original', 'width']) || null;
|
||||
const height = image.getIn(['meta', 'original', 'height']) || null;
|
||||
|
@ -218,13 +333,19 @@ class MediaModal extends ImmutablePureComponent {
|
|||
'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 (
|
||||
<div className='modal-root__modal media-modal'>
|
||||
<div
|
||||
className='media-modal__closer'
|
||||
role='presentation'
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className='media-modal__closer' role='presentation' onClick={onClose} >
|
||||
<ReactSwipeableViews
|
||||
style={swipeableViewsStyle}
|
||||
containerStyle={containerStyle}
|
||||
|
@ -243,15 +364,10 @@ class MediaModal extends ImmutablePureComponent {
|
|||
{leftNav}
|
||||
{rightNav}
|
||||
|
||||
{status && (
|
||||
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
<div className='media-modal__overlay'>
|
||||
{pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
|
||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul className='media-modal__pagination'>
|
||||
{pagination}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -45,6 +45,10 @@ export default class ModalRoot extends React.PureComponent {
|
|||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
backgroundColor: null,
|
||||
};
|
||||
|
||||
getSnapshotBeforeUpdate () {
|
||||
return { visible: !!this.props.type };
|
||||
}
|
||||
|
@ -59,6 +63,10 @@ export default class ModalRoot extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
setBackgroundColor = color => {
|
||||
this.setState({ backgroundColor: color });
|
||||
}
|
||||
|
||||
renderLoading = modalId => () => {
|
||||
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
|
||||
}
|
||||
|
@ -71,13 +79,14 @@ export default class ModalRoot extends React.PureComponent {
|
|||
|
||||
render () {
|
||||
const { type, props, onClose } = this.props;
|
||||
const { backgroundColor } = this.state;
|
||||
const visible = !!type;
|
||||
|
||||
return (
|
||||
<Base onClose={onClose}>
|
||||
<Base backgroundColor={backgroundColor} onClose={onClose}>
|
||||
{visible && (
|
||||
<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>
|
||||
)}
|
||||
</Base>
|
||||
|
|
|
@ -3,9 +3,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import PropTypes from 'prop-types';
|
||||
import Video from 'mastodon/features/video';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
export const previewState = 'previewVideoModal';
|
||||
|
||||
|
@ -13,7 +10,7 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
statusId: PropTypes.string,
|
||||
options: PropTypes.shape({
|
||||
startTime: PropTypes.number,
|
||||
autoPlay: PropTypes.bool,
|
||||
|
@ -48,15 +45,8 @@ export default class VideoModal 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, status, onClose } = this.props;
|
||||
const { media, onClose } = this.props;
|
||||
const options = this.props.options || {};
|
||||
|
||||
return (
|
||||
|
@ -75,12 +65,6 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||
alt={media.get('description')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<div className={classNames('media-modal__meta')}>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -118,7 +118,6 @@ class Video extends React.PureComponent {
|
|||
deployPictureInPicture: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
blurhash: PropTypes.string,
|
||||
link: PropTypes.node,
|
||||
autoPlay: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
|
@ -534,7 +533,7 @@ class Video extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props;
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const playerStyle = {};
|
||||
|
@ -648,8 +647,6 @@ class Video extends React.PureComponent {
|
|||
<span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{link && <span className='video-player__link'>{link}</span>}
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1652,11 +1652,11 @@ a.account__display-name {
|
|||
}
|
||||
}
|
||||
|
||||
.star-icon.active {
|
||||
.icon-button.star-icon.active {
|
||||
color: $gold-star;
|
||||
}
|
||||
|
||||
.bookmark-icon.active {
|
||||
.icon-button.bookmark-icon.active {
|
||||
color: $red-bookmark;
|
||||
}
|
||||
|
||||
|
@ -3007,7 +3007,6 @@ button.icon-button i.fa-retweet {
|
|||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
button.icon-button.active i.fa-retweet {
|
||||
|
@ -4487,16 +4486,19 @@ a.status-card.compact:hover {
|
|||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.extended-video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&__close,
|
||||
&__zoom-button {
|
||||
color: rgba($white, 0.7);
|
||||
|
||||
video {
|
||||
max-width: $media-modal-media-max-width;
|
||||
max-height: $media-modal-media-max-height;
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: $white;
|
||||
background-color: rgba($white, 0.15);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: rgba($white, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4533,10 +4535,10 @@ a.status-card.compact:hover {
|
|||
}
|
||||
|
||||
.media-modal__nav {
|
||||
background: rgba($base-overlay-background, 0.5);
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
color: $primary-text-color;
|
||||
color: rgba($primary-text-color, 0.7);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -4547,6 +4549,12 @@ a.status-card.compact:hover {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.media-modal__nav--left {
|
||||
|
@ -4557,58 +4565,86 @@ a.status-card.compact:hover {
|
|||
right: 0;
|
||||
}
|
||||
|
||||
.media-modal__pagination {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
.media-modal__overlay {
|
||||
max-width: 600px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 20px;
|
||||
pointer-events: none;
|
||||
}
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0 auto;
|
||||
|
||||
.media-modal__meta {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
.picture-in-picture__footer {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 20px 0;
|
||||
|
||||
&--shifted {
|
||||
bottom: 62px;
|
||||
}
|
||||
|
||||
a {
|
||||
pointer-events: auto;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
color: $ui-secondary-color;
|
||||
.icon-button {
|
||||
color: $white;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&: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 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.media-modal__button {
|
||||
background-color: $primary-text-color;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 6px;
|
||||
margin: 10px;
|
||||
flex: 0 0 auto;
|
||||
background-color: $white;
|
||||
opacity: 0.4;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
margin: 0 4px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
transition: opacity .2s ease-in-out;
|
||||
|
||||
.media-modal__button--active {
|
||||
background-color: $highlight-text-color;
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.media-modal__close {
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
"babel-runtime": "^6.26.0",
|
||||
"blurhash": "^1.1.3",
|
||||
"classnames": "^2.2.5",
|
||||
"color-blend": "^3.0.0",
|
||||
"compression-webpack-plugin": "^6.1.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^5.0.1",
|
||||
|
|
|
@ -2941,6 +2941,11 @@ collection-visit@^1.0.0:
|
|||
map-visit "^1.0.0"
|
||||
object-visit "^1.0.0"
|
||||
|
||||
color-blend@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/color-blend/-/color-blend-3.0.0.tgz#077073ee59ebce15e084f00590c5bf7577899cb5"
|
||||
integrity sha512-m21ytRyjsIkVOGG1jrrpijhx7icji0MljlxUoa0ER7lgGW11as0GPLrXQQuMULH1BWJ7OsR11Dy2S6A5lehg5A==
|
||||
|
||||
color-convert@^1.9.0, color-convert@^1.9.1:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
|
Reference in New Issue