Merge branch 'updating-zoom' into 'main'

feat: add 2x zoom on desktop

See merge request soapbox-pub/soapbox!3328
Daniel 2025-05-03 22:51:09 +00:00
commit 12d7e6da91
4 zmienionych plików z 106 dodań i 30 usunięć

Wyświetl plik

@ -12,6 +12,8 @@ interface IImageLoader {
width?: number;
height?: number;
onClick?: React.MouseEventHandler;
isMobile?: boolean;
}
class ImageLoader extends PureComponent<IImageLoader> {
@ -135,7 +137,7 @@ class ImageLoader extends PureComponent<IImageLoader> {
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 (
<div className={className}>
@ -154,6 +156,7 @@ class ImageLoader extends PureComponent<IImageLoader> {
alt={alt}
src={src}
onClick={onClick}
isMobile={this.props.isMobile}
/>
)}
</div>

Wyświetl plik

@ -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<IMediaModal> = (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<IMediaModal> = (props) => {
alt={attachment.description}
key={attachment.url}
onClick={toggleNavigation}
isMobile={isMobile}
/>
);
} else if (attachment.type === 'video') {
@ -241,7 +244,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
};
return (
<div className='media-modal pointer-events-auto fixed inset-0 z-[9999] flex size-full bg-gray-900/90'>
<div className='pointer-events-auto fixed inset-0 z-[9999] flex size-full bg-gray-900/90'>
<div
className='absolute inset-0'
role='presentation'
@ -256,7 +259,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
}
justifyContent='between'
>
<Stack className='relative h-full'>
<Stack className='relative h-full items-center'>
<HStack
alignItems='center'
justifyContent='between'
@ -295,10 +298,10 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
{/* Height based on height of top/bottom bars */}
<div
className='relative h-[calc(100vh-120px)] w-full grow'
className='relative flex h-[calc(100vh-120px)] w-full grow items-center justify-center'
>
{hasMultipleImages && (
<div className={clsx('absolute inset-y-0 left-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
<div className={clsx('absolute left-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
<button
tabIndex={0}
className='flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
@ -310,23 +313,20 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
</div>
)}
<div className='size-full'>
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
onChangeIndex={handleSwipe}
className='flex items-center justify-center'
index={getIndex()}
>
{content}
</ReactSwipeableViews>
</div>
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
onChangeIndex={handleSwipe}
index={getIndex()}
>
{content}
</ReactSwipeableViews>
{hasMultipleImages && (
<div className={clsx('absolute inset-y-0 right-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
<div className={clsx('absolute right-5 flex items-center transition-opacity', navigationHiddenClassName)}>
<button
tabIndex={0}
className='flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
className='z-10 flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
onClick={handleNextClick}
aria-label={intl.formatMessage(messages.next)}
>
@ -336,10 +336,10 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
)}
</div>
{actualStatus && (
{isFullScreen && actualStatus && (
<HStack
justifyContent='center'
className={clsx('absolute bottom-2 flex w-full transition-opacity', navigationHiddenClassName)}
className={clsx('absolute bottom-2 flex transition-opacity', navigationHiddenClassName)}
>
<StatusActionBar
status={normalizeStatus(actualStatus) as LegacyStatus}

Wyświetl plik

@ -20,6 +20,7 @@ interface IZoomableImage {
alt?: string;
src: string;
onClick?: React.MouseEventHandler;
isMobile?: boolean;
}
class ZoomableImage extends PureComponent<IZoomableImage> {
@ -32,11 +33,20 @@ class ZoomableImage extends PureComponent<IZoomableImage> {
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<IZoomableImage> {
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<IZoomableImage> {
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<IZoomableImage> {
// 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<IZoomableImage> {
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<IZoomableImage> {
<div
className='relative flex size-full items-center justify-center'
ref={this.setContainerRef}
style={{ overflow }}
style={{ overflow, cursor: scale > 1 ? 'grab' : 'default' }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
className={clsx('size-auto max-h-[80%] max-w-full object-contain', { 'size-full max-h-full': scale !== 1 })}
className={clsx('size-auto max-h-[80%] max-w-full object-contain', scale !== 1 ? 'size-full' : 'hover:cursor-pointer')}
title={alt}
src={src}
style={{
transform: `scale(${scale})`,
transformOrigin: '0 0',
transformOrigin: `${scale > 1 && !this.isMobile ? 'center' : '0 0'}`,
}}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
/>
</div>
);

Wyświetl plik

@ -534,7 +534,7 @@ const Video: React.FC<IVideo> = ({
return (
<div
role='menuitem'
className={clsx('relative box-border flex max-w-full overflow-hidden rounded-[10px] bg-black text-white focus:outline-0', { 'w-full h-full m-0': fullscreen })}
className={clsx('relative box-border flex h-screen max-w-full items-center justify-center overflow-hidden rounded-[10px] bg-black text-white focus:outline-0', { 'w-full h-full m-0': fullscreen })}
style={playerStyle}
ref={player}
onClick={handleClickRoot}