2018-04-11 23:58:04 +01:00
|
|
|
/*
|
|
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2019-07-06 10:42:14 +01:00
|
|
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
2021-04-09 08:01:14 +02:00
|
|
|
Copyright 2020, 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
2018-04-11 23:58:04 +01:00
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-02-24 19:17:33 +01:00
|
|
|
import React, { createRef } from 'react';
|
2021-10-22 17:23:32 -05:00
|
|
|
import FocusLock from "react-focus-lock";
|
|
|
|
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
|
|
|
|
2018-04-13 00:43:44 +01:00
|
|
|
import { _t } from '../../../languageHandler';
|
2021-02-24 14:43:33 +01:00
|
|
|
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
2021-06-07 15:43:43 +02:00
|
|
|
import { Key } from "../../../Keyboard";
|
2021-02-24 14:43:33 +01:00
|
|
|
import MemberAvatar from "../avatars/MemberAvatar";
|
2021-06-07 15:43:43 +02:00
|
|
|
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
2021-02-24 19:17:33 +01:00
|
|
|
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
2021-07-06 20:59:12 +02:00
|
|
|
import { aboveLeftOf } from '../../structures/ContextMenu';
|
2021-02-24 20:04:25 +01:00
|
|
|
import MessageTimestamp from "../messages/MessageTimestamp";
|
2021-02-24 20:07:41 +01:00
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2021-06-07 15:43:43 +02:00
|
|
|
import { formatFullDate } from "../../../DateUtils";
|
2021-02-24 20:14:12 +01:00
|
|
|
import dis from '../../../dispatcher/dispatcher';
|
2021-11-25 17:49:43 -03:00
|
|
|
import { Action } from '../../../dispatcher/actions';
|
2021-06-07 15:43:43 +02:00
|
|
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
2021-06-29 13:11:58 +01:00
|
|
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
2021-06-07 15:43:43 +02:00
|
|
|
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
2021-07-13 08:20:17 +02:00
|
|
|
import { IDialogProps } from '../dialogs/IDialogProps';
|
2021-07-22 21:26:15 +02:00
|
|
|
import UIStore from '../../../stores/UIStore';
|
2022-02-10 14:29:55 +00:00
|
|
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
2018-04-11 23:58:04 +01:00
|
|
|
|
2021-04-24 08:03:39 +02:00
|
|
|
// Max scale to keep gaps around the image
|
|
|
|
|
const MAX_SCALE = 0.95;
|
2021-03-15 19:28:21 +01:00
|
|
|
// This is used for the buttons
|
2021-04-24 10:35:25 +02:00
|
|
|
const ZOOM_STEP = 0.10;
|
2021-03-15 19:28:21 +01:00
|
|
|
// This is used for mouse wheel events
|
2021-04-24 10:35:25 +02:00
|
|
|
const ZOOM_COEFFICIENT = 0.0025;
|
2021-04-03 16:19:22 +02:00
|
|
|
// If we have moved only this much we can zoom
|
|
|
|
|
const ZOOM_DISTANCE = 10;
|
2021-02-25 11:19:50 +01:00
|
|
|
|
2021-09-14 18:06:01 +02:00
|
|
|
// Height of mx_ImageView_panel
|
2021-09-21 17:34:50 +02:00
|
|
|
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));
|
|
|
|
|
};
|
2021-07-22 21:52:36 +02:00
|
|
|
|
2021-07-13 08:20:17 +02:00
|
|
|
interface IProps extends IDialogProps {
|
2021-07-01 23:23:03 +01:00
|
|
|
src: string; // the source of the image being displayed
|
|
|
|
|
name?: string; // the main title ('name') for the image
|
|
|
|
|
link?: string; // the link (if any) applied to the name of the image
|
|
|
|
|
width?: number; // width of the image src in pixels
|
|
|
|
|
height?: number; // height of the image src in pixels
|
|
|
|
|
fileSize?: number; // size of the image src in bytes
|
2021-04-03 15:06:27 +02:00
|
|
|
|
|
|
|
|
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
|
|
|
|
// 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.
|
2021-07-22 21:26:15 +02:00
|
|
|
mxEvent?: MatrixEvent;
|
|
|
|
|
permalinkCreator?: RoomPermalinkCreator;
|
|
|
|
|
|
|
|
|
|
thumbnailInfo?: {
|
|
|
|
|
positionX: number;
|
|
|
|
|
positionY: number;
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
};
|
2021-04-03 15:06:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IState {
|
2021-07-01 23:23:03 +01:00
|
|
|
zoom: number;
|
|
|
|
|
minZoom: number;
|
|
|
|
|
maxZoom: number;
|
|
|
|
|
rotation: number;
|
|
|
|
|
translationX: number;
|
|
|
|
|
translationY: number;
|
|
|
|
|
moving: boolean;
|
|
|
|
|
contextMenuDisplayed: boolean;
|
2021-04-03 15:06:27 +02:00
|
|
|
}
|
|
|
|
|
|
2021-03-08 19:59:41 -07:00
|
|
|
@replaceableComponent("views.elements.ImageView")
|
2021-04-03 15:06:27 +02:00
|
|
|
export default class ImageView extends React.Component<IProps, IState> {
|
2019-03-29 23:31:15 -07:00
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
2021-07-22 21:26:15 +02:00
|
|
|
|
2021-07-23 13:52:29 +02:00
|
|
|
const { thumbnailInfo } = this.props;
|
2021-07-22 21:26:15 +02:00
|
|
|
|
2020-12-19 10:20:15 +01:00
|
|
|
this.state = {
|
2021-07-23 13:52:29 +02:00
|
|
|
zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize
|
2021-04-24 08:32:28 +02:00
|
|
|
minZoom: MAX_SCALE,
|
2021-04-24 09:24:25 +02:00
|
|
|
maxZoom: MAX_SCALE,
|
2020-12-20 17:40:16 +01:00
|
|
|
rotation: 0,
|
2021-07-22 21:52:36 +02:00
|
|
|
translationX: (
|
|
|
|
|
thumbnailInfo?.positionX +
|
|
|
|
|
(thumbnailInfo?.width / 2) -
|
|
|
|
|
(UIStore.instance.windowWidth / 2)
|
2021-07-23 10:07:09 +02:00
|
|
|
) ?? 0,
|
2021-07-22 21:52:36 +02:00
|
|
|
translationY: (
|
|
|
|
|
thumbnailInfo?.positionY +
|
|
|
|
|
(thumbnailInfo?.height / 2) -
|
|
|
|
|
(UIStore.instance.windowHeight / 2) -
|
2021-09-21 17:34:50 +02:00
|
|
|
(getPanelHeight() / 2)
|
2021-07-23 10:07:09 +02:00
|
|
|
) ?? 0,
|
2020-12-20 17:40:16 +01:00
|
|
|
moving: false,
|
2021-02-25 11:16:40 +01:00
|
|
|
contextMenuDisplayed: false,
|
2020-12-19 10:20:15 +01:00
|
|
|
};
|
2019-03-29 23:31:15 -07:00
|
|
|
}
|
2018-04-11 23:58:04 +01:00
|
|
|
|
2021-04-09 08:03:10 +02:00
|
|
|
// XXX: Refs to functional components
|
2021-04-09 08:02:38 +02:00
|
|
|
private contextMenuButton = createRef<any>();
|
|
|
|
|
private focusLock = createRef<any>();
|
2021-04-26 13:11:41 +02:00
|
|
|
private imageWrapper = createRef<HTMLDivElement>();
|
2021-04-26 13:30:14 +02:00
|
|
|
private image = createRef<HTMLImageElement>();
|
2021-04-03 15:06:27 +02:00
|
|
|
|
2021-04-09 08:02:38 +02:00
|
|
|
private initX = 0;
|
|
|
|
|
private initY = 0;
|
|
|
|
|
private previousX = 0;
|
|
|
|
|
private previousY = 0;
|
2020-12-20 17:40:16 +01:00
|
|
|
|
2021-07-23 13:52:29 +02:00
|
|
|
private animatingLoading = false;
|
|
|
|
|
private imageIsLoaded = false;
|
2021-07-23 08:00:51 +02:00
|
|
|
|
2020-12-19 13:30:56 +01:00
|
|
|
componentDidMount() {
|
2021-02-25 07:51:38 +01:00
|
|
|
// We have to use addEventListener() because the listener
|
|
|
|
|
// needs to be passive in order to work with Chromium
|
2021-04-03 15:06:27 +02:00
|
|
|
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
2021-04-26 13:49:29 +02:00
|
|
|
// We want to recalculate zoom whenever the window's size changes
|
2021-06-07 15:39:11 +02:00
|
|
|
window.addEventListener("resize", this.recalculateZoom);
|
2021-04-26 13:30:14 +02:00
|
|
|
// After the image loads for the first time we want to calculate the zoom
|
2021-07-22 21:26:15 +02:00
|
|
|
this.image.current.addEventListener("load", this.imageLoaded);
|
2020-12-19 13:30:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2021-04-03 15:06:27 +02:00
|
|
|
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
2021-06-07 15:39:11 +02:00
|
|
|
window.removeEventListener("resize", this.recalculateZoom);
|
2021-07-22 21:26:15 +02:00
|
|
|
this.image.current.removeEventListener("load", this.imageLoaded);
|
2020-12-19 13:30:56 +01:00
|
|
|
}
|
|
|
|
|
|
2021-09-14 18:15:27 +02:00
|
|
|
private imageLoaded = () => {
|
2021-07-23 13:52:29 +02:00
|
|
|
// First, we calculate the zoom, so that the image has the same size as
|
|
|
|
|
// the thumbnail
|
|
|
|
|
const { thumbnailInfo } = this.props;
|
|
|
|
|
if (thumbnailInfo?.width) {
|
2021-09-14 18:15:27 +02:00
|
|
|
this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth });
|
2021-07-23 13:52:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
2021-07-22 21:26:15 +02:00
|
|
|
this.setZoomAndRotation();
|
2021-09-14 18:15:27 +02:00
|
|
|
this.setState({
|
2021-07-22 21:26:15 +02:00
|
|
|
translationX: 0,
|
|
|
|
|
translationY: 0,
|
|
|
|
|
});
|
2021-07-23 13:52:29 +02:00
|
|
|
|
|
|
|
|
// Once the position is set, there is no need to animate anymore
|
|
|
|
|
this.animatingLoading = false;
|
2021-07-22 21:26:15 +02:00
|
|
|
};
|
|
|
|
|
|
2021-06-07 15:39:11 +02:00
|
|
|
private recalculateZoom = () => {
|
|
|
|
|
this.setZoomAndRotation();
|
2021-06-29 13:11:58 +01:00
|
|
|
};
|
2021-06-07 15:39:11 +02:00
|
|
|
|
|
|
|
|
private setZoomAndRotation = (inputRotation?: number) => {
|
2021-04-26 13:30:14 +02:00
|
|
|
const image = this.image.current;
|
2021-04-26 13:11:41 +02:00
|
|
|
const imageWrapper = this.imageWrapper.current;
|
2021-04-24 09:41:46 +02:00
|
|
|
|
2021-07-03 15:18:47 -04:00
|
|
|
const rotation = inputRotation ?? this.state.rotation;
|
2021-06-07 15:39:11 +02:00
|
|
|
|
2021-06-07 15:46:03 +02:00
|
|
|
const imageIsNotFlipped = rotation % 180 === 0;
|
2021-06-06 08:51:07 +02:00
|
|
|
|
|
|
|
|
// If the image is rotated take it into account
|
2021-06-07 15:46:03 +02:00
|
|
|
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
|
|
|
|
|
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
|
2021-06-06 08:51:07 +02:00
|
|
|
|
|
|
|
|
const zoomX = imageWrapper.clientWidth / width;
|
|
|
|
|
const zoomY = imageWrapper.clientHeight / height;
|
2021-04-26 13:47:06 +02:00
|
|
|
|
|
|
|
|
// If the image is smaller in both dimensions set its the zoom to 1 to
|
|
|
|
|
// display it in its original size
|
|
|
|
|
if (zoomX >= 1 && zoomY >= 1) {
|
|
|
|
|
this.setState({
|
|
|
|
|
zoom: 1,
|
|
|
|
|
minZoom: 1,
|
|
|
|
|
maxZoom: 1,
|
2021-06-07 15:39:11 +02:00
|
|
|
rotation: rotation,
|
2021-04-26 13:47:06 +02:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-04-24 09:41:46 +02:00
|
|
|
// We set minZoom to the min of the zoomX and zoomY to avoid overflow in
|
|
|
|
|
// any direction. We also multiply by MAX_SCALE to get a gap around the
|
|
|
|
|
// image by default
|
2021-04-24 08:32:28 +02:00
|
|
|
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
2021-04-24 08:03:39 +02:00
|
|
|
|
2021-06-07 15:26:54 +02:00
|
|
|
// If zoom is smaller than minZoom don't go below that value
|
2021-06-06 08:56:12 +02:00
|
|
|
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
|
|
|
|
|
|
2021-04-24 08:32:28 +02:00
|
|
|
this.setState({
|
|
|
|
|
minZoom: minZoom,
|
2021-04-26 13:47:06 +02:00
|
|
|
maxZoom: 1,
|
2021-06-07 15:39:11 +02:00
|
|
|
rotation: rotation,
|
2021-06-06 08:56:12 +02:00
|
|
|
zoom: zoom,
|
2021-04-24 08:32:28 +02:00
|
|
|
});
|
2021-06-29 13:11:58 +01:00
|
|
|
};
|
2021-04-24 08:03:39 +02:00
|
|
|
|
2021-07-22 09:48:56 -04:00
|
|
|
private zoomDelta(delta: number, anchorX?: number, anchorY?: number) {
|
|
|
|
|
this.zoom(this.state.zoom + delta, anchorX, anchorY);
|
2021-07-21 02:13:30 -04:00
|
|
|
}
|
|
|
|
|
|
2021-07-22 09:48:56 -04:00
|
|
|
private zoom(zoomLevel: number, anchorX?: number, anchorY?: number) {
|
2021-07-21 02:13:30 -04:00
|
|
|
const oldZoom = this.state.zoom;
|
|
|
|
|
const newZoom = Math.min(zoomLevel, this.state.maxZoom);
|
2020-12-20 17:40:16 +01:00
|
|
|
|
2021-04-24 08:32:28 +02:00
|
|
|
if (newZoom <= this.state.minZoom) {
|
2021-07-21 02:13:30 -04:00
|
|
|
// Zoom out fully
|
2020-12-19 10:20:15 +01:00
|
|
|
this.setState({
|
2021-04-24 08:32:28 +02:00
|
|
|
zoom: this.state.minZoom,
|
2020-12-20 20:09:01 +01:00
|
|
|
translationX: 0,
|
|
|
|
|
translationY: 0,
|
2020-12-19 10:20:15 +01:00
|
|
|
});
|
2021-07-22 09:48:56 -04:00
|
|
|
} else if (typeof anchorX !== "number" && typeof anchorY !== "number") {
|
2021-07-21 02:13:30 -04:00
|
|
|
// Zoom relative to the center of the view
|
|
|
|
|
this.setState({
|
|
|
|
|
zoom: newZoom,
|
|
|
|
|
translationX: this.state.translationX * newZoom / oldZoom,
|
|
|
|
|
translationY: this.state.translationY * newZoom / oldZoom,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Zoom relative to the given point on the image.
|
|
|
|
|
// First we need to figure out the offset of the anchor point
|
|
|
|
|
// relative to the center of the image, accounting for rotation.
|
|
|
|
|
let offsetX;
|
|
|
|
|
let offsetY;
|
2021-07-22 09:56:26 -04:00
|
|
|
// The modulo operator can return negative values for some
|
|
|
|
|
// rotations, so we have to do some extra work to normalize it
|
2021-07-21 02:13:30 -04:00
|
|
|
switch (((this.state.rotation % 360) + 360) % 360) {
|
|
|
|
|
case 0:
|
2021-07-22 09:48:56 -04:00
|
|
|
offsetX = this.image.current.clientWidth / 2 - anchorX;
|
|
|
|
|
offsetY = this.image.current.clientHeight / 2 - anchorY;
|
2021-07-21 02:13:30 -04:00
|
|
|
break;
|
|
|
|
|
case 90:
|
2021-07-22 09:48:56 -04:00
|
|
|
offsetX = anchorY - this.image.current.clientHeight / 2;
|
|
|
|
|
offsetY = this.image.current.clientWidth / 2 - anchorX;
|
2021-07-21 02:13:30 -04:00
|
|
|
break;
|
|
|
|
|
case 180:
|
2021-07-22 09:48:56 -04:00
|
|
|
offsetX = anchorX - this.image.current.clientWidth / 2;
|
|
|
|
|
offsetY = anchorY - this.image.current.clientHeight / 2;
|
2021-07-21 02:13:30 -04:00
|
|
|
break;
|
|
|
|
|
case 270:
|
2021-07-22 09:48:56 -04:00
|
|
|
offsetX = this.image.current.clientHeight / 2 - anchorY;
|
|
|
|
|
offsetY = anchorX - this.image.current.clientWidth / 2;
|
2021-07-21 02:13:30 -04:00
|
|
|
}
|
2020-12-20 20:28:19 +01:00
|
|
|
|
2021-07-21 02:13:30 -04:00
|
|
|
// Apply the zoom and offset
|
|
|
|
|
this.setState({
|
|
|
|
|
zoom: newZoom,
|
|
|
|
|
translationX: this.state.translationX + (newZoom - oldZoom) * offsetX,
|
|
|
|
|
translationY: this.state.translationY + (newZoom - oldZoom) * offsetY,
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-04-24 10:36:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onWheel = (ev: WheelEvent) => {
|
2021-07-21 02:13:30 -04:00
|
|
|
if (ev.target === this.image.current) {
|
2021-07-22 09:46:29 -04:00
|
|
|
ev.stopPropagation();
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
|
|
|
|
const { deltaY } = normalizeWheelEvent(ev);
|
2021-07-21 02:13:30 -04:00
|
|
|
// Zoom in on the point on the image targeted by the cursor
|
|
|
|
|
this.zoomDelta(-deltaY * ZOOM_COEFFICIENT, ev.offsetX, ev.offsetY);
|
|
|
|
|
}
|
2021-04-24 10:36:53 +02:00
|
|
|
};
|
|
|
|
|
|
2021-07-21 02:42:30 -04:00
|
|
|
private onZoomInClick = () => {
|
2021-07-21 02:13:30 -04:00
|
|
|
this.zoomDelta(ZOOM_STEP);
|
2021-04-24 10:36:53 +02:00
|
|
|
};
|
|
|
|
|
|
2021-07-21 02:42:30 -04:00
|
|
|
private onZoomOutClick = () => {
|
2021-07-21 02:13:30 -04:00
|
|
|
this.zoomDelta(-ZOOM_STEP);
|
2021-04-24 10:36:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private onKeyDown = (ev: KeyboardEvent) => {
|
|
|
|
|
if (ev.key === Key.ESCAPE) {
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
this.props.onFinished();
|
|
|
|
|
}
|
2021-04-03 15:06:27 +02:00
|
|
|
};
|
2020-12-19 10:20:15 +01:00
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onRotateCounterClockwiseClick = () => {
|
2020-12-20 17:40:16 +01:00
|
|
|
const cur = this.state.rotation;
|
2021-06-07 15:39:11 +02:00
|
|
|
this.setZoomAndRotation(cur - 90);
|
2019-03-29 23:31:15 -07:00
|
|
|
};
|
|
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onRotateClockwiseClick = () => {
|
2020-12-20 17:40:16 +01:00
|
|
|
const cur = this.state.rotation;
|
2021-06-07 15:39:11 +02:00
|
|
|
this.setZoomAndRotation(cur + 90);
|
2019-03-29 23:31:15 -07:00
|
|
|
};
|
|
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onDownloadClick = () => {
|
2021-02-24 16:28:12 +01:00
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = this.props.src;
|
|
|
|
|
a.download = this.props.name;
|
2021-04-13 08:15:42 +02:00
|
|
|
a.target = "_blank";
|
2021-05-13 20:55:14 -06:00
|
|
|
a.rel = "noreferrer noopener";
|
2021-02-24 16:28:12 +01:00
|
|
|
a.click();
|
2021-04-03 15:06:27 +02:00
|
|
|
};
|
2021-02-24 16:28:12 +01:00
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onOpenContextMenu = () => {
|
2021-02-24 19:17:33 +01:00
|
|
|
this.setState({
|
2021-02-25 11:16:40 +01:00
|
|
|
contextMenuDisplayed: true,
|
2021-02-24 19:17:33 +01:00
|
|
|
});
|
2021-04-03 15:06:27 +02:00
|
|
|
};
|
2021-02-24 19:17:33 +01:00
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onCloseContextMenu = () => {
|
2021-02-24 19:17:33 +01:00
|
|
|
this.setState({
|
2021-02-25 11:16:40 +01:00
|
|
|
contextMenuDisplayed: false,
|
2021-02-24 19:17:33 +01:00
|
|
|
});
|
2021-04-03 15:06:27 +02:00
|
|
|
};
|
2021-02-24 19:17:33 +01:00
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onPermalinkClicked = (ev: React.MouseEvent) => {
|
2021-02-24 20:14:12 +01:00
|
|
|
// This allows the permalink to be opened in a new tab/window or copied as
|
|
|
|
|
// matrix.to, but also for it to enable routing within Element when clicked.
|
2021-02-25 11:23:14 +01:00
|
|
|
ev.preventDefault();
|
2022-02-10 14:29:55 +00:00
|
|
|
dis.dispatch<ViewRoomPayload>({
|
2021-11-25 17:49:43 -03:00
|
|
|
action: Action.ViewRoom,
|
2021-02-24 20:14:12 +01:00
|
|
|
event_id: this.props.mxEvent.getId(),
|
|
|
|
|
highlighted: true,
|
|
|
|
|
room_id: this.props.mxEvent.getRoomId(),
|
2022-02-10 14:29:55 +00:00
|
|
|
_trigger: undefined, // room doesn't change
|
2021-02-24 20:14:12 +01:00
|
|
|
});
|
2021-02-25 08:13:27 +01:00
|
|
|
this.props.onFinished();
|
2021-02-24 20:14:12 +01:00
|
|
|
};
|
|
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onStartMoving = (ev: React.MouseEvent) => {
|
2020-12-20 17:40:16 +01:00
|
|
|
ev.stopPropagation();
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
2021-04-15 08:10:03 +02:00
|
|
|
// Don't do anything if we pressed any
|
|
|
|
|
// other button than the left one
|
|
|
|
|
if (ev.button !== 0) return;
|
|
|
|
|
|
2021-04-02 10:37:42 +02:00
|
|
|
// Zoom in if we are completely zoomed out
|
2021-04-24 08:32:28 +02:00
|
|
|
if (this.state.zoom === this.state.minZoom) {
|
2021-07-21 02:13:30 -04:00
|
|
|
this.zoom(this.state.maxZoom, ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
2021-04-02 10:37:42 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-07 15:43:43 +02:00
|
|
|
this.setState({ moving: true });
|
2021-04-02 10:01:41 +02:00
|
|
|
this.previousX = this.state.translationX;
|
|
|
|
|
this.previousY = this.state.translationY;
|
2021-06-06 08:14:43 +02:00
|
|
|
this.initX = ev.pageX - this.state.translationX;
|
|
|
|
|
this.initY = ev.pageY - this.state.translationY;
|
2021-04-03 15:06:27 +02:00
|
|
|
};
|
2020-12-20 17:40:16 +01:00
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onMoving = (ev: React.MouseEvent) => {
|
2020-12-20 17:40:16 +01:00
|
|
|
ev.stopPropagation();
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
2021-04-02 10:22:10 +02:00
|
|
|
if (!this.state.moving) return;
|
2020-12-20 17:40:16 +01:00
|
|
|
|
|
|
|
|
this.setState({
|
2021-06-06 08:14:43 +02:00
|
|
|
translationX: ev.pageX - this.initX,
|
|
|
|
|
translationY: ev.pageY - this.initY,
|
2020-12-20 17:40:16 +01:00
|
|
|
});
|
2021-04-03 15:06:27 +02:00
|
|
|
};
|
2020-12-20 17:40:16 +01:00
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private onEndMoving = () => {
|
2021-04-02 10:37:42 +02:00
|
|
|
// Zoom out if we haven't moved much
|
2021-04-02 10:01:41 +02:00
|
|
|
if (
|
|
|
|
|
this.state.moving === true &&
|
2021-04-03 16:19:22 +02:00
|
|
|
Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE &&
|
|
|
|
|
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
|
2021-04-02 10:01:41 +02:00
|
|
|
) {
|
2021-07-21 02:13:30 -04:00
|
|
|
this.zoom(this.state.minZoom);
|
2021-06-06 08:09:41 +02:00
|
|
|
this.initX = 0;
|
|
|
|
|
this.initY = 0;
|
2021-04-02 10:01:41 +02:00
|
|
|
}
|
2021-06-07 15:43:43 +02:00
|
|
|
this.setState({ moving: false });
|
2021-04-03 15:06:27 +02:00
|
|
|
};
|
2020-12-20 17:40:16 +01:00
|
|
|
|
2021-04-03 15:06:27 +02:00
|
|
|
private renderContextMenu() {
|
2021-02-24 19:17:33 +01:00
|
|
|
let contextMenu = null;
|
2021-02-25 11:16:40 +01:00
|
|
|
if (this.state.contextMenuDisplayed) {
|
2021-02-24 19:17:33 +01:00
|
|
|
contextMenu = (
|
2021-07-06 20:59:12 +02:00
|
|
|
<MessageContextMenu
|
2021-02-24 19:17:33 +01:00
|
|
|
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())}
|
2021-07-06 20:59:12 +02:00
|
|
|
mxEvent={this.props.mxEvent}
|
|
|
|
|
permalinkCreator={this.props.permalinkCreator}
|
2021-02-24 19:17:33 +01:00
|
|
|
onFinished={this.onCloseContextMenu}
|
2021-07-06 20:59:12 +02:00
|
|
|
onCloseDialog={this.props.onFinished}
|
|
|
|
|
/>
|
2021-02-24 19:17:33 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<React.Fragment>
|
|
|
|
|
{ contextMenu }
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-29 23:31:15 -07:00
|
|
|
render() {
|
2020-12-20 17:40:16 +01:00
|
|
|
const showEventMeta = !!this.props.mxEvent;
|
2021-04-26 15:47:58 +02:00
|
|
|
const zoomingDisabled = this.state.maxZoom === this.state.minZoom;
|
2018-04-11 23:58:04 +01:00
|
|
|
|
2021-09-21 17:59:13 +02:00
|
|
|
let transitionClassName;
|
|
|
|
|
if (this.animatingLoading) transitionClassName = "mx_ImageView_image_animatingLoading";
|
|
|
|
|
else if (this.state.moving || !this.imageIsLoaded) transitionClassName = "";
|
|
|
|
|
else transitionClassName = "mx_ImageView_image_animating";
|
2021-07-23 08:00:51 +02:00
|
|
|
|
2021-04-02 10:01:41 +02:00
|
|
|
let cursor;
|
2021-07-23 08:00:51 +02:00
|
|
|
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";
|
|
|
|
|
|
2020-12-20 17:40:16 +01:00
|
|
|
const rotationDegrees = this.state.rotation + "deg";
|
2021-04-24 08:35:45 +02:00
|
|
|
const zoom = this.state.zoom;
|
2020-12-20 17:40:16 +01:00
|
|
|
const translatePixelsX = this.state.translationX + "px";
|
|
|
|
|
const translatePixelsY = this.state.translationY + "px";
|
2021-02-25 07:51:38 +01:00
|
|
|
// The order of the values is important!
|
|
|
|
|
// First, we translate and only then we rotate, otherwise
|
|
|
|
|
// we would apply the translation to an already rotated
|
|
|
|
|
// image causing it translate in the wrong direction.
|
2020-12-20 17:40:16 +01:00
|
|
|
const style = {
|
2021-04-02 10:01:41 +02:00
|
|
|
cursor: cursor,
|
2020-12-20 20:00:11 +01:00
|
|
|
transform: `translateX(${translatePixelsX})
|
|
|
|
|
translateY(${translatePixelsY})
|
2021-04-24 08:35:45 +02:00
|
|
|
scale(${zoom})
|
2020-12-20 20:00:11 +01:00
|
|
|
rotate(${rotationDegrees})`,
|
2020-12-20 17:40:16 +01:00
|
|
|
};
|
2019-03-29 23:31:15 -07:00
|
|
|
|
2021-02-24 18:24:44 +01:00
|
|
|
let info;
|
2021-02-24 20:04:25 +01:00
|
|
|
if (showEventMeta) {
|
|
|
|
|
const mxEvent = this.props.mxEvent;
|
2021-02-24 20:07:41 +01:00
|
|
|
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
2021-02-24 20:14:12 +01:00
|
|
|
let permalink = "#";
|
|
|
|
|
if (this.props.permalinkCreator) {
|
|
|
|
|
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
|
|
|
|
}
|
2021-02-24 20:04:25 +01:00
|
|
|
|
|
|
|
|
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
2021-04-02 08:31:42 +02:00
|
|
|
const sender = (
|
|
|
|
|
<div className="mx_ImageView_info_sender">
|
2021-06-07 16:12:06 +02:00
|
|
|
{ senderName }
|
2021-04-02 08:31:42 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
2021-02-24 20:04:25 +01:00
|
|
|
const messageTimestamp = (
|
2021-02-24 20:14:12 +01:00
|
|
|
<a
|
|
|
|
|
href={permalink}
|
|
|
|
|
onClick={this.onPermalinkClicked}
|
2021-04-03 15:06:27 +02:00
|
|
|
aria-label={formatFullDate(new Date(this.props.mxEvent.getTs()), showTwelveHour, false)}
|
2021-02-24 20:14:12 +01:00
|
|
|
>
|
2021-04-03 15:06:27 +02:00
|
|
|
<MessageTimestamp
|
|
|
|
|
showFullDate={true}
|
|
|
|
|
showTwelveHour={showTwelveHour}
|
|
|
|
|
ts={mxEvent.getTs()}
|
|
|
|
|
showSeconds={false}
|
|
|
|
|
/>
|
2021-02-24 20:14:12 +01:00
|
|
|
</a>
|
2021-02-24 20:04:25 +01:00
|
|
|
);
|
|
|
|
|
const avatar = (
|
|
|
|
|
<MemberAvatar
|
|
|
|
|
member={mxEvent.sender}
|
2021-09-01 16:50:13 +02:00
|
|
|
fallbackUserId={mxEvent.getSender()}
|
2021-07-23 10:23:45 +01:00
|
|
|
width={32}
|
|
|
|
|
height={32}
|
2021-02-24 20:04:25 +01:00
|
|
|
viewUserOnClick={true}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
2021-02-24 18:24:44 +01:00
|
|
|
info = (
|
|
|
|
|
<div className="mx_ImageView_info_wrapper">
|
2021-06-07 16:12:06 +02:00
|
|
|
{ avatar }
|
2021-02-24 18:24:44 +01:00
|
|
|
<div className="mx_ImageView_info">
|
2021-06-07 16:12:06 +02:00
|
|
|
{ sender }
|
|
|
|
|
{ messageTimestamp }
|
2021-02-24 18:24:44 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// If there is no event - we're viewing an avatar, we set
|
|
|
|
|
// an empty div here, since the panel uses space-between
|
|
|
|
|
// and we want the same placement of elements
|
|
|
|
|
info = (
|
2021-07-23 10:23:45 +01:00
|
|
|
<div />
|
2021-02-24 18:24:44 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-13 08:11:09 +02:00
|
|
|
let contextMenuButton;
|
|
|
|
|
if (this.props.mxEvent) {
|
|
|
|
|
contextMenuButton = (
|
|
|
|
|
<ContextMenuTooltipButton
|
|
|
|
|
className="mx_ImageView_button mx_ImageView_button_more"
|
|
|
|
|
title={_t("Options")}
|
|
|
|
|
onClick={this.onOpenContextMenu}
|
|
|
|
|
inputRef={this.contextMenuButton}
|
|
|
|
|
isExpanded={this.state.contextMenuDisplayed}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-26 15:47:58 +02:00
|
|
|
let zoomOutButton;
|
|
|
|
|
let zoomInButton;
|
|
|
|
|
if (!zoomingDisabled) {
|
|
|
|
|
zoomOutButton = (
|
|
|
|
|
<AccessibleTooltipButton
|
|
|
|
|
className="mx_ImageView_button mx_ImageView_button_zoomOut"
|
|
|
|
|
title={_t("Zoom out")}
|
2021-07-23 10:23:45 +01:00
|
|
|
onClick={this.onZoomOutClick}
|
|
|
|
|
/>
|
2021-04-26 15:47:58 +02:00
|
|
|
);
|
|
|
|
|
zoomInButton = (
|
|
|
|
|
<AccessibleTooltipButton
|
|
|
|
|
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
|
|
|
|
title={_t("Zoom in")}
|
2021-07-23 10:23:45 +01:00
|
|
|
onClick={this.onZoomInClick}
|
|
|
|
|
/>
|
2021-04-26 15:47:58 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-11 23:58:04 +01:00
|
|
|
return (
|
2020-05-29 01:44:09 +01:00
|
|
|
<FocusLock
|
|
|
|
|
returnFocus={true}
|
|
|
|
|
lockProps={{
|
|
|
|
|
onKeyDown: this.onKeyDown,
|
|
|
|
|
role: "dialog",
|
|
|
|
|
}}
|
|
|
|
|
className="mx_ImageView"
|
2021-04-03 15:06:27 +02:00
|
|
|
ref={this.focusLock}
|
2020-05-29 01:44:09 +01:00
|
|
|
>
|
2021-04-03 15:06:27 +02:00
|
|
|
<div className="mx_ImageView_panel">
|
2021-06-07 16:12:06 +02:00
|
|
|
{ info }
|
2021-04-03 15:06:27 +02:00
|
|
|
<div className="mx_ImageView_toolbar">
|
2021-07-13 20:28:49 +02:00
|
|
|
{ zoomOutButton }
|
|
|
|
|
{ zoomInButton }
|
2021-04-03 15:06:27 +02:00
|
|
|
<AccessibleTooltipButton
|
|
|
|
|
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
|
|
|
|
title={_t("Rotate Left")}
|
2021-07-23 10:23:45 +01:00
|
|
|
onClick={this.onRotateCounterClockwiseClick}
|
|
|
|
|
/>
|
2021-05-14 08:20:07 +05:30
|
|
|
<AccessibleTooltipButton
|
|
|
|
|
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
|
|
|
|
title={_t("Rotate Right")}
|
2021-07-23 10:23:45 +01:00
|
|
|
onClick={this.onRotateClockwiseClick}
|
|
|
|
|
/>
|
2021-04-03 15:06:27 +02:00
|
|
|
<AccessibleTooltipButton
|
|
|
|
|
className="mx_ImageView_button mx_ImageView_button_download"
|
|
|
|
|
title={_t("Download")}
|
2021-07-23 10:23:45 +01:00
|
|
|
onClick={this.onDownloadClick}
|
|
|
|
|
/>
|
2021-06-07 16:12:06 +02:00
|
|
|
{ contextMenuButton }
|
2021-04-03 15:06:27 +02:00
|
|
|
<AccessibleTooltipButton
|
|
|
|
|
className="mx_ImageView_button mx_ImageView_button_close"
|
|
|
|
|
title={_t("Close")}
|
2021-07-23 10:23:45 +01:00
|
|
|
onClick={this.props.onFinished}
|
|
|
|
|
/>
|
2021-06-07 16:12:06 +02:00
|
|
|
{ this.renderContextMenu() }
|
2021-02-24 11:15:59 +01:00
|
|
|
</div>
|
2021-04-03 15:06:27 +02:00
|
|
|
</div>
|
2021-04-26 13:11:41 +02:00
|
|
|
<div
|
|
|
|
|
className="mx_ImageView_image_wrapper"
|
2021-06-05 21:08:44 -04:00
|
|
|
ref={this.imageWrapper}
|
|
|
|
|
onMouseDown={this.props.onFinished}
|
|
|
|
|
onMouseMove={this.onMoving}
|
|
|
|
|
onMouseUp={this.onEndMoving}
|
|
|
|
|
onMouseLeave={this.onEndMoving}
|
|
|
|
|
>
|
2021-04-03 15:06:27 +02:00
|
|
|
<img
|
|
|
|
|
src={this.props.src}
|
|
|
|
|
style={style}
|
2021-07-17 08:21:46 +02:00
|
|
|
alt={this.props.name}
|
2021-04-26 13:30:14 +02:00
|
|
|
ref={this.image}
|
2021-09-21 17:59:13 +02:00
|
|
|
className={`mx_ImageView_image ${transitionClassName}`}
|
2021-04-03 15:06:27 +02:00
|
|
|
draggable={true}
|
|
|
|
|
onMouseDown={this.onStartMoving}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2020-05-29 01:44:09 +01:00
|
|
|
</FocusLock>
|
2018-04-11 23:58:04 +01:00
|
|
|
);
|
2019-03-29 23:31:15 -07:00
|
|
|
}
|
|
|
|
|
}
|