From af6bd63ac7ab5b10664888a9c3f381abee4cf880 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Mar 2022 15:07:02 -0400 Subject: [PATCH] Fix some image/video scroll jumps (#8182) * Fix some image/video scroll jumps * Fix aspect ratio formatting * Fix videos not being responsive to timeline width --- res/css/views/messages/_MImageBody.scss | 42 ++++++++-------- res/css/views/messages/_MVideoBody.scss | 10 +++- res/css/views/rooms/_EventBubbleTile.scss | 21 +++++--- src/components/views/messages/MImageBody.tsx | 51 ++++++-------------- src/components/views/messages/MVideoBody.tsx | 50 +++++++++++-------- 5 files changed, 89 insertions(+), 85 deletions(-) diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index e155afcbf39..dc8ebd98044 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -17,40 +17,42 @@ limitations under the License. $timeline-image-border-radius: 8px; -.mx_MImageBody_thumbnail--blurhash { +.mx_MImageBody_placeholder { + // Position the placeholder on top of the thumbnail, so that the reveal animation can work position: absolute; left: 0; top: 0; -} - -.mx_MImageBody_thumbnail { - object-fit: contain; - border-radius: $timeline-image-border-radius; - - display: flex; - justify-content: center; - align-items: center; height: 100%; width: 100%; - // this is needed so that the Blurhash can get have rounded corners without beeing the correct size during loading. - overflow: hidden; + background-color: $background; + .mx_Blurhash > canvas { animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1); } - - .mx_no-image-placeholder { - background-color: $primary-content; - } } .mx_MImageBody_thumbnail_container { - // Prevent the padding-bottom (added inline in MImageBody.js) from - // affecting elements below the container. + border-radius: $timeline-image-border-radius; + + // Necessary for the border radius to apply correctly to the placeholder overflow: hidden; + contain: paint; +} - // Make sure the _thumbnail is positioned relative to the _container - position: relative; +.mx_MImageBody_thumbnail { + display: block; + + // Force the image to be the full size of the container, even if the + // pixel size is smaller. The problem here is that we don't know what + // thumbnail size the HS is going to give us, but we have to commit to + // a container size immediately and not change it when the image loads + // or we'll get a scroll jump (or have to leave blank space). + // This will obviously result in an upscaled image which will be a bit + // blurry. The best fix would be for the HS to advertise what size thumbnails + // it guarantees to produce. + height: 100%; + width: 100%; } .mx_MImageBody_gifLabel { diff --git a/res/css/views/messages/_MVideoBody.scss b/res/css/views/messages/_MVideoBody.scss index 5b29659b840..b4cd545bf07 100644 --- a/res/css/views/messages/_MVideoBody.scss +++ b/res/css/views/messages/_MVideoBody.scss @@ -15,7 +15,15 @@ limitations under the License. */ span.mx_MVideoBody { - video.mx_MVideoBody { + overflow: hidden; + + .mx_MVideoBody_container { border-radius: $timeline-image-border-radius; + overflow: hidden; + + video { + height: 100%; + width: 100%; + } } } diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 28f1eb87406..a46e91a4aec 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -126,8 +126,9 @@ limitations under the License. .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); - .mx_MImageBody .mx_MImageBody_thumbnail, + .mx_MImageBody .mx_MImageBody_thumbnail_container, .mx_MImageBody::before, + .mx_MVideoBody .mx_MVideoBody_container, .mx_MediaBody, .mx_MLocationBody_map { border-bottom-right-radius: var(--cornerRadius) !important; @@ -150,8 +151,9 @@ limitations under the License. float: right; border-bottom-left-radius: var(--cornerRadius); - .mx_MImageBody .mx_MImageBody_thumbnail, + .mx_MImageBody .mx_MImageBody_thumbnail_container, .mx_MImageBody::before, + .mx_MVideoBody .mx_MVideoBody_container, .mx_MediaBody, .mx_MLocationBody_map { border-bottom-left-radius: var(--cornerRadius) !important; @@ -266,7 +268,8 @@ limitations under the License. } //noinspection CssReplaceWithShorthandSafely - .mx_MImageBody .mx_MImageBody_thumbnail, + .mx_MImageBody .mx_MImageBody_thumbnail_container, + .mx_MVideoBody .mx_MVideoBody_container, .mx_MediaBody { border-radius: unset; border-top-left-radius: var(--cornerRadius); @@ -293,7 +296,8 @@ limitations under the License. &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line { border-top-left-radius: 0; - .mx_MImageBody .mx_MImageBody_thumbnail, + .mx_MImageBody .mx_MImageBody_thumbnail_container, + .mx_MVideoBody .mx_MVideoBody_container, .mx_MImageBody::before, .mx_MediaBody, .mx_MLocationBody_map { @@ -303,7 +307,8 @@ limitations under the License. &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line { border-bottom-left-radius: var(--cornerRadius); - .mx_MImageBody .mx_MImageBody_thumbnail, + .mx_MImageBody .mx_MImageBody_thumbnail_container, + .mx_MVideoBody .mx_MVideoBody_container, .mx_MImageBody::before, .mx_MediaBody, .mx_MLocationBody_map { @@ -314,7 +319,8 @@ limitations under the License. &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line { border-top-right-radius: 0; - .mx_MImageBody .mx_MImageBody_thumbnail, + .mx_MImageBody .mx_MImageBody_thumbnail_container, + .mx_MVideoBody .mx_MVideoBody_container, .mx_MImageBody::before, .mx_MediaBody, .mx_MLocationBody_map { @@ -324,7 +330,8 @@ limitations under the License. &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); - .mx_MImageBody .mx_MImageBody_thumbnail, + .mx_MImageBody .mx_MImageBody_thumbnail_container, + .mx_MVideoBody .mx_MVideoBody_container, .mx_MImageBody::before, .mx_MediaBody, .mx_MLocationBody_map { diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 19e71b72bdb..d5d8ebf4712 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -27,7 +27,7 @@ import MFileBody from './MFileBody'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; -import InlineSpinner from '../elements/InlineSpinner'; +import Spinner from '../elements/Spinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { Media, mediaFromContent } from "../../../customisations/Media"; import { BLURHASH_FIELD, createThumbnail } from "../../../ContentMessages"; @@ -427,15 +427,6 @@ export default class MImageBody extends React.Component { className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this.image} - // Force the image to be the full size of the container, even if the - // pixel size is smaller. The problem here is that we don't know what - // thumbnail size the HS is going to give us, but we have to commit to - // a container size immediately and not change it when the image loads - // or we'll get a scroll jump (or have to leave blank space). - // This will obviously result in an upscaled image which will be a bit - // blurry. The best fix would be for the HS to advertise what size thumbnails - // it guarantees to produce. - style={{ height: '100%' }} alt={content.body} onError={this.onImageError} onLoad={this.onImageLoad} @@ -456,44 +447,32 @@ export default class MImageBody extends React.Component { } const classes = classNames({ - 'mx_MImageBody_thumbnail': true, - 'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD], + 'mx_MImageBody_placeholder': true, + 'mx_MImageBody_placeholder--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD], }); - // This has incredibly broken types. - const C = CSSTransition as any; const thumbnail = (
- - { /* This weirdly looking div is necessary here, otherwise SwitchTransition fails */ } -
- { showPlaceholder &&
- { placeholder } -
} -
-
+ { showPlaceholder ?
+ { placeholder } +
: <> /* Transition always expects a child */ } +
-
+
{ img } { gifLabel }
+ { /* HACK: This div fills out space while the image loads, to prevent scroll jumps */ } + { !this.state.imgLoaded &&
} + { this.state.hover && this.getTooltip() }
); @@ -514,14 +493,12 @@ export default class MImageBody extends React.Component { if (blurhash) { if (this.state.placeholder === Placeholder.NoImage) { - return
; + return null; } else if (this.state.placeholder === Placeholder.Blurhash) { return ; } } - return ( - - ); + return ; } // Overidden by MStickerBody diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index fe5f59ed978..631618ae018 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -228,6 +228,18 @@ export default class MVideoBody extends React.PureComponent const content = this.props.mxEvent.getContent(); const autoplay = SettingsStore.getValue("autoplayVideo"); + let aspectRatio; + if (content.info?.w && content.info?.h) { + aspectRatio = `${content.info.w}/${content.info.h}`; + } + const { w: maxWidth, h: maxHeight } = suggestedVideoSize( + SettingsStore.getValue("Images.size") as ImageSize, + { w: content.info?.w, h: content.info?.h }, + ); + + // HACK: This div fills out space while the video loads, to prevent scroll jumps + const spaceFiller =
; + if (this.state.error !== null) { return ( @@ -241,21 +253,17 @@ export default class MVideoBody extends React.PureComponent if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) { // Need to decrypt the attachment // The attachment is decrypted in componentDidMount. - // For now add an img tag with a spinner. + // For now show a spinner. return ( -
+
+ { spaceFiller } ); } - const { w: maxWidth, h: maxHeight } = suggestedVideoSize( - SettingsStore.getValue("Images.size") as ImageSize, - { w: content.info?.w, h: content.info?.h }, - ); - const contentUrl = this.getContentUrl(); const thumbUrl = this.getThumbUrl(); let poster = null; @@ -268,19 +276,21 @@ export default class MVideoBody extends React.PureComponent const fileBody = this.getFileBody(); return ( - );