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 bar
This commit is contained in:
		
							parent
							
								
									cb7bd8ee03
								
							
						
					
					
						commit
						1e89e2ed98
					
				
					 14 changed files with 339 additions and 146 deletions
				
			
		| 
						 | 
				
			
			@ -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>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <ul className='media-modal__pagination'>
 | 
			
		||||
            {pagination}
 | 
			
		||||
          </ul>
 | 
			
		||||
          <div className='media-modal__overlay'>
 | 
			
		||||
            {pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
 | 
			
		||||
            {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
 | 
			
		||||
          </div>
 | 
			
		||||
        </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;
 | 
			
		||||
  }
 | 
			
		||||
    .icon-button {
 | 
			
		||||
      color: $white;
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    pointer-events: auto;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    color: $ui-secondary-color;
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:focus,
 | 
			
		||||
      &:active {
 | 
			
		||||
        color: $white;
 | 
			
		||||
        background-color: rgba($white, 0.15);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    &:hover,
 | 
			
		||||
    &:focus,
 | 
			
		||||
    &:active {
 | 
			
		||||
      text-decoration: underline;
 | 
			
		||||
      &: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__page-dot {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
.media-modal__pagination {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.media-modal__button {
 | 
			
		||||
  background-color: $primary-text-color;
 | 
			
		||||
  height: 12px;
 | 
			
		||||
  width: 12px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  margin: 10px;
 | 
			
		||||
.media-modal__page-dot {
 | 
			
		||||
  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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue