diff --git a/src/features/ui/components/image-loader.tsx b/src/features/ui/components/image-loader.tsx index a22edc49e..74d3680e0 100644 --- a/src/features/ui/components/image-loader.tsx +++ b/src/features/ui/components/image-loader.tsx @@ -12,6 +12,8 @@ interface IImageLoader { width?: number; height?: number; onClick?: React.MouseEventHandler; + isMobile?: boolean; + } class ImageLoader extends PureComponent { @@ -135,7 +137,7 @@ class ImageLoader extends PureComponent { const { alt, src, width, height, onClick } = this.props; const { loading } = this.state; - const className = 'relative h-screen flex items-center justify-center flex-col'; + const className = clsx('relative flex h-screen w-full grow flex-col items-center justify-center'); return (
@@ -154,6 +156,7 @@ class ImageLoader extends PureComponent { alt={alt} src={src} onClick={onClick} + isMobile={this.props.isMobile} /> )}
diff --git a/src/features/ui/components/modals/media-modal.tsx b/src/features/ui/components/modals/media-modal.tsx index 0654a444b..a28a46fde 100644 --- a/src/features/ui/components/modals/media-modal.tsx +++ b/src/features/ui/components/modals/media-modal.tsx @@ -24,6 +24,7 @@ import PlaceholderStatus from 'soapbox/features/placeholder/components/placehold import Thread from 'soapbox/features/status/components/thread.tsx'; import Video from 'soapbox/features/video/index.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts'; import { userTouching } from 'soapbox/is-mobile.ts'; import { normalizeStatus } from 'soapbox/normalizers/index.ts'; import { Status as StatusEntity, Attachment } from 'soapbox/schemas/index.ts'; @@ -72,6 +73,7 @@ const MediaModal: React.FC = (props) => { const dispatch = useAppDispatch(); const history = useHistory(); const intl = useIntl(); + const isMobile = useIsMobile(); const actualStatus = status ? getActualStatus(status) : undefined; @@ -143,6 +145,7 @@ const MediaModal: React.FC = (props) => { alt={attachment.description} key={attachment.url} onClick={toggleNavigation} + isMobile={isMobile} /> ); } else if (attachment.type === 'video') { @@ -241,7 +244,7 @@ const MediaModal: React.FC = (props) => { }; return ( -
+
= (props) => { } justifyContent='between' > - + = (props) => { {/* Height based on height of top/bottom bars */}
{hasMultipleImages && ( -
+
)} -
- - {content} - -
+ + {content} + {hasMultipleImages && ( -
+
- {actualStatus && ( + {isFullScreen && actualStatus && ( { @@ -32,11 +33,20 @@ class ZoomableImage extends PureComponent { state = { scale: MIN_SCALE, + isDragging: false, }; container: HTMLDivElement | null = null; image: HTMLImageElement | null = null; lastDistance = 0; + isDragging = false; + isMouseDown = false; + clickStartTime = 0; + startX = 0; + startY = 0; + startScrollLeft = 0; + startScrollTop = 0; + isMobile = this.props.isMobile; componentDidMount() { this.container?.addEventListener('touchstart', this.handleTouchStart); @@ -47,7 +57,7 @@ class ZoomableImage extends PureComponent { componentWillUnmount() { this.container?.removeEventListener('touchstart', this.handleTouchStart); - this.container?.removeEventListener('touchend', this.handleTouchMove); + this.container?.removeEventListener('touchmove', this.handleTouchMove); } handleTouchStart = (e: TouchEvent) => { @@ -81,11 +91,54 @@ class ZoomableImage extends PureComponent { this.lastDistance = distance; }; + handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (this.state.isDragging) { + this.setState({ isDragging: false }); + } + + if (this.state.scale === 1 || !this.container) return; + + this.isDragging = true; + this.startX = e.clientX; + this.startY = e.clientY; + this.startScrollLeft = this.container.scrollLeft; + this.startScrollTop = this.container.scrollTop; + + window.addEventListener('mousemove', this.handleMouseMove); + window.addEventListener('mouseup', this.handleMouseUp); + }; + + handleMouseMove = (e: MouseEvent) => { + if (!this.isDragging || !this.container) return; + + e.preventDefault(); + e.stopPropagation(); + + const deltaX = this.startX - e.clientX; + const deltaY = this.startY - e.clientY; + + if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) this.setState({ isDragging: true }); + + this.container.scrollLeft = this.startScrollLeft + deltaX; + this.container.scrollTop = this.startScrollTop + deltaY; + }; + + handleMouseUp = (e: MouseEvent) => { + this.isDragging = false; + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('mouseup', this.handleMouseUp); + }; + + + zoom(nextScale: number, midpoint: Point) { - if (!this.container) return; + if (!this.container || this.state.isDragging) return; const { scale } = this.state; - const { scrollLeft, scrollTop } = this.container; + const { scrollLeft, scrollTop, clientWidth, clientHeight } = this.container; // math memo: // x = (scrollLeft + midpoint.x) / scrollWidth @@ -93,6 +146,9 @@ class ZoomableImage extends PureComponent { // scrollWidth = clientWidth * scale // scrollWidth' = clientWidth * nextScale // Solve x = x' for nextScrollLeft + const originX = (midpoint.x / clientWidth) * 100; + const originY = (midpoint.y / clientHeight) * 100; + const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x; const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y; @@ -100,14 +156,30 @@ class ZoomableImage extends PureComponent { if (!this.container) return; this.container.scrollLeft = nextScrollLeft; this.container.scrollTop = nextScrollTop; + if (!this.isMobile) this.image!.style.transformOrigin = `${originX}% ${originY}%`; }); } handleClick: React.MouseEventHandler = e => { - // don't propagate event to MediaModal e.stopPropagation(); - const handler = this.props.onClick; - if (handler) handler(e); + + if (this.isMobile) { + const handler = this.props.onClick; + if (handler) handler(e); + } else { + if (this.state.scale !== 1) { + this.zoom(1, { x: 0, y: 0 }); + } else { + if (!this.image) return; + + const rect = this.image.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + + const midpoint: Point = { x: clickX, y: clickY }; + this.zoom(2, midpoint); + } + } }; setContainerRef = (c: HTMLDivElement) => { @@ -127,20 +199,21 @@ class ZoomableImage extends PureComponent {
1 ? 'grab' : 'default' }} > {alt} 1 && !this.isMobile ? 'center' : '0 0'}`, }} onClick={this.handleClick} + onMouseDown={this.handleMouseDown} />
); diff --git a/src/features/video/index.tsx b/src/features/video/index.tsx index 723d3349f..569941290 100644 --- a/src/features/video/index.tsx +++ b/src/features/video/index.tsx @@ -534,7 +534,7 @@ const Video: React.FC = ({ return (