feat: voice message recording + per-member encryption verification
CI / Build & Quality Checks (push) Successful in 10m20s

- 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 8ca9853dea
commit 74284902c2
4 changed files with 432 additions and 5 deletions
+34
View File
@@ -119,6 +119,7 @@ import { useTheme } from '../../hooks/useTheme';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { useComposingCheck } from '../../hooks/useComposingCheck';
import { VoiceMessageRecorder } from '../../components/VoiceMessageRecorder';
const GifPicker = React.lazy(() =>
import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })),
@@ -201,6 +202,38 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
);
};
const handleVoiceSend = useCallback(
async (blob: Blob, mimeType: string, durationMs: number, waveform: number[]) => {
const baseContent: IContent = {
msgtype: MsgType.Audio,
body: 'Voice message',
filename: 'voice-message.ogg',
'org.matrix.msc3245.voice': {},
'org.matrix.msc1767.audio': { duration: durationMs, waveform },
info: { mimetype: mimeType, size: blob.size, duration: durationMs },
};
if (room.hasEncryptionStateEvent()) {
const { encInfo, file: encBlob } = await encryptFile(blob);
const uploadResult = await mx.uploadContent(encBlob);
mx.sendMessage(roomId, {
...baseContent,
file: { ...encInfo, url: uploadResult.content_uri },
} as any);
} else {
const uploadResult = await mx.uploadContent(blob, {
name: 'voice-message.ogg',
type: mimeType,
});
mx.sendMessage(roomId, {
...baseContent,
url: uploadResult.content_uri,
} as any);
}
},
[mx, room, roomId],
);
const [autocompleteQuery, setAutocompleteQuery] =
useState<AutocompleteQuery<AutocompletePrefix>>();
@@ -849,6 +882,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<Icon src={Icons.Pin} size="100" />
)}
</IconButton>
<VoiceMessageRecorder onSend={handleVoiceSend} />
<IconButton
onClick={submit}
variant="SurfaceVariant"