feat(media): swipe between items in the full-screen viewer (#164)
CI / Build & Quality Checks (push) Successful in 1m29s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s

Touch had no way to move between images except the small arrow buttons.
A one-finger horizontal swipe now goes next/previous, and is inert while
the image is zoomed in (where the same gesture pans). The classifier is
pure and unit-tested: ≥ 60 px horizontal, not mostly vertical, under
800 ms, single finger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-23 15:20:22 -04:00
co-authored by Claude Opus 5
parent 738067cfc6
commit cf80729a5c
3 changed files with 101 additions and 1 deletions
+12 -1
View File
@@ -23,6 +23,7 @@ import classNames from 'classnames';
import { useNearViewport } from '../../hooks/useNearViewport';
import { useZoom } from '../../hooks/useZoom';
import { usePan, Pan } from '../../hooks/usePan';
import { useSwipeNav } from '../../hooks/useSwipeNav';
import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/matrix/common';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
@@ -255,7 +256,15 @@ export function Lightbox({
const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2);
// Pan is only active for a zoomed-in image; usePan resets its offset when this
// flips false (i.e. back to 1x, on navigation, or on a video).
const { pan, cursor, onMouseDown, onTouchStart } = usePan(isImage && zoom !== 1);
const zoomedIn = isImage && zoom !== 1;
const { pan, cursor, onMouseDown, onTouchStart } = usePan(zoomedIn);
// [Gitea #164] Swipe between items on touch — only while not zoomed in,
// where the same gesture pans the image instead.
const swipe = useSwipeNav(
!zoomedIn,
useCallback(() => setIndex((i) => Math.min(items.length - 1, i + 1)), [items.length]),
useCallback(() => setIndex((i) => Math.max(0, i - 1)), []),
);
const dialogRef = useRef<HTMLDivElement>(null);
const toggleZoom = useCallback(() => setZoom((z) => (z === 1 ? 2 : 1)), [setZoom]);
@@ -446,6 +455,8 @@ export function Lightbox({
alignItems="Center"
justifyContent="Center"
onWheel={handleWheel}
onTouchStart={swipe.onTouchStart}
onTouchEnd={swipe.onTouchEnd}
style={{ overflow: 'hidden', padding: config.space.S400 }}
>
{index > 0 && (