fix(media-gallery): address review — proper decrypt-download + mimetype sanitize
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 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
key={mEvent.getId()}
|
||||
@@ -867,22 +865,14 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) {
|
||||
>
|
||||
<Icon size="200" src={Icons.Message} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-label={`Download ${body}`}
|
||||
onClick={() => {
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = body;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noreferrer';
|
||||
a.click();
|
||||
}}
|
||||
>
|
||||
<Icon size="200" src={Icons.Download} />
|
||||
</IconButton>
|
||||
{mxcUrl && (
|
||||
<FileDownloadButton
|
||||
filename={body}
|
||||
url={mxcUrl}
|
||||
mimeType={c.info?.mimetype ?? 'application/octet-stream'}
|
||||
encInfo={c.file}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
@@ -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 (
|
||||
<Box
|
||||
key={mEvent.getId()}
|
||||
@@ -951,25 +944,15 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) {
|
||||
>
|
||||
<Icon size="200" src={Icons.Message} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-label={`Download ${body}`}
|
||||
onClick={() => {
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = body;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noreferrer';
|
||||
a.click();
|
||||
}}
|
||||
>
|
||||
<Icon size="200" src={Icons.Download} />
|
||||
</IconButton>
|
||||
<FileDownloadButton
|
||||
filename={filename}
|
||||
url={url}
|
||||
mimeType={mimeType}
|
||||
encInfo={c.file}
|
||||
/>
|
||||
</Box>
|
||||
<AudioContent
|
||||
mimeType={c.info?.mimetype ?? 'audio/ogg'}
|
||||
mimeType={mimeType}
|
||||
url={url}
|
||||
info={c.info ?? {}}
|
||||
encInfo={c.file}
|
||||
|
||||
Reference in New Issue
Block a user