Skip to content

Commit

Permalink
Massively zoom in on images, adds panning
Browse files Browse the repository at this point in the history
Co-authored-by: Josh Perez <60019601+josh-signal@users.noreply.github.com>
  • Loading branch information
automated-signal and josh-signal committed Sep 28, 2021
1 parent b5c1032 commit 136d333
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 16 deletions.
39 changes: 31 additions & 8 deletions stylesheets/components/Lightbox.scss
Expand Up @@ -72,18 +72,23 @@
}
}

&__shadow-container {
display: flex;
height: 100%;
padding: 0 16px;
position: absolute;
width: 100%;
z-index: 0;
}

&__object {
&--container {
display: inline-flex;
flex-grow: 1;
justify-content: center;
margin: 0 40px;
overflow: hidden;
position: relative;

&--zoomed {
margin: 0;
}
z-index: 1;
}

height: auto;
Expand All @@ -92,15 +97,32 @@
max-width: 100%;
object-fit: contain;
outline: none;
padding: 0 40px;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: auto;
}

&__object--container--zoomed &__object {
width: 100%;
&__shadow-container &__object {
max-height: 200%;
max-width: 200%;
padding: 0;
visibility: hidden;
}

&__object--container--fill &__object {
height: 100%;
padding: 0;
width: 100%;
}

&__object--container--zoom &__object {
left: 0;
max-height: 200%;
max-width: 200%;
padding: 0;
top: 0;
}

&__unsupported {
Expand Down Expand Up @@ -135,7 +157,8 @@
cursor: zoom-in;
}

&__object--container--zoomed {
&__object--container--zoom,
&__object--container--fill {
.Lightbox__zoom-button {
cursor: zoom-out;
}
Expand Down
118 changes: 110 additions & 8 deletions ts/components/Lightbox.tsx
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only

import React, {
CSSProperties,
ReactNode,
useCallback,
useEffect,
Expand Down Expand Up @@ -40,6 +41,12 @@ export type PropsType = {
selectedIndex?: number;
};

enum ZoomType {
None,
FillScreen,
ZoomAndPan,
}

export function Lightbox({
children,
close,
Expand All @@ -60,9 +67,15 @@ export function Lightbox({
null
);
const [videoTime, setVideoTime] = useState<number | undefined>();
const [zoomed, setZoomed] = useState(false);
const [zoomType, setZoomType] = useState<ZoomType>(ZoomType.None);
const containerRef = useRef<HTMLDivElement | null>(null);
const [focusRef] = useRestoreFocus();
const imageRef = useRef<HTMLImageElement | null>(null);
const [imagePanStyle, setImagePanStyle] = useState<CSSProperties>({});
const zoomCoordsRef = useRef<
| { screenWidth: number; screenHeight: number; x: number; y: number }
| undefined
>();

const onPrevious = useCallback(
(
Expand Down Expand Up @@ -123,13 +136,14 @@ export function Lightbox({
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
switch (event.key) {
case 'Escape':
case 'Escape': {
close();

event.preventDefault();
event.stopPropagation();

break;
}

case 'ArrowLeft':
onPrevious(event);
Expand Down Expand Up @@ -207,9 +221,62 @@ export function Lightbox({
};
}, [isViewOnce, isAttachmentGIF, onTimeUpdate, playVideo, videoElement]);

const positionImage = useCallback((ev?: MouseEvent) => {
const imageNode = imageRef.current;
const zoomCoords = zoomCoordsRef.current;
if (!imageNode || !zoomCoords) {
return;
}

if (ev) {
zoomCoords.x = ev.clientX;
zoomCoords.y = ev.clientY;
}

const scaleX =
(-1 / zoomCoords.screenWidth) *
(imageNode.offsetWidth - zoomCoords.screenWidth);
const scaleY =
(-1 / zoomCoords.screenHeight) *
(imageNode.offsetHeight - zoomCoords.screenHeight);

setImagePanStyle({
transform: `translate(${zoomCoords.x * scaleX}px, ${
zoomCoords.y * scaleY
}px)`,
});
}, []);

function canPanImage(): boolean {
const imageNode = imageRef.current;

return Boolean(
imageNode &&
(imageNode.naturalWidth > document.documentElement.clientWidth ||
imageNode.naturalHeight > document.documentElement.clientHeight)
);
}

useEffect(() => {
const imageNode = imageRef.current;
let hasListener = false;

if (imageNode && zoomType !== ZoomType.None && canPanImage()) {
hasListener = true;
document.addEventListener('mousemove', positionImage);
}

return () => {
if (hasListener) {
document.removeEventListener('mousemove', positionImage);
}
};
}, [positionImage, zoomType]);

const caption = attachment?.caption;

let content: JSX.Element;
let shadowImage: JSX.Element | undefined;
if (!contentType) {
content = <>{children}</>;
} else {
Expand All @@ -222,6 +289,19 @@ export function Lightbox({

if (isImageTypeSupported) {
if (objectURL) {
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>
);
content = (
<button
className="Lightbox__zoom-button"
Expand All @@ -231,7 +311,22 @@ export function Lightbox({
event.preventDefault();
event.stopPropagation();

setZoomed(!zoomed);
if (zoomType === ZoomType.None) {
if (canPanImage()) {
setZoomType(ZoomType.ZoomAndPan);
zoomCoordsRef.current = {
screenWidth: document.documentElement.clientWidth,
screenHeight: document.documentElement.clientHeight,
x: event.clientX,
y: event.clientY,
};
positionImage();
} else {
setZoomType(ZoomType.FillScreen);
}
} else {
setZoomType(ZoomType.None);
}
}}
type="button"
>
Expand All @@ -249,6 +344,7 @@ export function Lightbox({
}
}}
src={objectURL}
style={zoomType === ZoomType.ZoomAndPan ? imagePanStyle : {}}
/>
</button>
);
Expand Down Expand Up @@ -308,8 +404,10 @@ export function Lightbox({
}
}

const hasNext = !zoomed && selectedIndex < media.length - 1;
const hasPrevious = !zoomed && selectedIndex > 0;
const isZoomed = zoomType !== ZoomType.None;

const hasNext = isZoomed && selectedIndex < media.length - 1;
const hasPrevious = isZoomed && selectedIndex > 0;

return root
? createPortal(
Expand Down Expand Up @@ -339,7 +437,7 @@ export function Lightbox({
tabIndex={-1}
ref={focusRef}
>
{!zoomed && (
{!isZoomed && (
<div className="Lightbox__header">
{getConversation ? (
<LightboxHeader
Expand Down Expand Up @@ -378,11 +476,15 @@ export function Lightbox({
)}
<div
className={classNames('Lightbox__object--container', {
'Lightbox__object--container--zoomed': zoomed,
'Lightbox__object--container--fill':
zoomType === ZoomType.FillScreen,
'Lightbox__object--container--zoom':
zoomType === ZoomType.ZoomAndPan,
})}
>
{content}
</div>
{shadowImage}
{hasPrevious && (
<div className="Lightbox__nav-prev">
<button
Expand All @@ -404,7 +506,7 @@ export function Lightbox({
</div>
)}
</div>
{!zoomed && (
{!isZoomed && (
<div className="Lightbox__footer">
{isViewOnce && videoTime ? (
<div className="Lightbox__timestamp">
Expand Down
14 changes: 14 additions & 0 deletions ts/util/lint/exceptions.json
Expand Up @@ -12636,6 +12636,20 @@
"reasonCategory": "usageTrusted",
"updated": "2021-08-23T18:39:37.081Z"
},
{
"rule": "React-useRef",
"path": "ts/components/Lightbox.tsx",
"line": " const imageRef = useRef<HTMLImageElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-24T00:03:36.061Z"
},
{
"rule": "React-useRef",
"path": "ts/components/Lightbox.tsx",
"line": " const zoomCoordsRef = useRef<",
"reasonCategory": "usageTrusted",
"updated": "2021-09-24T00:03:36.061Z"
},
{
"rule": "React-createRef",
"path": "ts/components/MainHeader.js",
Expand Down

0 comments on commit 136d333

Please sign in to comment.