pl-fe: try to fix media modal behavior after recent ports

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-01-16 01:56:43 +01:00
parent 63d226a206
commit b97ce74ec2
3 changed files with 84 additions and 28 deletions

View File

@ -67,7 +67,6 @@ const ModalRoot: React.FC = () => {
const index = modals.length - 1;
const onClickClose = (type?: ModalType, all?: boolean) => {
console.log('Closing modal:', type, all);
switch (type) {
case 'COMPOSE':
dispatch(cancelReplyCompose());

View File

@ -1,8 +1,9 @@
import { animated, useSpring } from '@react-spring/web';
import { Link } from '@tanstack/react-router';
import { useDrag } from '@use-gesture/react';
import clsx from 'clsx';
import React, { type RefCallback, useCallback, useEffect, useState } from 'react';
import React, { type RefCallback, useCallback, useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import ReactSwipeableViews from 'react-swipeable-views';
import { fetchStatusWithContext } from 'pl-fe/actions/statuses';
import ExtendedVideoPlayer from 'pl-fe/components/extended-video-player';
@ -25,6 +26,8 @@ import { makeGetStatus } from 'pl-fe/selectors';
import type { MediaAttachment } from 'pl-api';
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
const MIN_SWIPE_DISTANCE = 400;
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
expand: { id: 'lightbox.expand', defaultMessage: 'Expand' },
@ -36,19 +39,6 @@ const messages = defineMessages({
download: { id: 'video.download', defaultMessage: 'Download file' },
});
// you can't use 100vh, because the viewport height is taller
// than the visible part of the document in some mobile
// browsers when its address bar is visible.
// https://developers.google.com/web/updates/2016/12/url-bar-resizing
const swipeableViewsStyle: React.CSSProperties = {
width: '100%',
height: '100%',
};
const containerStyle: React.CSSProperties = {
alignItems: 'center', // center vertically
};
interface MediaModalProps {
media?: Array<MediaAttachment>;
statusId?: string;
@ -77,6 +67,32 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
const [navigationHidden, setNavigationHidden] = useState(false);
const [isFullScreen, setIsFullScreen] = useState(!status);
const [wrapperStyles, api] = useSpring(() => ({
x: `-${index * 100}%`,
}));
const handleChangeIndex = useCallback(
(newIndex: number, animate = false) => {
if (newIndex < 0) {
newIndex = media.length + newIndex;
} else if (newIndex >= media.length) {
newIndex = newIndex % media.length;
}
setIndex(newIndex);
setZoomedIn(false);
if (animate) {
void api.start({ x: `calc(-${newIndex * 100}% + 0px)` });
}
},
[api, media.length],
);
const handlePrevClick = useCallback(() => {
handleChangeIndex(index - 1, true);
}, [handleChangeIndex, index]);
const handleNextClick = useCallback(() => {
handleChangeIndex(index + 1, true);
}, [handleChangeIndex, index]);
const [viewportDimensions, setViewportDimensions] = useState<{
width: number;
height: number;
@ -93,10 +109,6 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
const hasMultipleImages = media.length > 1;
const handleSwipe = (index: number) => setIndex(index % media.length);
const handleNextClick = () => setIndex((index + 1) % media.length);
const handlePrevClick = () => setIndex((media.length + index - 1) % media.length);
const navigationHiddenClassName = navigationHidden ? 'pointer-events-none opacity-0' : '';
const handleKeyDown = (e: KeyboardEvent) => {
@ -114,6 +126,30 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
}
};
const bind = useDrag(
({ active, movement: [mx], direction: [xDir], cancel }) => {
// Disable swipe when zoomed in.
if (zoomedIn) {
return;
}
// If dragging and swipe distance is enough, change the index.
if (
active &&
Math.abs(mx) > Math.min(window.innerWidth / 4, MIN_SWIPE_DISTANCE)
) {
handleChangeIndex(index - xDir);
cancel();
}
// Set the x position via calc to ensure proper centering regardless of screen size.
const x = active ? mx : 0;
void api.start({
x: `calc(-${index * 100}% + ${x}px)`,
});
},
{ pointer: { capture: false } },
);
const handleDownload = () => {
const mediaItem = hasMultipleImages ? media[index as number] : media[0];
window.open(mediaItem?.url);
@ -133,7 +169,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
setZoomedIn((prev) => !prev);
}, []);
const content = media.map((attachment, i) => {
const content = useMemo(() => media.map((attachment, i) => {
let width: number | undefined, height: number | undefined;
if (attachment.type === 'image' || attachment.type === 'gifv' || attachment.type === 'video') {
width = (attachment.meta?.original?.width);
@ -209,7 +245,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
}
return null;
});
}), [media.length, index, zoomedIn, handleZoomClick]);
// Load data.
useEffect(() => {
@ -313,6 +349,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
{/* Height based on height of top/bottom bars */}
<div
{...bind()}
className='relative h-[calc(100vh-120px)] w-full grow'
>
{hasMultipleImages && (
@ -328,14 +365,14 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
</div>
)}
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
onChangeIndex={handleSwipe}
index={index}
<animated.div
style={wrapperStyles}
className='media-modal__closer'
role='presentation'
onClick={() => onClose()}
>
{content}
</ReactSwipeableViews>
</animated.div>
{hasMultipleImages && (
<div className={clsx('absolute inset-y-0 right-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>

View File

@ -1,4 +1,6 @@
.media-modal {
touch-action: pan-y;
.audio-player.detailed,
.extended-video-player {
@apply flex items-center justify-center;
@ -15,5 +17,23 @@
@apply max-w-full max-h-[80%];
}
}
&__closer {
display: flex;
position: absolute;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
> div {
flex-shrink: 0;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
}
}