~cytrogen/masto-fe

598e63dad262ba454deda410f56c8f1b49ed96f8 — Claire 2 years ago 1eb51bd
Change media elements to use aspect-ratio rather than compute height themselves (#24686)

M app/javascript/mastodon/components/media_gallery.jsx => app/javascript/mastodon/components/media_gallery.jsx +3 -7
@@ 313,7 313,7 @@ class MediaGallery extends React.PureComponent {
  }

  render () {
    const { media, lang, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
    const { media, lang, intl, sensitive, defaultWidth, standalone, autoplay } = this.props;
    const { visible } = this.state;
    const width = this.state.width || defaultWidth;



@@ 322,13 322,9 @@ class MediaGallery extends React.PureComponent {
    const style = {};

    if (this.isFullSizeEligible() && (standalone || !cropImages)) {
      if (width) {
        style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
      }
    } else if (width) {
      style.height = width / (16/9);
      style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
    } else {
      style.height = height;
      style.aspectRatio = '16 / 9';
    }

    const size     = media.take(4).size;

M app/javascript/mastodon/components/picture_in_picture_placeholder.jsx => app/javascript/mastodon/components/picture_in_picture_placeholder.jsx +1 -41
@@ 3,62 3,22 @@ import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { FormattedMessage } from 'react-intl';

class PictureInPicturePlaceholder extends React.PureComponent {

  static propTypes = {
    width: PropTypes.number,
    dispatch: PropTypes.func.isRequired,
  };

  state = {
    width: this.props.width,
    height: this.props.width && (this.props.width / (16/9)),
  };

  handleClick = () => {
    const { dispatch } = this.props;
    dispatch(removePictureInPicture());
  };

  setRef = c => {
    this.node = c;

    if (this.node) {
      this._setDimensions();
    }
  };

  _setDimensions () {
    const width  = this.node.offsetWidth;
    const height = width / (16/9);

    this.setState({ width, height });
  }

  componentDidMount () {
    window.addEventListener('resize', this.handleResize, { passive: true });
  }

  componentWillUnmount () {
    window.removeEventListener('resize', this.handleResize);
  }

  handleResize = debounce(() => {
    if (this.node) {
      this._setDimensions();
    }
  }, 250, {
    trailing: true,
  });

  render () {
    const { height } = this.state;

    return (
      <div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}>
      <div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}>
        <Icon id='window-restore' />
        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
      </div>

M app/javascript/mastodon/components/status.jsx => app/javascript/mastodon/components/status.jsx +1 -6
@@ 411,7 411,7 @@ class Status extends ImmutablePureComponent {
    }

    if (pictureInPicture.get('inUse')) {
      media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
      media = <PictureInPicturePlaceholder />;
    } else if (status.get('media_attachments').size > 0) {
      if (this.props.muted) {
        media = (


@@ 460,12 460,9 @@ class Status extends ImmutablePureComponent {
                src={attachment.get('url')}
                alt={attachment.get('description')}
                lang={status.get('language')}
                width={this.props.cachedMediaWidth}
                height={110}
                inline
                sensitive={status.get('sensitive')}
                onOpenVideo={this.handleOpenVideo}
                cacheWidth={this.props.cacheMediaWidth}
                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
                visible={this.state.showMedia}
                onToggleVisibility={this.handleToggleMediaVisibility}


@@ 498,8 495,6 @@ class Status extends ImmutablePureComponent {
          onOpenMedia={this.handleOpenMedia}
          card={status.get('card')}
          compact
          cacheWidth={this.props.cacheMediaWidth}
          defaultWidth={this.props.cachedMediaWidth}
          sensitive={status.get('sensitive')}
        />
      );

M app/javascript/mastodon/features/audio/index.jsx => app/javascript/mastodon/features/audio/index.jsx +13 -6
@@ 384,7 384,7 @@ class Audio extends React.PureComponent {
  }

  _getRadius () {
    return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
    return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient());
  }

  _getScaleCoefficient () {


@@ 396,7 396,7 @@ class Audio extends React.PureComponent {
  }

  _getCY() {
    return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient()));
    return Math.floor((this.state.height || this.props.height) / 2);
  }

  _getAccentColor () {


@@ 470,7 470,7 @@ class Audio extends React.PureComponent {
    }

    return (
      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>

        <Blurhash
          hash={blurhash}


@@ 515,9 515,16 @@ class Audio extends React.PureComponent {
        {(revealed || editable) && <img
          src={this.props.poster}
          alt=''
          width={(this._getRadius() - TICK_SIZE) * 2}
          height={(this._getRadius() - TICK_SIZE) * 2}
          style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }}
          style={{
            position: 'absolute',
            left: '50%',
            top: '50%',
            height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`,
            aspectRatio: '1',
            transform: 'translate(-50%, -50%)',
            borderRadius: '50%',
            pointerEvents: 'none',
          }}
        />}

        <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>

M app/javascript/mastodon/features/status/components/card.jsx => app/javascript/mastodon/features/status/components/card.jsx +12 -35
@@ 8,7 8,6 @@ import classnames from 'classnames';
import Icon from 'mastodon/components/icon';
import { useBlurhash } from 'mastodon/initial_state';
import Blurhash from 'mastodon/components/blurhash';
import { debounce } from 'lodash';

const IDNA_PREFIX = 'xn--';



@@ 54,8 53,6 @@ export default class Card extends React.PureComponent {
    card: ImmutablePropTypes.map,
    onOpenMedia: PropTypes.func.isRequired,
    compact: PropTypes.bool,
    defaultWidth: PropTypes.number,
    cacheWidth: PropTypes.func,
    sensitive: PropTypes.bool,
  };



@@ 64,7 61,6 @@ export default class Card extends React.PureComponent {
  };

  state = {
    width: this.props.defaultWidth || 280,
    previewLoaded: false,
    embedded: false,
    revealed: !this.props.sensitive,


@@ 87,24 83,6 @@ export default class Card extends React.PureComponent {
    window.removeEventListener('resize', this.handleResize);
  }

  _setDimensions () {
    const width = this.node.offsetWidth;

    if (this.props.cacheWidth) {
      this.props.cacheWidth(width);
    }

    this.setState({ width });
  }

  handleResize = debounce(() => {
    if (this.node) {
      this._setDimensions();
    }
  }, 250, {
    trailing: true,
  });

  handlePhotoClick = () => {
    const { card, onOpenMedia } = this.props;



@@ 138,10 116,6 @@ export default class Card extends React.PureComponent {

  setRef = c => {
    this.node = c;

    if (this.node) {
      this._setDimensions();
    }
  };

  handleImageLoad = () => {


@@ 157,36 131,31 @@ export default class Card extends React.PureComponent {
  renderVideo () {
    const { card }  = this.props;
    const content   = { __html: addAutoPlay(card.get('html')) };
    const { width } = this.state;
    const ratio     = card.get('width') / card.get('height');
    const height    = width / ratio;

    return (
      <div
        ref={this.setRef}
        className='status-card__image status-card-video'
        dangerouslySetInnerHTML={content}
        style={{ height }}
        style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
      />
    );
  }

  render () {
    const { card, compact } = this.props;
    const { width, embedded, revealed } = this.state;
    const { embedded, revealed } = this.state;

    if (card === null) {
      return null;
    }

    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
    const horizontal  = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
    const horizontal  = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
    const interactive = card.get('type') !== 'link';
    const className   = classnames('status-card', { horizontal, compact, interactive });
    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
    const language    = card.get('language') || '';
    const ratio       = card.get('width') / card.get('height');
    const height      = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);

    const description = (
      <div className='status-card__content' lang={language}>


@@ 196,6 165,14 @@ export default class Card extends React.PureComponent {
      </div>
    );

    const thumbnailStyle = {
      visibility: revealed? null : 'hidden',
    };

    if (horizontal) {
      thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`;
    }

    let embed     = '';
    let canvas = (
      <Blurhash


@@ 206,7 183,7 @@ export default class Card extends React.PureComponent {
        dummy={!useBlurhash}
      />
    );
    let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
    let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
    let spoilerButton = (
      <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
        <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>

M app/javascript/mastodon/features/video/index.jsx => app/javascript/mastodon/features/video/index.jsx +5 -41
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { is } from 'immutable';
import { throttle, debounce } from 'lodash';
import { throttle } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from '../../initial_state';


@@ 102,8 102,6 @@ class Video extends React.PureComponent {
    src: PropTypes.string.isRequired,
    alt: PropTypes.string,
    lang: PropTypes.string,
    width: PropTypes.number,
    height: PropTypes.number,
    sensitive: PropTypes.bool,
    currentTime: PropTypes.number,
    onOpenVideo: PropTypes.func,


@@ 112,7 110,6 @@ class Video extends React.PureComponent {
    inline: PropTypes.bool,
    editable: PropTypes.bool,
    alwaysVisible: PropTypes.bool,
    cacheWidth: PropTypes.func,
    visible: PropTypes.bool,
    onToggleVisibility: PropTypes.func,
    deployPictureInPicture: PropTypes.func,


@@ 135,7 132,6 @@ class Video extends React.PureComponent {
    volume: 0.5,
    paused: true,
    dragging: false,
    containerWidth: this.props.width,
    fullscreen: false,
    hovered: false,
    muted: false,


@@ 144,24 140,8 @@ class Video extends React.PureComponent {

  setPlayerRef = c => {
    this.player = c;

    if (this.player) {
      this._setDimensions();
    }
  };

  _setDimensions () {
    const width = this.player.offsetWidth;

    if (this.props.cacheWidth) {
      this.props.cacheWidth(width);
    }

    this.setState({
      containerWidth: width,
    });
  }

  setVideoRef = c => {
    this.video = c;



@@ 370,12 350,10 @@ class Video extends React.PureComponent {
    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);

    window.addEventListener('scroll', this.handleScroll);
    window.addEventListener('resize', this.handleResize, { passive: true });
  }

  componentWillUnmount () {
    window.removeEventListener('scroll', this.handleScroll);
    window.removeEventListener('resize', this.handleResize);

    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);


@@ 404,14 382,6 @@ class Video extends React.PureComponent {
    }
  }

  handleResize = debounce(() => {
    if (this.player) {
      this._setDimensions();
    }
  }, 250, {
    trailing: true,
  });

  handleScroll = throttle(() => {
    if (!this.video) {
      return;


@@ 525,17 495,12 @@ class Video extends React.PureComponent {

  render () {
    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
    const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
    const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
    const progress = Math.min((currentTime / duration) * 100, 100);
    const playerStyle = {};

    let { width, height } = this.props;

    if (inline && containerWidth) {
      width  = containerWidth;
      height = containerWidth / (16/9);

      playerStyle.height = height;
    if (inline) {
      playerStyle.aspectRatio = '16 / 9';
    }

    let preload;


@@ 586,8 551,6 @@ class Video extends React.PureComponent {
          aria-label={alt}
          title={alt}
          lang={lang}
          width={width}
          height={height}
          volume={volume}
          onClick={this.togglePlay}
          onKeyDown={this.handleVideoKeyDown}


@@ 596,6 559,7 @@ class Video extends React.PureComponent {
          onLoadedData={this.handleLoadedData}
          onProgress={this.handleProgress}
          onVolumeChange={this.handleVolumeChange}
          style={{ ...playerStyle, width: '100%' }}
        />}

        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +5 -0
@@ 3804,6 3804,10 @@ a.status-card {
}

.status-card-video {
  // Firefox has a bug where frameborder=0 iframes add some extra blank space
  // see https://bugzilla.mozilla.org/show_bug.cgi?id=155174
  overflow: hidden;

  iframe {
    width: 100%;
    height: 100%;


@@ 8332,6 8336,7 @@ noscript {
  font-weight: 500;
  cursor: pointer;
  color: $darker-text-color;
  aspect-ratio: 16 / 9;

  i {
    display: block;