2021-08-23 23:14:53 +00:00
|
|
|
// Copyright 2018-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
import React, {
|
2021-09-28 20:27:35 +00:00
|
|
|
CSSProperties,
|
2021-08-23 23:14:53 +00:00
|
|
|
ReactNode,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from 'react';
|
2018-04-15 04:27:30 +00:00
|
|
|
import classNames from 'classnames';
|
2021-08-24 21:47:14 +00:00
|
|
|
import moment from 'moment';
|
2021-08-23 23:14:53 +00:00
|
|
|
import { createPortal } from 'react-dom';
|
2021-08-24 21:47:14 +00:00
|
|
|
import { noop } from 'lodash';
|
2018-04-25 22:15:57 +00:00
|
|
|
|
|
|
|
import * as GoogleChrome from '../util/GoogleChrome';
|
2021-08-23 23:14:53 +00:00
|
|
|
import { AttachmentType, isGIF } from '../types/Attachment';
|
|
|
|
import { Avatar, AvatarSize } from './Avatar';
|
|
|
|
import { ConversationType } from '../state/ducks/conversations';
|
|
|
|
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
|
2019-01-14 21:49:58 +00:00
|
|
|
import { LocalizerType } from '../types/Util';
|
2021-08-23 23:14:53 +00:00
|
|
|
import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
|
2021-08-24 21:47:14 +00:00
|
|
|
import { formatDuration } from '../util/formatDuration';
|
2021-09-17 22:24:21 +00:00
|
|
|
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
2021-09-17 18:27:53 +00:00
|
|
|
import * as log from '../logging/log';
|
2018-05-22 19:31:43 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
export type PropsType = {
|
2021-08-06 00:17:05 +00:00
|
|
|
children?: ReactNode;
|
2018-04-15 03:27:03 +00:00
|
|
|
close: () => void;
|
2021-08-23 23:14:53 +00:00
|
|
|
getConversation?: (id: string) => ConversationType;
|
2019-01-14 21:49:58 +00:00
|
|
|
i18n: LocalizerType;
|
2021-08-24 21:47:14 +00:00
|
|
|
isViewOnce?: boolean;
|
2021-08-23 23:14:53 +00:00
|
|
|
media: Array<MediaItemType>;
|
|
|
|
onForward?: (messageId: string) => void;
|
|
|
|
onSave?: (options: {
|
|
|
|
attachment: AttachmentType;
|
|
|
|
message: MessageAttributesType;
|
|
|
|
index: number;
|
|
|
|
}) => void;
|
|
|
|
selectedIndex?: number;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-04-15 03:27:03 +00:00
|
|
|
|
2021-09-28 20:27:35 +00:00
|
|
|
enum ZoomType {
|
|
|
|
None,
|
|
|
|
FillScreen,
|
|
|
|
ZoomAndPan,
|
|
|
|
}
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
export function Lightbox({
|
|
|
|
children,
|
|
|
|
close,
|
|
|
|
getConversation,
|
|
|
|
media,
|
|
|
|
i18n,
|
2021-08-24 21:47:14 +00:00
|
|
|
isViewOnce = false,
|
2021-08-23 23:14:53 +00:00
|
|
|
onForward,
|
|
|
|
onSave,
|
2021-08-24 21:47:14 +00:00
|
|
|
selectedIndex: initialSelectedIndex = 0,
|
2021-08-23 23:14:53 +00:00
|
|
|
}: PropsType): JSX.Element | null {
|
|
|
|
const [root, setRoot] = React.useState<HTMLElement | undefined>();
|
|
|
|
const [selectedIndex, setSelectedIndex] = useState<number>(
|
2021-08-24 21:47:14 +00:00
|
|
|
initialSelectedIndex
|
2021-08-23 23:14:53 +00:00
|
|
|
);
|
2018-04-15 03:27:03 +00:00
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
|
|
|
|
null
|
|
|
|
);
|
|
|
|
const [videoTime, setVideoTime] = useState<number | undefined>();
|
2021-09-28 20:27:35 +00:00
|
|
|
const [zoomType, setZoomType] = useState<ZoomType>(ZoomType.None);
|
2021-08-23 23:14:53 +00:00
|
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
2021-09-02 19:35:23 +00:00
|
|
|
const [focusRef] = useRestoreFocus();
|
2021-09-28 20:27:35 +00:00
|
|
|
const imageRef = useRef<HTMLImageElement | null>(null);
|
|
|
|
const [imagePanStyle, setImagePanStyle] = useState<CSSProperties>({});
|
|
|
|
const zoomCoordsRef = useRef<
|
2021-10-04 20:12:14 +00:00
|
|
|
| {
|
|
|
|
initX: number;
|
|
|
|
initY: number;
|
|
|
|
screenWidth: number;
|
|
|
|
screenHeight: number;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
}
|
2021-09-28 20:27:35 +00:00
|
|
|
| undefined
|
|
|
|
>();
|
2018-04-26 15:18:24 +00:00
|
|
|
|
2021-09-30 21:18:56 +00:00
|
|
|
const isZoomed = zoomType !== ZoomType.None;
|
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
const onPrevious = useCallback(
|
|
|
|
(
|
|
|
|
event: KeyboardEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
|
|
) => {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
2021-09-30 21:18:56 +00:00
|
|
|
if (isZoomed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
setSelectedIndex(prevSelectedIndex => Math.max(prevSelectedIndex - 1, 0));
|
|
|
|
},
|
2021-09-30 21:18:56 +00:00
|
|
|
[isZoomed]
|
2021-09-07 16:12:26 +00:00
|
|
|
);
|
2018-04-15 04:27:30 +00:00
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
const onNext = useCallback(
|
|
|
|
(
|
|
|
|
event: KeyboardEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
|
|
) => {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
2021-09-30 21:18:56 +00:00
|
|
|
if (isZoomed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
setSelectedIndex(prevSelectedIndex =>
|
|
|
|
Math.min(prevSelectedIndex + 1, media.length - 1)
|
|
|
|
);
|
|
|
|
},
|
2021-09-30 21:18:56 +00:00
|
|
|
[isZoomed, media]
|
2021-09-07 16:12:26 +00:00
|
|
|
);
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
const onTimeUpdate = useCallback(() => {
|
|
|
|
if (!videoElement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setVideoTime(videoElement.currentTime);
|
|
|
|
}, [setVideoTime, videoElement]);
|
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
const handleSave = (
|
|
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
|
|
) => {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const mediaItem = media[selectedIndex];
|
|
|
|
const { attachment, message, index } = mediaItem;
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
onSave?.({ attachment, message, index });
|
|
|
|
};
|
2018-07-18 23:02:10 +00:00
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
const handleForward = (
|
|
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
|
|
) => {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
close();
|
|
|
|
const mediaItem = media[selectedIndex];
|
|
|
|
onForward?.(mediaItem.message.id);
|
|
|
|
};
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const onKeyDown = useCallback(
|
|
|
|
(event: KeyboardEvent) => {
|
|
|
|
switch (event.key) {
|
2021-09-28 20:27:35 +00:00
|
|
|
case 'Escape': {
|
2021-09-07 16:12:26 +00:00
|
|
|
close();
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
2018-04-15 04:50:18 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
break;
|
2021-09-28 20:27:35 +00:00
|
|
|
}
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
case 'ArrowLeft':
|
2021-09-07 16:12:26 +00:00
|
|
|
onPrevious(event);
|
2021-08-23 23:14:53 +00:00
|
|
|
break;
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
case 'ArrowRight':
|
2021-09-07 16:12:26 +00:00
|
|
|
onNext(event);
|
2021-08-23 23:14:53 +00:00
|
|
|
break;
|
2018-07-18 23:02:10 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
},
|
2021-09-07 16:12:26 +00:00
|
|
|
[close, onNext, onPrevious]
|
2021-08-23 23:14:53 +00:00
|
|
|
);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
const onClose = (event: React.MouseEvent<HTMLElement>) => {
|
2021-08-23 23:14:53 +00:00
|
|
|
event.stopPropagation();
|
2021-09-07 16:12:26 +00:00
|
|
|
event.preventDefault();
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
close();
|
|
|
|
};
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
const playVideo = useCallback(() => {
|
|
|
|
if (!videoElement) {
|
2019-10-03 19:03:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
if (videoElement.paused) {
|
|
|
|
videoElement.play();
|
2018-07-18 23:02:10 +00:00
|
|
|
} else {
|
2021-08-24 21:47:14 +00:00
|
|
|
videoElement.pause();
|
2018-07-18 23:02:10 +00:00
|
|
|
}
|
2021-08-24 21:47:14 +00:00
|
|
|
}, [videoElement]);
|
2018-04-25 22:15:57 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
|
|
|
const div = document.createElement('div');
|
|
|
|
document.body.appendChild(div);
|
|
|
|
setRoot(div);
|
2018-04-25 22:15:57 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
return () => {
|
|
|
|
document.body.removeChild(div);
|
|
|
|
setRoot(undefined);
|
|
|
|
};
|
|
|
|
}, []);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
|
|
|
const useCapture = true;
|
|
|
|
document.addEventListener('keydown', onKeyDown, useCapture);
|
2020-11-03 00:47:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
return () => {
|
|
|
|
document.removeEventListener('keydown', onKeyDown, useCapture);
|
|
|
|
};
|
|
|
|
}, [onKeyDown]);
|
2018-04-24 20:12:11 +00:00
|
|
|
|
2021-09-02 21:38:46 +00:00
|
|
|
const { attachment, contentType, loop = false, objectURL, message } =
|
|
|
|
media[selectedIndex] || {};
|
|
|
|
|
2021-09-07 16:12:26 +00:00
|
|
|
const isAttachmentGIF = isGIF(attachment ? [attachment] : undefined);
|
2021-09-02 21:38:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
2021-08-24 21:47:14 +00:00
|
|
|
playVideo();
|
2018-04-15 05:48:21 +00:00
|
|
|
|
2021-09-02 21:38:46 +00:00
|
|
|
if (!videoElement || !isViewOnce) {
|
|
|
|
return noop;
|
|
|
|
}
|
2021-08-24 21:47:14 +00:00
|
|
|
|
2021-09-02 21:38:46 +00:00
|
|
|
if (isAttachmentGIF) {
|
|
|
|
return noop;
|
2021-08-24 21:47:14 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 21:38:46 +00:00
|
|
|
videoElement.addEventListener('timeupdate', onTimeUpdate);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
videoElement.removeEventListener('timeupdate', onTimeUpdate);
|
|
|
|
};
|
|
|
|
}, [isViewOnce, isAttachmentGIF, onTimeUpdate, playVideo, videoElement]);
|
2018-04-26 21:25:16 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
const positionImage = useCallback(
|
|
|
|
(ev?: { clientX: number; clientY: number }) => {
|
|
|
|
const imageNode = imageRef.current;
|
|
|
|
const zoomCoords = zoomCoordsRef.current;
|
|
|
|
if (!imageNode || !zoomCoords) {
|
|
|
|
return;
|
|
|
|
}
|
2021-09-28 20:27:35 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
if (ev) {
|
|
|
|
zoomCoords.x = ev.clientX;
|
|
|
|
zoomCoords.y = ev.clientY;
|
|
|
|
}
|
2021-09-28 20:27:35 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
const shouldTransformX = imageNode.naturalWidth > zoomCoords.screenWidth;
|
|
|
|
const shouldTransformY =
|
|
|
|
imageNode.naturalHeight > zoomCoords.screenHeight;
|
2021-09-30 21:18:56 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
const nextImagePanStyle: CSSProperties = {
|
|
|
|
left: '50%',
|
|
|
|
top: '50%',
|
|
|
|
};
|
2021-09-30 21:18:56 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
let translateX = '-50%';
|
|
|
|
let translateY = '-50%';
|
2021-09-30 21:18:56 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
if (shouldTransformX) {
|
|
|
|
const offset = imageNode.offsetWidth - zoomCoords.screenWidth;
|
2021-09-30 21:18:56 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
const scaleX = (-1 / zoomCoords.screenWidth) * offset;
|
2021-09-30 21:18:56 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
const posX = Math.max(
|
|
|
|
0,
|
|
|
|
Math.min(zoomCoords.screenWidth, zoomCoords.x)
|
|
|
|
);
|
2021-09-30 21:18:56 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
translateX = `${posX * scaleX}px`;
|
|
|
|
nextImagePanStyle.left = 0;
|
|
|
|
}
|
2021-09-28 20:27:35 +00:00
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
if (shouldTransformY) {
|
|
|
|
const offset = imageNode.offsetHeight - zoomCoords.screenHeight;
|
|
|
|
|
|
|
|
const scaleY = (-1 / zoomCoords.screenHeight) * offset;
|
|
|
|
|
|
|
|
const posY = Math.max(
|
|
|
|
0,
|
|
|
|
Math.min(zoomCoords.screenHeight, zoomCoords.y)
|
|
|
|
);
|
|
|
|
|
|
|
|
translateY = `${posY * scaleY}px`;
|
|
|
|
nextImagePanStyle.top = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
setImagePanStyle({
|
|
|
|
...nextImagePanStyle,
|
|
|
|
transform: `translate(${translateX}, ${translateY})`,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[]
|
|
|
|
);
|
2021-09-28 20:27:35 +00:00
|
|
|
|
|
|
|
function canPanImage(): boolean {
|
|
|
|
const imageNode = imageRef.current;
|
|
|
|
|
|
|
|
return Boolean(
|
|
|
|
imageNode &&
|
|
|
|
(imageNode.naturalWidth > document.documentElement.clientWidth ||
|
|
|
|
imageNode.naturalHeight > document.documentElement.clientHeight)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-04 20:12:14 +00:00
|
|
|
const handleTouchMove = useCallback(
|
|
|
|
(ev: TouchEvent) => {
|
|
|
|
const imageNode = imageRef.current;
|
|
|
|
const zoomCoords = zoomCoordsRef.current;
|
|
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
if (!imageNode || !zoomCoords) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const [touch] = ev.touches;
|
|
|
|
const { initX, initY } = zoomCoords;
|
|
|
|
|
|
|
|
positionImage({
|
|
|
|
clientX: initX + (initX - touch.clientX),
|
|
|
|
clientY: initY + (initY - touch.clientY),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[positionImage]
|
|
|
|
);
|
|
|
|
|
2021-09-28 20:27:35 +00:00
|
|
|
useEffect(() => {
|
|
|
|
const imageNode = imageRef.current;
|
|
|
|
let hasListener = false;
|
|
|
|
|
|
|
|
if (imageNode && zoomType !== ZoomType.None && canPanImage()) {
|
|
|
|
hasListener = true;
|
|
|
|
document.addEventListener('mousemove', positionImage);
|
2021-10-04 20:12:14 +00:00
|
|
|
document.addEventListener('touchmove', handleTouchMove);
|
2021-09-28 20:27:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
if (hasListener) {
|
|
|
|
document.removeEventListener('mousemove', positionImage);
|
2021-10-04 20:12:14 +00:00
|
|
|
document.removeEventListener('touchmove', handleTouchMove);
|
2021-09-28 20:27:35 +00:00
|
|
|
}
|
|
|
|
};
|
2021-10-04 20:12:14 +00:00
|
|
|
}, [handleTouchMove, positionImage, zoomType]);
|
2021-09-28 20:27:35 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const caption = attachment?.caption;
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
let content: JSX.Element;
|
2021-09-28 20:27:35 +00:00
|
|
|
let shadowImage: JSX.Element | undefined;
|
2021-08-23 23:14:53 +00:00
|
|
|
if (!contentType) {
|
|
|
|
content = <>{children}</>;
|
|
|
|
} else {
|
|
|
|
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
|
|
|
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
|
|
|
|
const isUnsupportedImageType =
|
|
|
|
!isImageTypeSupported && isImage(contentType);
|
|
|
|
const isUnsupportedVideoType =
|
|
|
|
!isVideoTypeSupported && isVideo(contentType);
|
2018-04-26 21:25:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
if (isImageTypeSupported) {
|
|
|
|
if (objectURL) {
|
2021-09-28 20:27:35 +00:00
|
|
|
shadowImage = (
|
|
|
|
<div className="Lightbox__shadow-container">
|
|
|
|
<div className="Lightbox__object--container">
|
|
|
|
<img
|
|
|
|
alt={i18n('lightboxImageAlt')}
|
|
|
|
className="Lightbox__object"
|
|
|
|
ref={imageRef}
|
|
|
|
src={objectURL}
|
|
|
|
tabIndex={-1}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2021-08-23 23:14:53 +00:00
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
className="Lightbox__zoom-button"
|
2021-09-07 16:12:26 +00:00
|
|
|
onClick={(
|
|
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
|
|
) => {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
2021-09-28 20:27:35 +00:00
|
|
|
if (zoomType === ZoomType.None) {
|
|
|
|
if (canPanImage()) {
|
|
|
|
setZoomType(ZoomType.ZoomAndPan);
|
|
|
|
zoomCoordsRef.current = {
|
2021-10-04 20:12:14 +00:00
|
|
|
initX: event.clientX,
|
|
|
|
initY: event.clientY,
|
2021-09-28 20:27:35 +00:00
|
|
|
screenHeight: document.documentElement.clientHeight,
|
2021-10-04 20:12:14 +00:00
|
|
|
screenWidth: document.documentElement.clientWidth,
|
2021-09-28 20:27:35 +00:00
|
|
|
x: event.clientX,
|
|
|
|
y: event.clientY,
|
|
|
|
};
|
|
|
|
positionImage();
|
|
|
|
} else {
|
|
|
|
setZoomType(ZoomType.FillScreen);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
setZoomType(ZoomType.None);
|
|
|
|
}
|
2021-09-07 16:12:26 +00:00
|
|
|
}}
|
2021-08-23 23:14:53 +00:00
|
|
|
type="button"
|
|
|
|
>
|
|
|
|
<img
|
|
|
|
alt={i18n('lightboxImageAlt')}
|
|
|
|
className="Lightbox__object"
|
2021-09-07 16:12:26 +00:00
|
|
|
onContextMenu={(event: React.MouseEvent<HTMLImageElement>) => {
|
2021-08-23 23:14:53 +00:00
|
|
|
// These are the only image types supported by Electron's NativeImage
|
|
|
|
if (
|
|
|
|
event &&
|
|
|
|
contentType !== IMAGE_PNG &&
|
|
|
|
!/image\/jpe?g/g.test(contentType)
|
|
|
|
) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
src={objectURL}
|
2021-09-28 20:27:35 +00:00
|
|
|
style={zoomType === ZoomType.ZoomAndPan ? imagePanStyle : {}}
|
2021-08-23 23:14:53 +00:00
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('lightboxImageAlt')}
|
|
|
|
className={classNames({
|
|
|
|
Lightbox__object: true,
|
|
|
|
Lightbox__unsupported: true,
|
|
|
|
'Lightbox__unsupported--missing': true,
|
|
|
|
})}
|
2021-09-07 16:12:26 +00:00
|
|
|
onClick={onClose}
|
2021-08-23 23:14:53 +00:00
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else if (isVideoTypeSupported) {
|
2021-09-02 21:38:46 +00:00
|
|
|
const shouldLoop = loop || isAttachmentGIF || isViewOnce;
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
content = (
|
|
|
|
<video
|
|
|
|
className="Lightbox__object"
|
|
|
|
controls={!shouldLoop}
|
|
|
|
key={objectURL}
|
|
|
|
loop={shouldLoop}
|
2021-08-24 21:47:14 +00:00
|
|
|
ref={setVideoElement}
|
2021-08-23 23:14:53 +00:00
|
|
|
>
|
|
|
|
<source src={objectURL} />
|
|
|
|
</video>
|
|
|
|
);
|
|
|
|
} else if (isUnsupportedImageType || isUnsupportedVideoType) {
|
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('unsupportedAttachment')}
|
|
|
|
className={classNames({
|
|
|
|
Lightbox__object: true,
|
|
|
|
Lightbox__unsupported: true,
|
|
|
|
'Lightbox__unsupported--image': isUnsupportedImageType,
|
|
|
|
'Lightbox__unsupported--video': isUnsupportedVideoType,
|
|
|
|
})}
|
2021-09-07 16:12:26 +00:00
|
|
|
onClick={onClose}
|
2021-08-23 23:14:53 +00:00
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
} else {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('Lightbox: Unexpected content type', { contentType });
|
2018-04-15 05:48:21 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('unsupportedAttachment')}
|
|
|
|
className="Lightbox__object Lightbox__unsupported Lightbox__unsupported--file"
|
2021-09-07 16:12:26 +00:00
|
|
|
onClick={onClose}
|
2021-08-23 23:14:53 +00:00
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
);
|
2018-04-15 05:48:21 +00:00
|
|
|
}
|
2021-08-23 23:14:53 +00:00
|
|
|
}
|
2018-04-15 05:48:21 +00:00
|
|
|
|
2021-09-30 21:18:56 +00:00
|
|
|
const hasNext = !isZoomed && selectedIndex < media.length - 1;
|
|
|
|
const hasPrevious = !isZoomed && selectedIndex > 0;
|
2021-08-23 23:14:53 +00:00
|
|
|
|
|
|
|
return root
|
|
|
|
? createPortal(
|
|
|
|
<div
|
|
|
|
className="Lightbox Lightbox__container"
|
2021-09-07 16:12:26 +00:00
|
|
|
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
close();
|
|
|
|
}}
|
|
|
|
onKeyUp={(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
|
|
if (
|
|
|
|
(containerRef && event.target !== containerRef.current) ||
|
|
|
|
event.keyCode !== 27
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
close();
|
|
|
|
}}
|
|
|
|
ref={containerRef}
|
|
|
|
role="presentation"
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
className="Lightbox__main-container"
|
|
|
|
tabIndex={-1}
|
|
|
|
ref={focusRef}
|
|
|
|
>
|
2021-09-28 20:27:35 +00:00
|
|
|
{!isZoomed && (
|
2021-08-23 23:14:53 +00:00
|
|
|
<div className="Lightbox__header">
|
|
|
|
{getConversation ? (
|
|
|
|
<LightboxHeader
|
|
|
|
getConversation={getConversation}
|
|
|
|
i18n={i18n}
|
|
|
|
message={message}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div />
|
|
|
|
)}
|
|
|
|
<div className="Lightbox__controls">
|
|
|
|
{onForward ? (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('forwardMessage')}
|
|
|
|
className="Lightbox__button Lightbox__button--forward"
|
|
|
|
onClick={handleForward}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
{onSave ? (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('save')}
|
|
|
|
className="Lightbox__button Lightbox__button--save"
|
|
|
|
onClick={handleSave}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
<button
|
|
|
|
aria-label={i18n('close')}
|
|
|
|
className="Lightbox__button Lightbox__button--close"
|
|
|
|
onClick={close}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<div
|
|
|
|
className={classNames('Lightbox__object--container', {
|
2021-09-28 20:27:35 +00:00
|
|
|
'Lightbox__object--container--fill':
|
|
|
|
zoomType === ZoomType.FillScreen,
|
|
|
|
'Lightbox__object--container--zoom':
|
|
|
|
zoomType === ZoomType.ZoomAndPan,
|
2021-08-23 23:14:53 +00:00
|
|
|
})}
|
|
|
|
>
|
|
|
|
{content}
|
|
|
|
</div>
|
2021-09-28 20:27:35 +00:00
|
|
|
{shadowImage}
|
2021-08-23 23:14:53 +00:00
|
|
|
{hasPrevious && (
|
|
|
|
<div className="Lightbox__nav-prev">
|
|
|
|
<button
|
|
|
|
aria-label={i18n('previous')}
|
|
|
|
className="Lightbox__button Lightbox__button--previous"
|
|
|
|
onClick={onPrevious}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{hasNext && (
|
|
|
|
<div className="Lightbox__nav-next">
|
|
|
|
<button
|
|
|
|
aria-label={i18n('next')}
|
|
|
|
className="Lightbox__button Lightbox__button--next"
|
|
|
|
onClick={onNext}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
2021-09-28 20:27:35 +00:00
|
|
|
{!isZoomed && (
|
2021-08-23 23:14:53 +00:00
|
|
|
<div className="Lightbox__footer">
|
2021-08-24 21:47:14 +00:00
|
|
|
{isViewOnce && videoTime ? (
|
|
|
|
<div className="Lightbox__timestamp">
|
|
|
|
{formatDuration(videoTime)}
|
|
|
|
</div>
|
|
|
|
) : null}
|
2021-08-23 23:14:53 +00:00
|
|
|
{caption ? (
|
|
|
|
<div className="Lightbox__caption">{caption}</div>
|
|
|
|
) : null}
|
|
|
|
{media.length > 1 && (
|
|
|
|
<div className="Lightbox__thumbnails--container">
|
|
|
|
<div
|
|
|
|
className="Lightbox__thumbnails"
|
|
|
|
style={{
|
|
|
|
marginLeft:
|
|
|
|
0 - (selectedIndex * 64 + selectedIndex * 8 + 32),
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{media.map((item, index) => (
|
|
|
|
<button
|
|
|
|
className={classNames({
|
|
|
|
Lightbox__thumbnail: true,
|
|
|
|
'Lightbox__thumbnail--selected':
|
|
|
|
index === selectedIndex,
|
|
|
|
})}
|
|
|
|
key={item.thumbnailObjectUrl}
|
|
|
|
type="button"
|
2021-09-07 16:12:26 +00:00
|
|
|
onClick={(
|
|
|
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
|
|
) => {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
setSelectedIndex(index);
|
|
|
|
}}
|
2021-08-23 23:14:53 +00:00
|
|
|
>
|
|
|
|
{item.thumbnailObjectUrl ? (
|
|
|
|
<img
|
|
|
|
alt={i18n('lightboxImageAlt')}
|
|
|
|
src={item.thumbnailObjectUrl}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div className="Lightbox__thumbnail--unavailable" />
|
|
|
|
)}
|
|
|
|
</button>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>,
|
|
|
|
root
|
|
|
|
)
|
|
|
|
: null;
|
|
|
|
}
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
function LightboxHeader({
|
|
|
|
getConversation,
|
|
|
|
i18n,
|
|
|
|
message,
|
|
|
|
}: {
|
|
|
|
getConversation: (id: string) => ConversationType;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
message: MessageAttributesType;
|
|
|
|
}): JSX.Element {
|
|
|
|
const conversation = getConversation(message.conversationId);
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
return (
|
|
|
|
<div className="Lightbox__header--container">
|
|
|
|
<div className="Lightbox__header--avatar">
|
|
|
|
<Avatar
|
|
|
|
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
|
|
|
avatarPath={conversation.avatarPath}
|
|
|
|
color={conversation.color}
|
|
|
|
conversationType={conversation.type}
|
|
|
|
i18n={i18n}
|
|
|
|
isMe={conversation.isMe}
|
|
|
|
name={conversation.name}
|
|
|
|
phoneNumber={conversation.e164}
|
|
|
|
profileName={conversation.profileName}
|
|
|
|
sharedGroupNames={conversation.sharedGroupNames}
|
|
|
|
size={AvatarSize.THIRTY_TWO}
|
|
|
|
title={conversation.title}
|
|
|
|
unblurredAvatarPath={conversation.unblurredAvatarPath}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="Lightbox__header--content">
|
|
|
|
<div className="Lightbox__header--name">{conversation.title}</div>
|
|
|
|
<div className="Lightbox__header--timestamp">
|
|
|
|
{moment(message.received_at_ms).format('L LT')}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2018-04-15 03:27:03 +00:00
|
|
|
}
|