feat: voice message recording + per-member encryption verification
CI / Build & Quality Checks (push) Successful in 10m20s
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:
@@ -59,6 +59,8 @@ import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
import { useCrossSigningActive } from '../../hooks/useCrossSigning';
|
||||
import { useUserVerifiedStatus } from '../../hooks/useUserVerifiedStatus';
|
||||
|
||||
type MemberDrawerHeaderProps = {
|
||||
room: Room;
|
||||
@@ -110,7 +112,37 @@ type MemberItemProps = {
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
pressed?: boolean;
|
||||
typing?: boolean;
|
||||
showEncryption?: boolean;
|
||||
};
|
||||
|
||||
function MemberVerificationBadge({ userId }: { userId: string }) {
|
||||
const vs = useUserVerifiedStatus(userId);
|
||||
if (vs === 'unknown') return null;
|
||||
const color =
|
||||
vs === 'verified' ? 'var(--tc-positive-normal, #5effc4)' : 'var(--tc-warning-normal, #ffcc55)';
|
||||
const label = vs === 'verified' ? 'Identity verified' : 'Not verified';
|
||||
return (
|
||||
<TooltipProvider
|
||||
position="Top"
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text size="T200">{label}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
title={label}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', flexShrink: 0 }}
|
||||
>
|
||||
<Icon size="50" src={Icons.ShieldUser} style={{ color }} />
|
||||
</span>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function MemberItem({
|
||||
mx,
|
||||
useAuthentication,
|
||||
@@ -119,6 +151,7 @@ function MemberItem({
|
||||
onClick,
|
||||
pressed,
|
||||
typing,
|
||||
showEncryption,
|
||||
}: MemberItemProps) {
|
||||
const name =
|
||||
getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
|
||||
@@ -146,11 +179,14 @@ function MemberItem({
|
||||
</Avatar>
|
||||
}
|
||||
after={
|
||||
typing && (
|
||||
<Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
|
||||
<TypingIndicator size="300" />
|
||||
</Badge>
|
||||
)
|
||||
<>
|
||||
{showEncryption && <MemberVerificationBadge userId={member.userId} />}
|
||||
{typing && (
|
||||
<Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
|
||||
<TypingIndicator size="300" />
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
@@ -180,6 +216,9 @@ type MembersDrawerProps = {
|
||||
export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const isEncrypted = room.hasEncryptionStateEvent();
|
||||
const crossSigningActive = useCrossSigningActive();
|
||||
const showEncryption = isEncrypted && crossSigningActive;
|
||||
const scrollRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
||||
const searchInputRef = useRef<HTMLInputElement>(null) as React.RefObject<HTMLInputElement>;
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
||||
@@ -252,6 +291,26 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
direction="Column"
|
||||
>
|
||||
<MemberDrawerHeader room={room} />
|
||||
{isEncrypted && (
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
style={{
|
||||
padding: `${config.space.S100} ${config.space.S300}`,
|
||||
background: 'var(--bg-surface-variant)',
|
||||
borderBottom: '1px solid var(--border-surface-variant)',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size="50"
|
||||
src={Icons.Lock}
|
||||
style={{ color: 'var(--tc-positive-normal, #5effc4)', flexShrink: 0 }}
|
||||
/>
|
||||
<Text size="T200" style={{ color: 'var(--tc-surface-low-contrast)' }}>
|
||||
{crossSigningActive ? 'E2EE · Shield = verified identity' : 'End-to-end encrypted room'}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box className={css.MemberDrawerContentBase} grow="Yes">
|
||||
<Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover" hideTrack>
|
||||
<Box className={css.MemberDrawerContent} direction="Column" gap="200">
|
||||
@@ -423,6 +482,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
typing={typingMembers.some(
|
||||
(receipt) => receipt.userId === tagOrMember.userId,
|
||||
)}
|
||||
showEncryption={showEncryption}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user