feat: extend verification badge to user profile and settings members list

Extract MemberVerificationBadge into a shared component and render it in:
- UserRoomProfile: shield badge beside the display name on the profile card
- common-settings Members: badge next to each member in the room/space
  settings members page (accessible from the lobby)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 12:53:33 -04:00
parent 4c5cff13ad
commit 161b9bb75e
4 changed files with 56 additions and 35 deletions
@@ -0,0 +1,35 @@
import React from 'react';
import { Icon, Icons, Text, Tooltip, TooltipProvider } from 'folds';
import { useUserVerifiedStatus } from '../hooks/useUserVerifiedStatus';
type MemberVerificationBadgeProps = {
userId: string;
};
export function MemberVerificationBadge({ userId }: MemberVerificationBadgeProps) {
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="100" src={Icons.ShieldUser} style={{ color }} />
</span>
)}
</TooltipProvider>
);
}
@@ -10,6 +10,8 @@ import { usePowerLevels } from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom'; import { useRoom } from '../../hooks/useRoom';
import { useUserPresence } from '../../hooks/useUserPresence'; import { useUserPresence } from '../../hooks/useUserPresence';
import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips';
import { useCrossSigningActive } from '../../hooks/useCrossSigning';
import { MemberVerificationBadge } from '../MemberVerificationBadge';
import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { PowerChip } from './PowerChip'; import { PowerChip } from './PowerChip';
import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration';
@@ -28,6 +30,7 @@ type UserRoomProfileProps = {
}; };
export function UserRoomProfile({ userId }: UserRoomProfileProps) { export function UserRoomProfile({ userId }: UserRoomProfileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const crossSigningActive = useCrossSigningActive();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const navigate = useNavigate(); const navigate = useNavigate();
const closeUserRoomProfile = useCloseUserRoomProfile(); const closeUserRoomProfile = useCloseUserRoomProfile();
@@ -35,6 +38,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
const ignored = ignoredUsers.includes(userId); const ignored = ignoredUsers.includes(userId);
const room = useRoom(); const room = useRoom();
const showEncryption = room.hasEncryptionStateEvent() && crossSigningActive;
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room); const creators = useRoomCreators(room);
@@ -76,8 +80,9 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
/> />
<Box direction="Column" gap="500" style={{ padding: config.space.S400 }}> <Box direction="Column" gap="500" style={{ padding: config.space.S400 }}>
<Box direction="Column" gap="400"> <Box direction="Column" gap="400">
<Box gap="400" alignItems="Start"> <Box gap="400" alignItems="Center">
<UserHeroName displayName={displayName} userId={userId} /> <UserHeroName displayName={displayName} userId={userId} />
{showEncryption && <MemberVerificationBadge userId={userId} />}
{userId !== myUserId && ( {userId !== myUserId && (
<Box shrink="No"> <Box shrink="No">
<Button <Button
@@ -56,6 +56,8 @@ import { useSpaceOptionally } from '../../../hooks/useSpace';
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../../hooks/useMemberPowerTag'; import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../../hooks/useMemberPowerTag';
import { useRoomCreators } from '../../../hooks/useRoomCreators'; import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { getMouseEventCords } from '../../../utils/dom'; import { getMouseEventCords } from '../../../utils/dom';
import { useCrossSigningActive } from '../../../hooks/useCrossSigning';
import { MemberVerificationBadge } from '../../../components/MemberVerificationBadge';
const SEARCH_OPTIONS: UseAsyncSearchOptions = { const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000, limit: 1000,
@@ -79,6 +81,8 @@ export function Members({ requestClose }: MembersProps) {
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const room = useRoom(); const room = useRoom();
const members = useRoomMembers(mx, room.roomId); const members = useRoomMembers(mx, room.roomId);
const crossSigningActive = useCrossSigningActive();
const showEncryption = room.hasEncryptionStateEvent() && crossSigningActive;
const fetchingMembers = members.length < room.getJoinedMemberCount(); const fetchingMembers = members.length < room.getJoinedMemberCount();
const openProfile = useOpenUserRoomProfile(); const openProfile = useOpenUserRoomProfile();
const profileUser = useUserRoomProfileState(); const profileUser = useUserRoomProfileState();
@@ -328,11 +332,16 @@ export function Members({ requestClose }: MembersProps) {
member={tagOrMember} member={tagOrMember}
useAuthentication={useAuthentication} useAuthentication={useAuthentication}
after={ after={
server && ( <>
<Box as="span" shrink="No" alignSelf="End"> {showEncryption && (
<ServerBadge server={server} fill="None" /> <MemberVerificationBadge userId={tagOrMember.userId} />
</Box> )}
) {server && (
<Box as="span" shrink="No" alignSelf="End">
<ServerBadge server={server} fill="None" />
</Box>
)}
</>
} }
/> />
</div> </div>
+1 -29
View File
@@ -60,7 +60,7 @@ import { ContainerColor } from '../../styles/ContainerColor.css';
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag'; import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useCrossSigningActive } from '../../hooks/useCrossSigning'; import { useCrossSigningActive } from '../../hooks/useCrossSigning';
import { useUserVerifiedStatus } from '../../hooks/useUserVerifiedStatus'; import { MemberVerificationBadge } from '../../components/MemberVerificationBadge';
type MemberDrawerHeaderProps = { type MemberDrawerHeaderProps = {
room: Room; room: Room;
@@ -115,34 +115,6 @@ type MemberItemProps = {
showEncryption?: 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({ function MemberItem({
mx, mx,
useAuthentication, useAuthentication,