From 598e63dad262ba454deda410f56c8f1b49ed96f8 Mon Sep 17 00:00:00 2001 From: Claire <claire.github-309c@sitedethib.com> Date: Tue, 2 May 2023 13:58:48 +0200 Subject: [PATCH] Change media elements to use aspect-ratio rather than compute height themselves (#24686) --- .../mastodon/components/media_gallery.jsx | 10 ++-- .../picture_in_picture_placeholder.jsx | 42 +---------------- app/javascript/mastodon/components/status.jsx | 7 +-- .../mastodon/features/audio/index.jsx | 19 +++++--- .../features/status/components/card.jsx | 47 +++++-------------- .../mastodon/features/video/index.jsx | 46 ++---------------- .../styles/mastodon/components.scss | 5 ++ 7 files changed, 40 insertions(+), 136 deletions(-) diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index 5be0070a3..859d16a32 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -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; diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx index 6322b1c66..a51c97401 100644 --- a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx @@ -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> diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index cd8423b2f..b5242e769 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -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')} /> ); diff --git a/app/javascript/mastodon/features/audio/index.jsx b/app/javascript/mastodon/features/audio/index.jsx index 53f24c6a3..e8fe2c4d9 100644 --- a/app/javascript/mastodon/features/audio/index.jsx +++ b/app/javascript/mastodon/features/audio/index.jsx @@ -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}> diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index b67f671c5..88b38c65a 100644 --- a/app/javascript/mastodon/features/status/components/card.jsx +++ b/app/javascript/mastodon/features/status/components/card.jsx @@ -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> diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx index e2637e0be..d76d34546 100644 --- a/app/javascript/mastodon/features/video/index.jsx +++ b/app/javascript/mastodon/features/video/index.jsx @@ -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 })}> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index df3330316..3710695e0 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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;