feat: voice message recording + per-member encryption verification

- Add VoiceMessageRecorder component: mic button in composer toolbar,
  live waveform + timer, preview before send, MSC3245-compliant content
  (org.matrix.msc3245.voice, org.matrix.msc1767.audio with waveform),
  E2EE support via encryptFile before upload
- Add useUserVerifiedStatus hook: uses crypto.getUserVerificationStatus,
  reacts live to CryptoEvent.UserTrustStatusChanged
- MembersDrawer: show green/yellow shield badge per member in encrypted
  rooms (cross-signing verified/unverified), E2EE status banner in header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 12:19:06 -04:00
parent 46b6fecdde
commit e17bb6a534
4 changed files with 432 additions and 5 deletions
+41
View File
@@ -0,0 +1,41 @@
import { useCallback, useEffect, useState } from 'react';
import { useMatrixClient } from './useMatrixClient';
import { useUserTrustStatusChange } from './useUserTrustStatusChange';
import { useCrossSigningActive } from './useCrossSigning';
export type UserVerifiedStatus = 'verified' | 'unverified' | 'unknown';
export function useUserVerifiedStatus(userId: string): UserVerifiedStatus {
const mx = useMatrixClient();
const crossSigningActive = useCrossSigningActive();
const [status, setStatus] = useState<UserVerifiedStatus>('unknown');
const check = useCallback(async () => {
const crypto = mx.getCrypto();
if (!crypto || !crossSigningActive) {
setStatus('unknown');
return;
}
try {
const vs = await crypto.getUserVerificationStatus(userId);
setStatus(vs.isVerified() ? 'verified' : 'unverified');
} catch {
setStatus('unknown');
}
}, [mx, userId, crossSigningActive]);
useEffect(() => {
check();
}, [check]);
useUserTrustStatusChange(
useCallback(
(changedUserId: string) => {
if (changedUserId === userId) check();
},
[userId, check],
),
);
return status;
}