diff --git a/src/app/features/room/MediaGallery.tsx b/src/app/features/room/MediaGallery.tsx index f4e12fa6a..5e28d3149 100644 --- a/src/app/features/room/MediaGallery.tsx +++ b/src/app/features/room/MediaGallery.tsx @@ -135,10 +135,17 @@ function formatBytes(bytes: number): string { } // A sensible download filename: prefer the event body/filename; if it has no -// extension, append one derived from the mimetype so the saved file opens. +// plausible extension already, append one derived from the mimetype so the +// saved file opens. "Plausible" = a short alphanumeric tail after the last dot, +// so "Screenshot 2024.01.05" still gets a real extension appended. +function hasFileExtension(name: string): boolean { + const dot = name.lastIndexOf('.'); + if (dot <= 0 || dot === name.length - 1) return false; + return /^[a-z0-9]{1,5}$/i.test(name.slice(dot + 1)); +} function mediaFilename(body: string, mimeType?: string): string { const name = body.trim() || 'media'; - if (name.includes('.')) return name; + if (hasFileExtension(name)) return name; const ext = mimeType ? mimeTypeToExt(mimeType) : ''; return ext ? `${name}.${ext}` : name; } @@ -254,7 +261,12 @@ function LightboxMedia({ borderRadius: config.radii.R300, display: 'block', cursor, - transform: `scale(${zoom}) translate(${pan.translateX}px, ${pan.translateY}px)`, + // translate is nested inside scale(), so it runs in scaled space — + // divide by zoom so a dragged pixel moves the image one screen pixel + // (1:1 with the cursor) rather than `zoom` pixels. + transform: `scale(${zoom}) translate(${pan.translateX / zoom}px, ${ + pan.translateY / zoom + }px)`, transition: cursor === 'grabbing' ? 'none' : 'transform 120ms ease-out', willChange: 'transform', }} @@ -313,7 +325,7 @@ function Lightbox({ (e: React.WheelEvent) => { if (!isImage) return; if (e.deltaY < 0) zoomIn(); - else zoomOut(); + else if (e.deltaY > 0) zoomOut(); }, [isImage, zoomIn, zoomOut], ); @@ -372,14 +384,20 @@ function Lightbox({ {index + 1} / {items.length} {isImage && ( - +