diff --git a/package-lock.json b/package-lock.json
index 859704f6..083e5ae4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"preact": "~10.13.1",
"react-hotkeys-hook": "~4.3.8",
"react-intersection-observer": "~9.4.3",
+ "react-quick-pinch-zoom": "~4.6.0",
"react-router-dom": "6.6.2",
"string-length": "~5.0.1",
"swiped-events": "~1.1.7",
@@ -5814,6 +5815,27 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-quick-pinch-zoom": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/react-quick-pinch-zoom/-/react-quick-pinch-zoom-4.6.0.tgz",
+ "integrity": "sha512-M3woYVzWt8Kh6FCAytBtJJ4KC/5noG98GpI8ZTxTIE2sjR1XRPjV0NpFRhFgxPQpDvD+lkMp63sxP130uhafaw==",
+ "peerDependencies": {
+ "react": ">=16.4.0",
+ "react-dom": ">=16.4.0",
+ "tslib": ">=2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "tslib": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
@@ -11072,6 +11094,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "react-quick-pinch-zoom": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/react-quick-pinch-zoom/-/react-quick-pinch-zoom-4.6.0.tgz",
+ "integrity": "sha512-M3woYVzWt8Kh6FCAytBtJJ4KC/5noG98GpI8ZTxTIE2sjR1XRPjV0NpFRhFgxPQpDvD+lkMp63sxP130uhafaw==",
+ "requires": {}
+ },
"react-router": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
diff --git a/package.json b/package.json
index 618d56dd..b18bf764 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"preact": "~10.13.1",
"react-hotkeys-hook": "~4.3.8",
"react-intersection-observer": "~9.4.3",
+ "react-quick-pinch-zoom": "~4.6.0",
"react-router-dom": "6.6.2",
"string-length": "~5.0.1",
"swiped-events": "~1.1.7",
diff --git a/src/components/media-modal.jsx b/src/components/media-modal.jsx
index 9fdb5b0f..9b850714 100644
--- a/src/components/media-modal.jsx
+++ b/src/components/media-modal.jsx
@@ -93,7 +93,8 @@ function MediaModal({
onClick={(e) => {
if (
e.target.classList.contains('carousel-item') ||
- e.target.classList.contains('media')
+ e.target.classList.contains('media') ||
+ e.target.classList.contains('media-zoom')
) {
onClose();
}
diff --git a/src/components/media.jsx b/src/components/media.jsx
index 548f6618..6ad46727 100644
--- a/src/components/media.jsx
+++ b/src/components/media.jsx
@@ -1,5 +1,6 @@
import { getBlurHashAverageColor } from 'fast-blurhash';
-import { useRef } from 'preact/hooks';
+import { useCallback, useRef, useState } from 'preact/hooks';
+import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
import Icon from './icon';
import { formatDuration } from './status';
@@ -39,6 +40,19 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
focalBackgroundPosition = `${x.toFixed(0)}% ${y.toFixed(0)}%`;
}
+ const imgRef = useRef();
+ const onUpdate = useCallback(({ x, y, scale }) => {
+ const { current: img } = imgRef;
+
+ if (img) {
+ const value = make3dTransformValue({ x, y, scale });
+
+ img.style.setProperty('transform', value);
+ }
+ }, []);
+
+ const [imageLoaded, setImageLoaded] = useState(false);
+
if (type === 'image' || (type === 'unknown' && previewUrl && url)) {
// Note: type: unknown might not have width/height
return (
@@ -46,33 +60,55 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
class={`media media-image`}
onClick={onClick}
style={
- showOriginal && {
+ showOriginal &&
+ !imageLoaded && {
backgroundImage: `url(${previewUrl})`,
}
}
>
-
+
{
+ setImageLoaded(true);
+ }}
+ />
+
+ ) : (
+
{
- // Open original image in new tab
- window.open(url, '_blank');
- }}
- onLoad={(e) => {
- // Hide background image after image loads
- e.target.parentElement.style.backgroundImage = 'none';
- }}
- />
+ }}
+ onLoad={(e) => {
+ setImageLoaded(true);
+ }}
+ />
+ )}
);
} else if (type === 'gifv' || type === 'video') {
diff --git a/src/components/status.css b/src/components/status.css
index 8040e568..8ce58d9d 100644
--- a/src/components/status.css
+++ b/src/components/status.css
@@ -668,6 +668,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
align-items: center;
gap: 8px;
font-size: 90%;
+ z-index: 1;
}
.carousel-item button.media-alt .media-alt-desc {
overflow: hidden;