From 2529b88bf61f6169634b92911a185be10a4b96bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 21:24:24 +0200 Subject: [PATCH 01/14] Give lightbox a background load animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_common.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/res/css/_common.scss b/res/css/_common.scss index b128a824428..066bbea510f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -315,9 +315,20 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { opacity: 0.4; } +@keyframes mx_Dialog_lightbox_background_keyframes { + from { + opacity: 0; + } + to { + opacity: $lightbox-background-bg-opacity; + } +} + .mx_Dialog_lightbox .mx_Dialog_background { opacity: $lightbox-background-bg-opacity; background-color: $lightbox-background-bg-color; + animation-name: mx_Dialog_lightbox_background_keyframes; + animation-duration: 0.25s; } .mx_Dialog_lightbox .mx_Dialog { From ce7f387dd6b8efd10b2704ce576d41dee4168af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 21:24:46 +0200 Subject: [PATCH 02/14] Extends IMediaEventContent by thumbnail info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/customisations/models/IMediaEventContent.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts index 62dfe4ee19a..d4f6340ccb7 100644 --- a/src/customisations/models/IMediaEventContent.ts +++ b/src/customisations/models/IMediaEventContent.ts @@ -38,6 +38,10 @@ export interface IMediaEventContent { info?: { thumbnail_url?: string; // eslint-disable-line camelcase thumbnail_file?: IEncryptedFile; // eslint-disable-line camelcase + thumbnail_info?: { // eslint-disable-line camelcase + w: number; + h: number; + }; mimetype: string; w?: number; h?: number; From 1b00b304bbe8fb8942ab0b4bf6a0de8deb63398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 21:25:11 +0200 Subject: [PATCH 03/14] Give image view panel a loading animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_ImageView.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index cf92ffec643..41d24ac0517 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -38,12 +38,23 @@ $button-gap: 24px; flex-shrink: 0; } +@keyframes mx_ImageView_panel_keyframes { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + .mx_ImageView_panel { width: 100%; height: 68px; display: flex; justify-content: space-between; align-items: center; + animation-name: mx_ImageView_panel_keyframes; + animation-duration: 0.25s; } .mx_ImageView_info_wrapper { From d421c3203f7c823b9359cc781d0d469678405b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 21:26:15 +0200 Subject: [PATCH 04/14] Initial implementation of loading animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 35 ++++++++++++++----- src/components/views/messages/MImageBody.tsx | 11 ++++++ .../views/rooms/LinkPreviewWidget.tsx | 18 ++++++++-- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index db63dbbfc20..56c81c937ea 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -34,6 +34,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { normalizeWheelEvent } from "../../../utils/Mouse"; import { IDialogProps } from '../dialogs/IDialogProps'; +import UIStore from '../../../stores/UIStore'; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -56,8 +57,15 @@ interface IProps extends IDialogProps { // redactions, senders, timestamps etc. Other descriptors are taken from the explicit // properties above, which let us use lightboxes to display images which aren't associated // with events. - mxEvent: MatrixEvent; - permalinkCreator: RoomPermalinkCreator; + mxEvent?: MatrixEvent; + permalinkCreator?: RoomPermalinkCreator; + + thumbnailInfo?: { + positionX: number; + positionY: number; + width: number; + height: number; + }; } interface IState { @@ -75,13 +83,16 @@ interface IState { export default class ImageView extends React.Component { constructor(props) { super(props); + + const { thumbnailInfo, width } = this.props; + this.state = { - zoom: 0, + zoom: thumbnailInfo?.width / width ?? 0, minZoom: MAX_SCALE, maxZoom: MAX_SCALE, rotation: 0, - translationX: 0, - translationY: 0, + translationX: thumbnailInfo?.positionX + (thumbnailInfo?.width / 2) - (UIStore.instance.windowWidth / 2), + translationY: thumbnailInfo?.positionY + (thumbnailInfo?.height / 2) - (UIStore.instance.windowHeight / 2), moving: false, contextMenuDisplayed: false, }; @@ -105,15 +116,23 @@ export default class ImageView extends React.Component { // We want to recalculate zoom whenever the window's size changes window.addEventListener("resize", this.recalculateZoom); // After the image loads for the first time we want to calculate the zoom - this.image.current.addEventListener("load", this.recalculateZoom); + this.image.current.addEventListener("load", this.imageLoaded); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); window.removeEventListener("resize", this.recalculateZoom); - this.image.current.removeEventListener("load", this.recalculateZoom); + this.image.current.removeEventListener("load", this.imageLoaded); } + private imageLoaded =() => { + this.setZoomAndRotation(); + this.setState({ + translationX: 0, + translationY: 0, + }); + }; + private recalculateZoom = () => { this.setZoomAndRotation(); }; @@ -380,7 +399,7 @@ export default class ImageView extends React.Component { // image causing it translate in the wrong direction. const style = { cursor: cursor, - transition: this.state.moving ? null : "transform 200ms ease 0s", + transition: this.state.moving ? null : "transform 250ms ease 0s", transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) scale(${zoom}) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 44c15d50e76..c433ddb8e36 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -112,6 +112,17 @@ export default class MImageBody extends React.Component { params.fileSize = content.info.size; } + if (this.image.current) { + const clientRect = this.image.current.getBoundingClientRect(); + + params.thumbnailInfo = { + width: clientRect.width, + height: clientRect.height, + positionX: clientRect.x, + positionY: clientRect.y, + }; + } + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); } }; diff --git a/src/components/views/rooms/LinkPreviewWidget.tsx b/src/components/views/rooms/LinkPreviewWidget.tsx index 55e123f4e02..5e7154dc8ac 100644 --- a/src/components/views/rooms/LinkPreviewWidget.tsx +++ b/src/components/views/rooms/LinkPreviewWidget.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { ComponentProps, createRef } from 'react'; import { AllHtmlEntities } from 'html-entities'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client'; @@ -36,6 +36,7 @@ interface IProps { @replaceableComponent("views.rooms.LinkPreviewWidget") export default class LinkPreviewWidget extends React.Component { private readonly description = createRef(); + private image = createRef(); componentDidMount() { if (this.description.current) { @@ -59,7 +60,7 @@ export default class LinkPreviewWidget extends React.Component { src = mediaFromMxc(src).srcHttp; } - const params = { + const params: Omit, "onFinished"> = { src: src, width: p["og:image:width"], height: p["og:image:height"], @@ -68,6 +69,17 @@ export default class LinkPreviewWidget extends React.Component { link: this.props.link, }; + if (this.image.current) { + const clientRect = this.image.current.getBoundingClientRect(); + + params.thumbnailInfo = { + width: clientRect.width, + height: clientRect.height, + positionX: clientRect.x, + positionY: clientRect.y, + }; + } + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); }; @@ -100,7 +112,7 @@ export default class LinkPreviewWidget extends React.Component { let img; if (image) { img =
- +
; } From 7c81526805e97e45b7faf88718874c77e705b941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 21:52:36 +0200 Subject: [PATCH 05/14] Take panel height into account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 56c81c937ea..f589ba7dc02 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -45,6 +45,8 @@ const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; +const PANEL_HEIGHT = 68; + interface IProps extends IDialogProps { src: string; // the source of the image being displayed name?: string; // the main title ('name') for the image @@ -91,8 +93,17 @@ export default class ImageView extends React.Component { minZoom: MAX_SCALE, maxZoom: MAX_SCALE, rotation: 0, - translationX: thumbnailInfo?.positionX + (thumbnailInfo?.width / 2) - (UIStore.instance.windowWidth / 2), - translationY: thumbnailInfo?.positionY + (thumbnailInfo?.height / 2) - (UIStore.instance.windowHeight / 2), + translationX: ( + thumbnailInfo?.positionX + + (thumbnailInfo?.width / 2) - + (UIStore.instance.windowWidth / 2) + ), + translationY: ( + thumbnailInfo?.positionY + + (thumbnailInfo?.height / 2) - + (UIStore.instance.windowHeight / 2) - + (PANEL_HEIGHT / 2) + ), moving: false, contextMenuDisplayed: false, }; @@ -125,7 +136,7 @@ export default class ImageView extends React.Component { this.image.current.removeEventListener("load", this.imageLoaded); } - private imageLoaded =() => { + private imageLoaded = () => { this.setZoomAndRotation(); this.setState({ translationX: 0, From 2fd221bc1807fa2e891053e4265508c8583e1b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 23 Jul 2021 08:00:51 +0200 Subject: [PATCH 06/14] Update animation speed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_common.scss | 2 +- res/css/views/elements/_ImageView.scss | 2 +- src/components/views/elements/ImageView.tsx | 28 ++++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 066bbea510f..c85a2a74e51 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -328,7 +328,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { opacity: $lightbox-background-bg-opacity; background-color: $lightbox-background-bg-color; animation-name: mx_Dialog_lightbox_background_keyframes; - animation-duration: 0.25s; + animation-duration: 300ms; } .mx_Dialog_lightbox .mx_Dialog { diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 41d24ac0517..4bf957b93be 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -54,7 +54,7 @@ $button-gap: 24px; justify-content: space-between; align-items: center; animation-name: mx_ImageView_panel_keyframes; - animation-duration: 0.25s; + animation-duration: 300ms; } .mx_ImageView_info_wrapper { diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index f589ba7dc02..d09062f3abe 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -120,6 +120,8 @@ export default class ImageView extends React.Component { private previousX = 0; private previousY = 0; + private loaded = false; + componentDidMount() { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium @@ -136,12 +138,13 @@ export default class ImageView extends React.Component { this.image.current.removeEventListener("load", this.imageLoaded); } - private imageLoaded = () => { + private imageLoaded = async () => { this.setZoomAndRotation(); - this.setState({ + await this.setState({ translationX: 0, translationY: 0, }); + this.loaded = true; }; private recalculateZoom = () => { @@ -390,16 +393,17 @@ export default class ImageView extends React.Component { const showEventMeta = !!this.props.mxEvent; const zoomingDisabled = this.state.maxZoom === this.state.minZoom; + let transition; + if (!this.loaded) transition = "transform 300ms ease 0s"; + else if (this.state.moving) transition = null; + else transition = "transform 200ms ease 0s"; + let cursor; - if (this.state.moving) { - cursor= "grabbing"; - } else if (zoomingDisabled) { - cursor = "default"; - } else if (this.state.zoom === this.state.minZoom) { - cursor = "zoom-in"; - } else { - cursor = "zoom-out"; - } + if (this.state.moving) cursor = "grabbing"; + else if (zoomingDisabled) cursor = "default"; + else if (this.state.zoom === this.state.minZoom) cursor = "zoom-in"; + else cursor = "zoom-out"; + const rotationDegrees = this.state.rotation + "deg"; const zoom = this.state.zoom; const translatePixelsX = this.state.translationX + "px"; @@ -410,7 +414,7 @@ export default class ImageView extends React.Component { // image causing it translate in the wrong direction. const style = { cursor: cursor, - transition: this.state.moving ? null : "transform 250ms ease 0s", + transition: transition, transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) scale(${zoom}) From 8d5fd9306f42ed2e10549c291e24d1b17af92c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 23 Jul 2021 10:07:09 +0200 Subject: [PATCH 07/14] Add some null guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index d09062f3abe..bc559603e01 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -97,13 +97,13 @@ export default class ImageView extends React.Component { thumbnailInfo?.positionX + (thumbnailInfo?.width / 2) - (UIStore.instance.windowWidth / 2) - ), + ) ?? 0, translationY: ( thumbnailInfo?.positionY + (thumbnailInfo?.height / 2) - (UIStore.instance.windowHeight / 2) - (PANEL_HEIGHT / 2) - ), + ) ?? 0, moving: false, contextMenuDisplayed: false, }; From 91a11b6997ad29e3517be3f866865d968ca399f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 23 Jul 2021 13:52:29 +0200 Subject: [PATCH 08/14] Fix animation issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 26 ++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index bc559603e01..90920ff67f0 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -86,10 +86,10 @@ export default class ImageView extends React.Component { constructor(props) { super(props); - const { thumbnailInfo, width } = this.props; + const { thumbnailInfo } = this.props; this.state = { - zoom: thumbnailInfo?.width / width ?? 0, + zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize minZoom: MAX_SCALE, maxZoom: MAX_SCALE, rotation: 0, @@ -120,7 +120,8 @@ export default class ImageView extends React.Component { private previousX = 0; private previousY = 0; - private loaded = false; + private animatingLoading = false; + private imageIsLoaded = false; componentDidMount() { // We have to use addEventListener() because the listener @@ -139,12 +140,25 @@ export default class ImageView extends React.Component { } private imageLoaded = async () => { + // First, we calculate the zoom, so that the image has the same size as + // the thumbnail + const { thumbnailInfo } = this.props; + if (thumbnailInfo?.width) { + await this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth }); + } + + // Once the zoom is set, we the image is considered loaded and we can + // start animating it into the center of the screen + this.imageIsLoaded = true; + this.animatingLoading = true; this.setZoomAndRotation(); await this.setState({ translationX: 0, translationY: 0, }); - this.loaded = true; + + // Once the position is set, there is no need to animate anymore + this.animatingLoading = false; }; private recalculateZoom = () => { @@ -394,8 +408,8 @@ export default class ImageView extends React.Component { const zoomingDisabled = this.state.maxZoom === this.state.minZoom; let transition; - if (!this.loaded) transition = "transform 300ms ease 0s"; - else if (this.state.moving) transition = null; + if (this.animatingLoading) transition = "transform 300ms ease 0s"; + else if (this.state.moving || !this.imageIsLoaded) transition = null; else transition = "transform 200ms ease 0s"; let cursor; From 8e3be15365af599de094ead7038f01ae3c1215aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 14 Sep 2021 18:05:15 +0200 Subject: [PATCH 09/14] Move animations into _animations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_animations.scss | 20 ++++++++++++++++++++ res/css/_common.scss | 9 --------- res/css/views/elements/_ImageView.scss | 9 --------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/res/css/_animations.scss b/res/css/_animations.scss index 4d3ad971413..89c406dd850 100644 --- a/res/css/_animations.scss +++ b/res/css/_animations.scss @@ -53,3 +53,23 @@ limitations under the License. transition: none; } } + + +@keyframes mx_Dialog_lightbox_background_keyframes { + from { + opacity: 0; + } + to { + opacity: $lightbox-background-bg-opacity; + } +} + + +@keyframes mx_ImageView_panel_keyframes { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/res/css/_common.scss b/res/css/_common.scss index 07f6c316b67..d7f8355d81a 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -315,15 +315,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { opacity: 0.4; } -@keyframes mx_Dialog_lightbox_background_keyframes { - from { - opacity: 0; - } - to { - opacity: $lightbox-background-bg-opacity; - } -} - .mx_Dialog_lightbox .mx_Dialog_background { opacity: $lightbox-background-bg-opacity; background-color: $lightbox-background-bg-color; diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 4bf957b93be..6510cc2ec65 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -38,15 +38,6 @@ $button-gap: 24px; flex-shrink: 0; } -@keyframes mx_ImageView_panel_keyframes { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - .mx_ImageView_panel { width: 100%; height: 68px; From ccc042b7d7260dbcbbf81f31d8c2fc461c098790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 14 Sep 2021 18:06:01 +0200 Subject: [PATCH 10/14] Where does that magic number come from? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 8dc2aee269d..eedf492e1f9 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -45,6 +45,7 @@ const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; +// Height of mx_ImageView_panel const PANEL_HEIGHT = 68; interface IProps extends IDialogProps { From f5d8bb7cbe7bca3cb4bbc102a3959823b51c72f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 14 Sep 2021 18:15:27 +0200 Subject: [PATCH 11/14] Remove awaiting setState() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index eedf492e1f9..362d1c7da94 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -140,12 +140,12 @@ export default class ImageView extends React.Component { this.image.current.removeEventListener("load", this.imageLoaded); } - private imageLoaded = async () => { + private imageLoaded = () => { // First, we calculate the zoom, so that the image has the same size as // the thumbnail const { thumbnailInfo } = this.props; if (thumbnailInfo?.width) { - await this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth }); + this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth }); } // Once the zoom is set, we the image is considered loaded and we can @@ -153,7 +153,7 @@ export default class ImageView extends React.Component { this.imageIsLoaded = true; this.animatingLoading = true; this.setZoomAndRotation(); - await this.setState({ + this.setState({ translationX: 0, translationY: 0, }); From 499b470d072426a69aabac1fbcd8d26d574ccbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 21 Sep 2021 17:34:50 +0200 Subject: [PATCH 12/14] Use CSS var in JS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_ImageView.scss | 6 +++++- src/components/views/elements/ImageView.tsx | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 6510cc2ec65..0f230f0f61f 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -18,6 +18,10 @@ $button-size: 32px; $icon-size: 22px; $button-gap: 24px; +:root { + --image-view-panel-height: 68px; +} + .mx_ImageView { display: flex; width: 100%; @@ -40,7 +44,7 @@ $button-gap: 24px; .mx_ImageView_panel { width: 100%; - height: 68px; + height: var(--image-view-panel-height); display: flex; justify-content: space-between; align-items: center; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 362d1c7da94..1b666eed99f 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -46,7 +46,11 @@ const ZOOM_COEFFICIENT = 0.0025; const ZOOM_DISTANCE = 10; // Height of mx_ImageView_panel -const PANEL_HEIGHT = 68; +const getPanelHeight = (): number => { + const value = getComputedStyle(document.documentElement).getPropertyValue("--image-view-panel-height"); + // Return the value as a number without the unit + return parseInt(value.slice(0, value.length - 2)); +}; interface IProps extends IDialogProps { src: string; // the source of the image being displayed @@ -103,7 +107,7 @@ export default class ImageView extends React.Component { thumbnailInfo?.positionY + (thumbnailInfo?.height / 2) - (UIStore.instance.windowHeight / 2) - - (PANEL_HEIGHT / 2) + (getPanelHeight() / 2) ) ?? 0, moving: false, contextMenuDisplayed: false, From afdec1971f43c900fa114356f4e2460e099d7e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 21 Sep 2021 17:42:23 +0200 Subject: [PATCH 13/14] Handle prefers-reduced-motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_animations.scss | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/res/css/_animations.scss b/res/css/_animations.scss index 89c406dd850..26252fcaf61 100644 --- a/res/css/_animations.scss +++ b/res/css/_animations.scss @@ -34,27 +34,12 @@ limitations under the License. transition: opacity 300ms ease; } - @keyframes mx--anim-pulse { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } } - -@media (prefers-reduced-motion) { - @keyframes mx--anim-pulse { - // Override all keyframes in reduced-motion - } - .mx_rtg--fade-enter-active { - transition: none; - } - .mx_rtg--fade-exit-active { - transition: none; - } -} - - @keyframes mx_Dialog_lightbox_background_keyframes { from { opacity: 0; @@ -64,7 +49,6 @@ limitations under the License. } } - @keyframes mx_ImageView_panel_keyframes { from { opacity: 0; @@ -73,3 +57,24 @@ limitations under the License. opacity: 1; } } + +@media (prefers-reduced-motion) { + @keyframes mx--anim-pulse { + // Override all keyframes in reduced-motion + } + + @keyframes mx_Dialog_lightbox_background_keyframes { + // Override all keyframes in reduced-motion + } + + @keyframes mx_ImageView_panel_keyframes { + // Override all keyframes in reduced-motion + } + + .mx_rtg--fade-enter-active { + transition: none; + } + .mx_rtg--fade-exit-active { + transition: none; + } +} From b2c0f57c4bae2893b106db5d0662add02a8ae793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 21 Sep 2021 17:59:13 +0200 Subject: [PATCH 14/14] More prefers-reduced-motion friendliness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_ImageView.scss | 18 ++++++++++++++++++ src/components/views/elements/ImageView.tsx | 11 +++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 0f230f0f61f..787d33ddc22 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -40,6 +40,14 @@ $button-gap: 24px; .mx_ImageView_image { flex-shrink: 0; + + &.mx_ImageView_image_animating { + transition: transform 200ms ease 0s; + } + + &.mx_ImageView_image_animatingLoading { + transition: transform 300ms ease 0s; + } } .mx_ImageView_panel { @@ -130,3 +138,13 @@ $button-gap: 24px; mask-size: 40%; } } + +@media (prefers-reduced-motion) { + .mx_ImageView_image_animating { + transition: none !important; + } + + .mx_ImageView_image_animatingLoading { + transition: none !important; + } +} diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 1b666eed99f..44ff6644d75 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -412,10 +412,10 @@ export default class ImageView extends React.Component { const showEventMeta = !!this.props.mxEvent; const zoomingDisabled = this.state.maxZoom === this.state.minZoom; - let transition; - if (this.animatingLoading) transition = "transform 300ms ease 0s"; - else if (this.state.moving || !this.imageIsLoaded) transition = null; - else transition = "transform 200ms ease 0s"; + let transitionClassName; + if (this.animatingLoading) transitionClassName = "mx_ImageView_image_animatingLoading"; + else if (this.state.moving || !this.imageIsLoaded) transitionClassName = ""; + else transitionClassName = "mx_ImageView_image_animating"; let cursor; if (this.state.moving) cursor = "grabbing"; @@ -433,7 +433,6 @@ export default class ImageView extends React.Component { // image causing it translate in the wrong direction. const style = { cursor: cursor, - transition: transition, transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) scale(${zoom}) @@ -581,7 +580,7 @@ export default class ImageView extends React.Component { style={style} alt={this.props.name} ref={this.image} - className="mx_ImageView_image" + className={`mx_ImageView_image ${transitionClassName}`} draggable={true} onMouseDown={this.onStartMoving} />