From 2d0c804abc64e5b2dac5e540f207a4d9bcb6aa95 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 9 Jul 2026 21:35:35 -0400 Subject: [PATCH] =?UTF-8?q?fix(media-gallery):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20proper=20decrypt-download=20+=20mimetype=20sanitize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of 28cb004e (3 agents) surfaced two real issues: - Downloads used a raw mxc→http anchor, so encrypted media (voice messages are almost always in E2EE DMs) downloaded ciphertext. Reuse FileDownloadButton for both the audio AND file rows — it decrypts before saving, adds loading/success state + a toast, and derives a filename extension. - Audio playback bypassed MAudio's mimetype sanitization; pass the value through getBlobSafeMimeType (e.g. application/ogg → audio/ogg) so encrypted/odd-mimetype audio actually plays. Give voice messages a real filename+extension. Also corrected the docs: AudioContent renders a seek bar, not an MSC3245 waveform. Co-Authored-By: Claude Opus 4.8 --- LOTUS_FEATURES.md | 2 +- src/app/features/room/MediaGallery.tsx | 59 +++++++++----------------- 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index af08ccce3..ed9e168a5 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -1084,7 +1084,7 @@ A toggle in **Settings → Privacy** switches between sending `m.read` (public r - Four tabs: **Images**, **Videos**, **Audio**, **Files** (each with a live count) - **Images/Videos** — a month-grouped grid; tiles decrypt on demand (lazy, near-viewport), open a keyboard-navigable **lightbox** (←/→/Esc, prev/next) -- **Audio** — voice messages + audio files (`m.audio`) with an inline player (reuses `AudioContent`: play/seek/**speed control**, MSC3245 voice waveform; decrypts on play) +- **Audio** — voice messages + audio files (`m.audio`) with an inline player (reuses `AudioContent`: play/seek/**speed control**; decrypts on play) - **Files** — name/size/sender rows with download - **Jump to message** — a "Go to message" action on file rows, audio rows, and in the lightbox navigates the timeline to the source event (`useRoomNavigate`) and closes the drawer - Encrypted media is decrypted client-side on demand (no lock placeholder); download works for all types diff --git a/src/app/features/room/MediaGallery.tsx b/src/app/features/room/MediaGallery.tsx index 9c4b8a521..45cf16278 100644 --- a/src/app/features/room/MediaGallery.tsx +++ b/src/app/features/room/MediaGallery.tsx @@ -24,8 +24,9 @@ import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/ma import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../utils/matrix'; -import { AudioContent } from '../../components/message'; +import { AudioContent, FileDownloadButton } from '../../components/message'; import { MediaControl } from '../../components/media'; +import { getBlobSafeMimeType, mimeTypeToExt } from '../../utils/mimeTypes'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { ContainerColor } from '../../styles/ContainerColor.css'; import { stopPropagation } from '../../utils/keyboard'; @@ -827,9 +828,6 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) { const body: string = c.body ?? 'Unnamed file'; const size: number | undefined = c.info?.size; const sender = getSenderName(room, mEvent.getSender() ?? ''); - const downloadUrl = mxcUrl - ? (mxcUrlToHttp(mx, mxcUrl, useAuthentication) ?? '#') - : '#'; return ( - { - const a = document.createElement('a'); - a.href = downloadUrl; - a.download = body; - a.target = '_blank'; - a.rel = 'noreferrer'; - a.click(); - }} - > - - + {mxcUrl && ( + + )} ); })} @@ -914,7 +904,10 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) { const body: string = c.body || 'Voice message'; const sender = getSenderName(room, mEvent.getSender() ?? ''); const relDate = formatRelativeDate(mEvent.getTs()); - const downloadUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? '#'; + // Sanitize the mimetype the way MAudio does (e.g. application/ogg → + // audio/ogg) so the decrypted blob actually plays. + const mimeType = getBlobSafeMimeType(c.info?.mimetype ?? 'audio/ogg'); + const filename = body.includes('.') ? body : `${body}.${mimeTypeToExt(mimeType)}`; return ( - { - const a = document.createElement('a'); - a.href = downloadUrl; - a.download = body; - a.target = '_blank'; - a.rel = 'noreferrer'; - a.click(); - }} - > - - +