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();
- }}
- >
-
-
+