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})`, } } > - {description} + {description} { + setImageLoaded(true); + }} + /> + + ) : ( + {description} { - // 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;