feat(messages): reply quotes show a media thumbnail (#151)
CI / Build & Quality Checks (push) Successful in 1m46s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Successful in 11s
CI / Playwright smoke (e2e) (push) Successful in 9m44s

A reply to an image, video or sticker used to quote just the filename.
The quote (timeline) and the composer's reply-draft preview now carry a
36 px thumbnail from the event's own thumbnail, decrypted for E2EE media
via the same hook the gallery uses — never the full-size file. Clicking
still jumps to the original.

useDecryptedMediaUrl and getThumbMxc moved out of MediaGallery into
hooks/ and utils/ so components/message can use them without a cycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-20 14:27:23 -04:00
co-authored by Claude Opus 5
parent be2c202543
commit 22d46a7922
8 changed files with 175 additions and 77 deletions
+65
View File
@@ -0,0 +1,65 @@
import { useEffect, useRef, useState } from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import { IEncryptedFile } from '../../types/matrix/common';
import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../utils/matrix';
type DecryptState = { status: 'loading' } | { status: 'ok'; url: string } | { status: 'error' };
export function useDecryptedMediaUrl(
mx: MatrixClient,
mxcUrl: string | undefined,
encInfo: IEncryptedFile | undefined,
useAuthentication: boolean,
mimeType?: string,
enabled = true,
): DecryptState {
const [state, setState] = useState<DecryptState>({ status: 'loading' });
const prevBlobUrl = useRef<string | null>(null);
useEffect(() => {
if (!enabled) return undefined;
if (!mxcUrl) {
setState({ status: 'error' });
return;
}
let cancelled = false;
setState({ status: 'loading' });
const run = async () => {
const httpUrl = mxcUrlToHttp(mx, mxcUrl, useAuthentication);
if (!httpUrl) throw new Error('bad url');
if (encInfo) {
const blob = await downloadEncryptedMedia(httpUrl, (buf) =>
decryptFile(buf, mimeType ?? 'application/octet-stream', encInfo),
);
const blobUrl = URL.createObjectURL(blob);
if (cancelled) {
URL.revokeObjectURL(blobUrl);
return;
}
if (prevBlobUrl.current) URL.revokeObjectURL(prevBlobUrl.current);
prevBlobUrl.current = blobUrl;
setState({ status: 'ok', url: blobUrl });
} else {
setState({ status: 'ok', url: httpUrl });
}
};
run().catch(() => {
if (!cancelled) setState({ status: 'error' });
});
return () => {
cancelled = true;
};
}, [mx, mxcUrl, encInfo, useAuthentication, mimeType, enabled]);
useEffect(
() => () => {
if (prevBlobUrl.current) URL.revokeObjectURL(prevBlobUrl.current);
},
[],
);
return state;
}