Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit af6bd63

Browse files
authored
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
1 parent afa60ac commit af6bd63

File tree

5 files changed

+89
-85
lines changed

5 files changed

+89
-85
lines changed

res/css/views/messages/_MImageBody.scss

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,42 @@ limitations under the License.
1717

1818
$timeline-image-border-radius: 8px;
1919

20-
.mx_MImageBody_thumbnail--blurhash {
20+
.mx_MImageBody_placeholder {
21+
// Position the placeholder on top of the thumbnail, so that the reveal animation can work
2122
position: absolute;
2223
left: 0;
2324
top: 0;
24-
}
25-
26-
.mx_MImageBody_thumbnail {
27-
object-fit: contain;
28-
border-radius: $timeline-image-border-radius;
29-
30-
display: flex;
31-
justify-content: center;
32-
align-items: center;
3325
height: 100%;
3426
width: 100%;
3527

36-
// this is needed so that the Blurhash can get have rounded corners without beeing the correct size during loading.
37-
overflow: hidden;
28+
background-color: $background;
29+
3830
.mx_Blurhash > canvas {
3931
animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1);
4032
}
41-
42-
.mx_no-image-placeholder {
43-
background-color: $primary-content;
44-
}
4533
}
4634

4735
.mx_MImageBody_thumbnail_container {
48-
// Prevent the padding-bottom (added inline in MImageBody.js) from
49-
// affecting elements below the container.
36+
border-radius: $timeline-image-border-radius;
37+
38+
// Necessary for the border radius to apply correctly to the placeholder
5039
overflow: hidden;
40+
contain: paint;
41+
}
5142

52-
// Make sure the _thumbnail is positioned relative to the _container
53-
position: relative;
43+
.mx_MImageBody_thumbnail {
44+
display: block;
45+
46+
// Force the image to be the full size of the container, even if the
47+
// pixel size is smaller. The problem here is that we don't know what
48+
// thumbnail size the HS is going to give us, but we have to commit to
49+
// a container size immediately and not change it when the image loads
50+
// or we'll get a scroll jump (or have to leave blank space).
51+
// This will obviously result in an upscaled image which will be a bit
52+
// blurry. The best fix would be for the HS to advertise what size thumbnails
53+
// it guarantees to produce.
54+
height: 100%;
55+
width: 100%;
5456
}
5557

5658
.mx_MImageBody_gifLabel {

res/css/views/messages/_MVideoBody.scss

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ limitations under the License.
1515
*/
1616

1717
span.mx_MVideoBody {
18-
video.mx_MVideoBody {
18+
overflow: hidden;
19+
20+
.mx_MVideoBody_container {
1921
border-radius: $timeline-image-border-radius;
22+
overflow: hidden;
23+
24+
video {
25+
height: 100%;
26+
width: 100%;
27+
}
2028
}
2129
}

res/css/views/rooms/_EventBubbleTile.scss

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ limitations under the License.
126126
.mx_EventTile_line {
127127
border-bottom-right-radius: var(--cornerRadius);
128128

129-
.mx_MImageBody .mx_MImageBody_thumbnail,
129+
.mx_MImageBody .mx_MImageBody_thumbnail_container,
130130
.mx_MImageBody::before,
131+
.mx_MVideoBody .mx_MVideoBody_container,
131132
.mx_MediaBody,
132133
.mx_MLocationBody_map {
133134
border-bottom-right-radius: var(--cornerRadius) !important;
@@ -150,8 +151,9 @@ limitations under the License.
150151
float: right;
151152
border-bottom-left-radius: var(--cornerRadius);
152153

153-
.mx_MImageBody .mx_MImageBody_thumbnail,
154+
.mx_MImageBody .mx_MImageBody_thumbnail_container,
154155
.mx_MImageBody::before,
156+
.mx_MVideoBody .mx_MVideoBody_container,
155157
.mx_MediaBody,
156158
.mx_MLocationBody_map {
157159
border-bottom-left-radius: var(--cornerRadius) !important;
@@ -266,7 +268,8 @@ limitations under the License.
266268
}
267269

268270
//noinspection CssReplaceWithShorthandSafely
269-
.mx_MImageBody .mx_MImageBody_thumbnail,
271+
.mx_MImageBody .mx_MImageBody_thumbnail_container,
272+
.mx_MVideoBody .mx_MVideoBody_container,
270273
.mx_MediaBody {
271274
border-radius: unset;
272275
border-top-left-radius: var(--cornerRadius);
@@ -293,7 +296,8 @@ limitations under the License.
293296
&.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
294297
border-top-left-radius: 0;
295298

296-
.mx_MImageBody .mx_MImageBody_thumbnail,
299+
.mx_MImageBody .mx_MImageBody_thumbnail_container,
300+
.mx_MVideoBody .mx_MVideoBody_container,
297301
.mx_MImageBody::before,
298302
.mx_MediaBody,
299303
.mx_MLocationBody_map {
@@ -303,7 +307,8 @@ limitations under the License.
303307
&.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
304308
border-bottom-left-radius: var(--cornerRadius);
305309

306-
.mx_MImageBody .mx_MImageBody_thumbnail,
310+
.mx_MImageBody .mx_MImageBody_thumbnail_container,
311+
.mx_MVideoBody .mx_MVideoBody_container,
307312
.mx_MImageBody::before,
308313
.mx_MediaBody,
309314
.mx_MLocationBody_map {
@@ -314,7 +319,8 @@ limitations under the License.
314319
&.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
315320
border-top-right-radius: 0;
316321

317-
.mx_MImageBody .mx_MImageBody_thumbnail,
322+
.mx_MImageBody .mx_MImageBody_thumbnail_container,
323+
.mx_MVideoBody .mx_MVideoBody_container,
318324
.mx_MImageBody::before,
319325
.mx_MediaBody,
320326
.mx_MLocationBody_map {
@@ -324,7 +330,8 @@ limitations under the License.
324330
&.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
325331
border-bottom-right-radius: var(--cornerRadius);
326332

327-
.mx_MImageBody .mx_MImageBody_thumbnail,
333+
.mx_MImageBody .mx_MImageBody_thumbnail_container,
334+
.mx_MVideoBody .mx_MVideoBody_container,
328335
.mx_MImageBody::before,
329336
.mx_MediaBody,
330337
.mx_MLocationBody_map {

src/components/views/messages/MImageBody.tsx

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import MFileBody from './MFileBody';
2727
import Modal from '../../../Modal';
2828
import { _t } from '../../../languageHandler';
2929
import SettingsStore from "../../../settings/SettingsStore";
30-
import InlineSpinner from '../elements/InlineSpinner';
30+
import Spinner from '../elements/Spinner';
3131
import { replaceableComponent } from "../../../utils/replaceableComponent";
3232
import { Media, mediaFromContent } from "../../../customisations/Media";
3333
import { BLURHASH_FIELD, createThumbnail } from "../../../ContentMessages";
@@ -427,15 +427,6 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
427427
className="mx_MImageBody_thumbnail"
428428
src={thumbUrl}
429429
ref={this.image}
430-
// Force the image to be the full size of the container, even if the
431-
// pixel size is smaller. The problem here is that we don't know what
432-
// thumbnail size the HS is going to give us, but we have to commit to
433-
// a container size immediately and not change it when the image loads
434-
// or we'll get a scroll jump (or have to leave blank space).
435-
// This will obviously result in an upscaled image which will be a bit
436-
// blurry. The best fix would be for the HS to advertise what size thumbnails
437-
// it guarantees to produce.
438-
style={{ height: '100%' }}
439430
alt={content.body}
440431
onError={this.onImageError}
441432
onLoad={this.onImageLoad}
@@ -456,44 +447,32 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
456447
}
457448

458449
const classes = classNames({
459-
'mx_MImageBody_thumbnail': true,
460-
'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
450+
'mx_MImageBody_placeholder': true,
451+
'mx_MImageBody_placeholder--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
461452
});
462453

463-
// This has incredibly broken types.
464-
const C = CSSTransition as any;
465454
const thumbnail = (
466455
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight, maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
467456
<SwitchTransition mode="out-in">
468-
<C
457+
<CSSTransition
469458
classNames="mx_rtg--fade"
470459
key={`img-${showPlaceholder}`}
471460
timeout={300}
472461
>
473-
{ /* This weirdly looking div is necessary here, otherwise SwitchTransition fails */ }
474-
<div>
475-
{ showPlaceholder && <div
476-
className={classes}
477-
style={{
478-
// Constrain width here so that spinner appears central to the loaded thumbnail
479-
maxWidth,
480-
maxHeight,
481-
aspectRatio: `${infoWidth}/${infoHeight}`,
482-
}}
483-
>
484-
{ placeholder }
485-
</div> }
486-
</div>
487-
</C>
462+
{ showPlaceholder ? <div className={classes}>
463+
{ placeholder }
464+
</div> : <></> /* Transition always expects a child */ }
465+
</CSSTransition>
488466
</SwitchTransition>
489467

490-
<div style={{
491-
height: '100%',
492-
}}>
468+
<div style={{ maxHeight, maxWidth }}>
493469
{ img }
494470
{ gifLabel }
495471
</div>
496472

473+
{ /* HACK: This div fills out space while the image loads, to prevent scroll jumps */ }
474+
{ !this.state.imgLoaded && <div style={{ height: maxHeight, width: maxWidth }} /> }
475+
497476
{ this.state.hover && this.getTooltip() }
498477
</div>
499478
);
@@ -514,14 +493,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
514493

515494
if (blurhash) {
516495
if (this.state.placeholder === Placeholder.NoImage) {
517-
return <div className="mx_no-image-placeholder" style={{ width: width, height: height }} />;
496+
return null;
518497
} else if (this.state.placeholder === Placeholder.Blurhash) {
519498
return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />;
520499
}
521500
}
522-
return (
523-
<InlineSpinner w={32} h={32} />
524-
);
501+
return <Spinner w={32} h={32} />;
525502
}
526503

527504
// Overidden by MStickerBody

src/components/views/messages/MVideoBody.tsx

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,18 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
228228
const content = this.props.mxEvent.getContent();
229229
const autoplay = SettingsStore.getValue("autoplayVideo");
230230

231+
let aspectRatio;
232+
if (content.info?.w && content.info?.h) {
233+
aspectRatio = `${content.info.w}/${content.info.h}`;
234+
}
235+
const { w: maxWidth, h: maxHeight } = suggestedVideoSize(
236+
SettingsStore.getValue("Images.size") as ImageSize,
237+
{ w: content.info?.w, h: content.info?.h },
238+
);
239+
240+
// HACK: This div fills out space while the video loads, to prevent scroll jumps
241+
const spaceFiller = <div style={{ width: maxWidth, height: maxHeight }} />;
242+
231243
if (this.state.error !== null) {
232244
return (
233245
<span className="mx_MVideoBody">
@@ -241,21 +253,17 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
241253
if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
242254
// Need to decrypt the attachment
243255
// The attachment is decrypted in componentDidMount.
244-
// For now add an img tag with a spinner.
256+
// For now show a spinner.
245257
return (
246258
<span className="mx_MVideoBody">
247-
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner">
259+
<div className="mx_MVideoBody_container" style={{ maxWidth, maxHeight, aspectRatio }}>
248260
<InlineSpinner />
249261
</div>
262+
{ spaceFiller }
250263
</span>
251264
);
252265
}
253266

254-
const { w: maxWidth, h: maxHeight } = suggestedVideoSize(
255-
SettingsStore.getValue("Images.size") as ImageSize,
256-
{ w: content.info?.w, h: content.info?.h },
257-
);
258-
259267
const contentUrl = this.getContentUrl();
260268
const thumbUrl = this.getThumbUrl();
261269
let poster = null;
@@ -268,19 +276,21 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
268276
const fileBody = this.getFileBody();
269277
return (
270278
<span className="mx_MVideoBody">
271-
<video
272-
className="mx_MVideoBody"
273-
ref={this.videoRef}
274-
src={contentUrl}
275-
title={content.body}
276-
controls
277-
preload={preload}
278-
muted={autoplay}
279-
autoPlay={autoplay}
280-
style={{ maxHeight, maxWidth }}
281-
poster={poster}
282-
onPlay={this.videoOnPlay}
283-
/>
279+
<div className="mx_MVideoBody_container" style={{ maxWidth, maxHeight, aspectRatio }}>
280+
<video
281+
className="mx_MVideoBody"
282+
ref={this.videoRef}
283+
src={contentUrl}
284+
title={content.body}
285+
controls
286+
preload={preload}
287+
muted={autoplay}
288+
autoPlay={autoplay}
289+
poster={poster}
290+
onPlay={this.videoOnPlay}
291+
/>
292+
{ spaceFiller }
293+
</div>
284294
{ fileBody }
285295
</span>
286296
);

0 commit comments

Comments
 (0)