Update styling of UserInfo right panel card (#12788)

* Add colour to PresenceLabel in UserInfo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update button positions & styles in UserInfo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update UserInfo styles

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert Ignore->Block copy change

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-07-18 15:24:44 +01:00 committed by GitHub
parent 2920e76b64
commit f706ac4fa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 670 additions and 419 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -41,35 +41,17 @@ limitations under the License.
} }
} }
h2 {
font-size: $font-18px;
font-weight: var(--cpd-font-weight-semibold);
margin: 18px 0 0 0;
}
.mx_UserInfo_container { .mx_UserInfo_container {
padding: $spacing-8 $spacing-16; padding: var(--cpd-space-4x) 0;
margin: 0 var(--cpd-space-4x);
&:not(.mx_UserInfo_separator) {
padding-top: $spacing-16;
padding-bottom: 0;
> :not(h3) {
margin-inline-start: $spacing-8;
display: flex;
flex-flow: column;
align-items: flex-start;
row-gap: $spacing-8;
}
}
.mx_UserInfo_container_verifyButton { .mx_UserInfo_container_verifyButton {
margin-top: $spacing-8; margin-top: $spacing-8;
} }
}
.mx_UserInfo_separator { & + .mx_UserInfo_container {
border-bottom: 1px solid $separator; border-top: 1px solid $separator;
}
} }
.mx_UserInfo_memberDetailsContainer { .mx_UserInfo_memberDetailsContainer {
@ -94,7 +76,7 @@ limitations under the License.
margin: $spacing-24 $spacing-32 0 $spacing-32; margin: $spacing-24 $spacing-32 0 $spacing-32;
.mx_UserInfo_avatar_transition { .mx_UserInfo_avatar_transition {
max-width: 30vh; max-width: 120px;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
margin: 0 auto; margin: 0 auto;
transition: 0.5s; transition: 0.5s;
@ -112,7 +94,7 @@ limitations under the License.
} }
} }
h3 { h2 {
text-transform: uppercase; text-transform: uppercase;
color: $tertiary-content; color: $tertiary-content;
font: var(--cpd-font-heading-sm-semibold); font: var(--cpd-font-heading-sm-semibold);
@ -125,18 +107,10 @@ limitations under the License.
} }
.mx_UserInfo_profile { .mx_UserInfo_profile {
text-align: center; h1 {
font-size: $font-20px;
h2 {
display: flex;
font-size: $font-17px;
line-height: $font-25px; line-height: $font-25px;
flex: 1;
justify-content: center;
/* We reverse things here so for accessible technologies the name comes before the e2e shield */
flex-direction: row-reverse;
span {
/* limit to 2 lines, show an ellipsis if it overflows */ /* limit to 2 lines, show an ellipsis if it overflows */
/* this looks webkit specific but is supported by Firefox 68+ */ /* this looks webkit specific but is supported by Firefox 68+ */
display: -webkit-box; display: -webkit-box;
@ -146,20 +120,23 @@ limitations under the License.
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
text-overflow: ellipsis; text-overflow: ellipsis;
}
.mx_E2EIcon { /* E2E icon wrapper */
margin-top: 3px; /* visual vertical centering to the top line of text. */ .mx_Flex > span {
margin-inline-end: $spacing-4; /* margin from displayName */ display: inline-block;
min-width: 18px; /* convince flexbox to not collapse it */
} }
} }
.mx_UserInfo_profileStatus { .mx_UserInfo_profileStatus {
margin-top: $spacing-12; margin: var(--cpd-space-1x) 0;
} }
} }
.mx_PresenceLabel {
font: var(--cpd-font-body-sm-regular);
opacity: 1;
}
.mx_UserInfo_memberDetails { .mx_UserInfo_memberDetails {
.mx_UserInfo_profileField { .mx_UserInfo_profileField {
display: flex; display: flex;
@ -184,10 +161,6 @@ limitations under the License.
.mx_UserInfo_field { .mx_UserInfo_field {
line-height: $font-16px; line-height: $font-16px;
&.mx_UserInfo_destructive {
color: $alert;
}
} }
.mx_UserInfo_statusMessage { .mx_UserInfo_statusMessage {

View file

@ -18,3 +18,7 @@ limitations under the License.
font-size: $font-11px; font-size: $font-11px;
opacity: 0.5; opacity: 0.5;
} }
.mx_PresenceLabel_online {
color: var(--cpd-color-text-success-primary);
}

View file

@ -34,6 +34,18 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { UserVerificationStatus, VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { UserVerificationStatus, VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { Heading, MenuItem, Text } from "@vector-im/compound-web";
import { Icon as ChatIcon } from "@vector-im/compound-design-tokens/icons/chat.svg";
import { Icon as CheckIcon } from "@vector-im/compound-design-tokens/icons/check.svg";
import { Icon as ShareIcon } from "@vector-im/compound-design-tokens/icons/share.svg";
import { Icon as MentionIcon } from "@vector-im/compound-design-tokens/icons/mention.svg";
import { Icon as InviteIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg";
import { Icon as BlockIcon } from "@vector-im/compound-design-tokens/icons/block.svg";
import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg";
import { Icon as CloseIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
import { Icon as ChatProblemIcon } from "@vector-im/compound-design-tokens/icons/chat-problem.svg";
import { Icon as VisibilityOffIcon } from "@vector-im/compound-design-tokens/icons/visibility-off.svg";
import { Icon as LeaveIcon } from "@vector-im/compound-design-tokens/icons/leave.svg";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -79,7 +91,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages"; import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
import { SdkContextClass } from "../../../contexts/SDKContext"; import { SdkContextClass } from "../../../contexts/SDKContext";
import { asyncSome } from "../../../utils/arrays"; import { asyncSome } from "../../../utils/arrays";
import UIStore from "../../../stores/UIStore"; import { Flex } from "../../utils/Flex";
import CopyableText from "../elements/CopyableText";
export interface IDevice extends Device { export interface IDevice extends Device {
ambiguous?: boolean; ambiguous?: boolean;
@ -391,31 +404,29 @@ const MessageButton = ({ member }: { member: Member }): JSX.Element => {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
return ( return (
<AccessibleButton <MenuItem
kind="link" role="button"
onClick={async () => { onSelect={async (ev) => {
ev.preventDefault();
if (busy) return; if (busy) return;
setBusy(true); setBusy(true);
await openDmForUser(cli, member); await openDmForUser(cli, member);
setBusy(false); setBusy(false);
}} }}
className="mx_UserInfo_field"
disabled={busy} disabled={busy}
> label={_t("user_info|send_message")}
{_t("common|message")} Icon={ChatIcon}
</AccessibleButton> />
); );
}; };
export const UserOptionsSection: React.FC<{ export const UserOptionsSection: React.FC<{
member: Member; member: Member;
isIgnored: boolean;
canInvite: boolean; canInvite: boolean;
isSpace?: boolean; isSpace?: boolean;
}> = ({ member, isIgnored, canInvite, isSpace }) => { }> = ({ member, canInvite, isSpace, children }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
let ignoreButton: JSX.Element | undefined;
let insertPillButton: JSX.Element | undefined; let insertPillButton: JSX.Element | undefined;
let inviteUserButton: JSX.Element | undefined; let inviteUserButton: JSX.Element | undefined;
let readReceiptButton: JSX.Element | undefined; let readReceiptButton: JSX.Element | undefined;
@ -427,42 +438,9 @@ export const UserOptionsSection: React.FC<{
}); });
}; };
const unignore = useCallback(() => {
const ignoredUsers = cli.getIgnoredUsers();
const index = ignoredUsers.indexOf(member.userId);
if (index !== -1) ignoredUsers.splice(index, 1);
cli.setIgnoredUsers(ignoredUsers);
}, [cli, member]);
const ignore = useCallback(async () => {
const name = (member instanceof User ? member.displayName : member.name) || member.userId;
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("user_info|ignore_confirm_title", { user: name }),
description: <div>{_t("user_info|ignore_confirm_description")}</div>,
button: _t("action|ignore"),
});
const [confirmed] = await finished;
if (confirmed) {
const ignoredUsers = cli.getIgnoredUsers();
ignoredUsers.push(member.userId);
cli.setIgnoredUsers(ignoredUsers);
}
}, [cli, member]);
// Only allow the user to ignore the user if its not ourselves // Only allow the user to ignore the user if its not ourselves
// same goes for jumping to read receipt // same goes for jumping to read receipt
if (!isMe) { if (!isMe) {
ignoreButton = (
<AccessibleButton
onClick={isIgnored ? unignore : ignore}
kind="link"
className={classNames("mx_UserInfo_field", { mx_UserInfo_destructive: !isIgnored })}
>
{isIgnored ? _t("action|unignore") : _t("action|ignore")}
</AccessibleButton>
);
if (member instanceof RoomMember && member.roomId && !isSpace) { if (member instanceof RoomMember && member.roomId && !isSpace) {
const onReadReceiptButton = function (): void { const onReadReceiptButton = function (): void {
const room = cli.getRoom(member.roomId); const room = cli.getRoom(member.roomId);
@ -487,16 +465,28 @@ export const UserOptionsSection: React.FC<{
const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : undefined; const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : undefined;
if (room?.getEventReadUpTo(member.userId)) { if (room?.getEventReadUpTo(member.userId)) {
readReceiptButton = ( readReceiptButton = (
<AccessibleButton kind="link" onClick={onReadReceiptButton} className="mx_UserInfo_field"> <MenuItem
{_t("user_info|jump_to_rr_button")} role="button"
</AccessibleButton> onSelect={async (ev) => {
ev.preventDefault();
onReadReceiptButton();
}}
label={_t("user_info|jump_to_rr_button")}
Icon={CheckIcon}
/>
); );
} }
insertPillButton = ( insertPillButton = (
<AccessibleButton kind="link" onClick={onInsertPillButton} className="mx_UserInfo_field"> <MenuItem
{_t("action|mention")} role="button"
</AccessibleButton> onSelect={async (ev) => {
ev.preventDefault();
onInsertPillButton();
}}
label={_t("action|mention")}
Icon={MentionIcon}
/>
); );
} }
@ -507,7 +497,7 @@ export const UserOptionsSection: React.FC<{
shouldShowComponent(UIComponent.InviteUsers) shouldShowComponent(UIComponent.InviteUsers)
) { ) {
const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId(); const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId();
const onInviteUserButton = async (ev: ButtonEvent): Promise<void> => { const onInviteUserButton = async (ev: Event): Promise<void> => {
try { try {
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user. // We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
const inviter = new MultiInviter(cli, roomId || ""); const inviter = new MultiInviter(cli, roomId || "");
@ -538,34 +528,43 @@ export const UserOptionsSection: React.FC<{
}; };
inviteUserButton = ( inviteUserButton = (
<AccessibleButton kind="link" onClick={onInviteUserButton} className="mx_UserInfo_field"> <MenuItem
{_t("action|invite")} role="button"
</AccessibleButton> onSelect={async (ev) => {
ev.preventDefault();
onInviteUserButton(ev);
}}
label={_t("action|invite")}
Icon={InviteIcon}
/>
); );
} }
} }
const shareUserButton = ( const shareUserButton = (
<AccessibleButton kind="link" onClick={onShareUserClick} className="mx_UserInfo_field"> <MenuItem
{_t("user_info|share_button")} role="button"
</AccessibleButton> onSelect={async (ev) => {
ev.preventDefault();
onShareUserClick();
}}
label={_t("user_info|share_button")}
Icon={ShareIcon}
/>
); );
const directMessageButton = const directMessageButton =
isMe || !shouldShowComponent(UIComponent.CreateRooms) ? null : <MessageButton member={member} />; isMe || !shouldShowComponent(UIComponent.CreateRooms) ? null : <MessageButton member={member} />;
return ( return (
<div className="mx_UserInfo_container"> <Container>
<h3>{_t("common|options")}</h3> {children}
<div>
{directMessageButton} {directMessageButton}
{inviteUserButton}
{readReceiptButton} {readReceiptButton}
{shareUserButton} {shareUserButton}
{insertPillButton} {insertPillButton}
{inviteUserButton} </Container>
{ignoreButton}
</div>
</div>
); );
}; };
@ -586,15 +585,10 @@ export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
return !!confirmed; return !!confirmed;
}; };
const GenericAdminToolsContainer: React.FC<{ const Container: React.FC<{
children: ReactNode; children: ReactNode;
}> = ({ children }) => { }> = ({ children }) => {
return ( return <div className="mx_UserInfo_container">{children}</div>;
<div className="mx_UserInfo_container">
<h3>{_t("user_info|admin_tools_section")}</h3>
<div className="mx_UserInfo_buttons">{children}</div>
</div>
);
}; };
interface IPowerLevelsContent { interface IPowerLevelsContent {
@ -756,14 +750,17 @@ export const RoomKickButton = ({
: _t("user_info|kick_button_room"); : _t("user_info|kick_button_room");
return ( return (
<AccessibleButton <MenuItem
kind="link" role="button"
className="mx_UserInfo_field mx_UserInfo_destructive" onSelect={async (ev) => {
onClick={onKick} ev.preventDefault();
onKick();
}}
disabled={isUpdating} disabled={isUpdating}
> label={kickLabel}
{kickLabel} kind="critical"
</AccessibleButton> Icon={LeaveIcon}
/>
); );
}; };
@ -782,13 +779,16 @@ const RedactMessagesButton: React.FC<IBaseProps> = ({ member }) => {
}; };
return ( return (
<AccessibleButton <MenuItem
kind="link" role="button"
className="mx_UserInfo_field mx_UserInfo_destructive" onSelect={async (ev) => {
onClick={onRedactAllMessages} ev.preventDefault();
> onRedactAllMessages();
{_t("user_info|redact_button")} }}
</AccessibleButton> label={_t("user_info|redact_button")}
kind="critical"
Icon={CloseIcon}
/>
); );
}; };
@ -904,14 +904,18 @@ export const BanToggleButton = ({
label = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room"); label = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room");
} }
const classes = classNames("mx_UserInfo_field", {
mx_UserInfo_destructive: !isBanned,
});
return ( return (
<AccessibleButton kind="link" className={classes} onClick={onBanOrUnban} disabled={isUpdating}> <MenuItem
{label} role="button"
</AccessibleButton> onSelect={async (ev) => {
ev.preventDefault();
onBanOrUnban();
}}
disabled={isUpdating}
label={label}
kind="critical"
Icon={ChatProblemIcon}
/>
); );
}; };
@ -981,15 +985,81 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({
}); });
}; };
const classes = classNames("mx_UserInfo_field", {
mx_UserInfo_destructive: !muted,
});
const muteLabel = muted ? _t("common|unmute") : _t("common|mute"); const muteLabel = muted ? _t("common|unmute") : _t("common|mute");
return ( return (
<AccessibleButton kind="link" className={classes} onClick={onMuteToggle} disabled={isUpdating}> <MenuItem
{muteLabel} role="button"
</AccessibleButton> onSelect={async (ev) => {
ev.preventDefault();
onMuteToggle();
}}
disabled={isUpdating}
label={muteLabel}
kind="critical"
Icon={VisibilityOffIcon}
/>
);
};
const IgnoreToggleButton: React.FC<{
member: User | RoomMember;
}> = ({ member }) => {
const cli = useContext(MatrixClientContext);
const unignore = useCallback(() => {
const ignoredUsers = cli.getIgnoredUsers();
const index = ignoredUsers.indexOf(member.userId);
if (index !== -1) ignoredUsers.splice(index, 1);
cli.setIgnoredUsers(ignoredUsers);
}, [cli, member]);
const ignore = useCallback(async () => {
const name = (member instanceof User ? member.displayName : member.name) || member.userId;
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("user_info|ignore_confirm_title", { user: name }),
description: <div>{_t("user_info|ignore_confirm_description")}</div>,
button: _t("action|ignore"),
});
const [confirmed] = await finished;
if (confirmed) {
const ignoredUsers = cli.getIgnoredUsers();
ignoredUsers.push(member.userId);
cli.setIgnoredUsers(ignoredUsers);
}
}, [cli, member]);
// Check whether the user is ignored
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId));
// Recheck if the user or client changes
useEffect(() => {
setIsIgnored(cli.isUserIgnored(member.userId));
}, [cli, member.userId]);
// Recheck also if we receive new accountData m.ignored_user_list
const accountDataHandler = useCallback(
(ev) => {
if (ev.getType() === "m.ignored_user_list") {
setIsIgnored(cli.isUserIgnored(member.userId));
}
},
[cli, member.userId],
);
useTypedEventEmitter(cli, ClientEvent.AccountData, accountDataHandler);
return (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
if (isIgnored) {
unignore();
} else {
ignore();
}
}}
label={isIgnored ? _t("user_info|unignore_button") : _t("user_info|ignore_button")}
kind="critical"
Icon={BlockIcon}
/>
); );
}; };
@ -1070,13 +1140,13 @@ export const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
if (kickButton || banButton || muteButton || redactButton || children) { if (kickButton || banButton || muteButton || redactButton || children) {
return ( return (
<GenericAdminToolsContainer> <Container>
{muteButton} {muteButton}
{redactButton}
{kickButton} {kickButton}
{banButton} {banButton}
{redactButton}
{children} {children}
</GenericAdminToolsContainer> </Container>
); );
} }
@ -1352,23 +1422,6 @@ const BasicUserInfo: React.FC<{
// Load whether or not we are a Synapse Admin // Load whether or not we are a Synapse Admin
const isSynapseAdmin = useIsSynapseAdmin(cli); const isSynapseAdmin = useIsSynapseAdmin(cli);
// Check whether the user is ignored
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId));
// Recheck if the user or client changes
useEffect(() => {
setIsIgnored(cli.isUserIgnored(member.userId));
}, [cli, member.userId]);
// Recheck also if we receive new accountData m.ignored_user_list
const accountDataHandler = useCallback(
(ev) => {
if (ev.getType() === "m.ignored_user_list") {
setIsIgnored(cli.isUserIgnored(member.userId));
}
},
[cli, member.userId],
);
useTypedEventEmitter(cli, ClientEvent.AccountData, accountDataHandler);
// Count of how many operations are currently in progress, if > 0 then show a Spinner // Count of how many operations are currently in progress, if > 0 then show a Spinner
const [pendingUpdateCount, setPendingUpdateCount] = useState(0); const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
const startUpdating = useCallback(() => { const startUpdating = useCallback(() => {
@ -1412,13 +1465,16 @@ const BasicUserInfo: React.FC<{
// someone does figure out how to bypass this check the worst that happens is an error. // someone does figure out how to bypass this check the worst that happens is an error.
if (isSynapseAdmin && member.userId.endsWith(`:${cli.getDomain()}`)) { if (isSynapseAdmin && member.userId.endsWith(`:${cli.getDomain()}`)) {
synapseDeactivateButton = ( synapseDeactivateButton = (
<AccessibleButton <MenuItem
kind="link" role="button"
className="mx_UserInfo_field mx_UserInfo_destructive" onSelect={async (ev) => {
onClick={onSynapseDeactivate} ev.preventDefault();
> onSynapseDeactivate();
{_t("user_info|deactivate_confirm_action")} }}
</AccessibleButton> label={_t("user_info|deactivate_confirm_action")}
kind="critical"
Icon={DeleteIcon}
/>
); );
} }
@ -1428,23 +1484,12 @@ const BasicUserInfo: React.FC<{
// hide the Roles section for DMs as it doesn't make sense there // hide the Roles section for DMs as it doesn't make sense there
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) { if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
memberDetails = ( memberDetails = (
<div className="mx_UserInfo_container">
<h3>
{_t(
"user_info|role_label",
{},
{
RoomName: () => <b>{room.name}</b>,
},
)}
</h3>
<PowerLevelSection <PowerLevelSection
powerLevels={powerLevels} powerLevels={powerLevels}
user={member as RoomMember} user={member as RoomMember}
room={room} room={room}
roomPermissions={roomPermissions} roomPermissions={roomPermissions}
/> />
</div>
); );
} }
@ -1461,7 +1506,7 @@ const BasicUserInfo: React.FC<{
</RoomAdminToolsContainer> </RoomAdminToolsContainer>
); );
} else if (synapseDeactivateButton) { } else if (synapseDeactivateButton) {
adminToolsContainer = <GenericAdminToolsContainer>{synapseDeactivateButton}</GenericAdminToolsContainer>; adminToolsContainer = <Container>{synapseDeactivateButton}</Container>;
} }
if (pendingUpdateCount > 0) { if (pendingUpdateCount > 0) {
@ -1559,8 +1604,8 @@ const BasicUserInfo: React.FC<{
} }
const securitySection = ( const securitySection = (
<div className="mx_UserInfo_container"> <Container>
<h3>{_t("common|security")}</h3> <h2>{_t("common|security")}</h2>
<p>{text}</p> <p>{text}</p>
{verifyButton} {verifyButton}
{cryptoEnabled && ( {cryptoEnabled && (
@ -1572,23 +1617,29 @@ const BasicUserInfo: React.FC<{
/> />
)} )}
{editDevices} {editDevices}
</div> </Container>
); );
return ( return (
<React.Fragment> <React.Fragment>
{memberDetails}
{securitySection} {securitySection}
<UserOptionsSection <UserOptionsSection
canInvite={roomPermissions.canInvite} canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={member as RoomMember} member={member as RoomMember}
isSpace={room?.isSpaceRoom()} isSpace={room?.isSpaceRoom()}
/> >
{memberDetails}
</UserOptionsSection>
{adminToolsContainer} {adminToolsContainer}
{!isMe && (
<Container>
<IgnoreToggleButton member={member} />
</Container>
)}
{spinner} {spinner}
</React.Fragment> </React.Fragment>
); );
@ -1621,24 +1672,6 @@ export const UserInfoHeader: React.FC<{
const avatarUrl = (member as User).avatarUrl; const avatarUrl = (member as User).avatarUrl;
const avatarElement = (
<div className="mx_UserInfo_avatar">
<div className="mx_UserInfo_avatar_transition">
<div className="mx_UserInfo_avatar_transition_child">
<MemberAvatar
key={member.userId} // to instantly blank the avatar when UserInfo changes members
member={member as RoomMember}
size={UIStore.instance.windowHeight * 0.3 + "px"}
resizeMethod="scale"
fallbackUserId={member.userId}
onClick={onMemberAvatarClick}
urls={avatarUrl ? [avatarUrl] : undefined}
/>
</div>
</div>
</div>
);
let presenceState: string | undefined; let presenceState: string | undefined;
let presenceLastActiveAgo: number | undefined; let presenceLastActiveAgo: number | undefined;
let presenceCurrentlyActive: boolean | undefined; let presenceCurrentlyActive: boolean | undefined;
@ -1661,36 +1694,52 @@ export const UserInfoHeader: React.FC<{
activeAgo={presenceLastActiveAgo} activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive} currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} presenceState={presenceState}
className="mx_UserInfo_profileStatus"
coloured
/> />
); );
} }
const e2eIcon = e2eStatus ? <E2EIcon size={18} status={e2eStatus} isUser={true} /> : null; const e2eIcon = e2eStatus ? <E2EIcon size={18} status={e2eStatus} isUser={true} /> : null;
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
roomId,
withDisplayName: true,
});
const displayName = (member as RoomMember).rawDisplayName; const displayName = (member as RoomMember).rawDisplayName;
return ( return (
<React.Fragment> <React.Fragment>
{avatarElement} <div className="mx_UserInfo_avatar">
<div className="mx_UserInfo_avatar_transition">
<div className="mx_UserInfo_avatar_transition_child">
<MemberAvatar
key={member.userId} // to instantly blank the avatar when UserInfo changes members
member={member as RoomMember}
size="120px"
resizeMethod="scale"
fallbackUserId={member.userId}
onClick={onMemberAvatarClick}
urls={avatarUrl ? [avatarUrl] : undefined}
/>
</div>
</div>
</div>
<div className="mx_UserInfo_container mx_UserInfo_separator"> <Container>
<div className="mx_UserInfo_profile"> <Flex direction="column" align="center" className="mx_UserInfo_profile">
<div> <Heading size="sm" weight="semibold" as="h1" dir="auto">
<h2> <Flex direction="row-reverse" align="center">
<span title={displayName} aria-label={displayName} dir="auto">
{displayName} {displayName}
</span>
{e2eIcon} {e2eIcon}
</h2> </Flex>
</div> </Heading>
<div className="mx_UserInfo_profile_mxid"> {presenceLabel}
{UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, { <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
roomId, <CopyableText getTextToCopy={() => userIdentifier} border={false}>
withDisplayName: true, {userIdentifier}
})} </CopyableText>
</div> </Text>
<div className="mx_UserInfo_profileStatus">{presenceLabel}</div> </Flex>
</div> </Container>
</div>
</React.Fragment> </React.Fragment>
); );
}; };

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
import classNames from "classnames";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { formatDuration } from "../../../DateUtils"; import { formatDuration } from "../../../DateUtils";
@ -31,6 +32,9 @@ interface IProps {
currentlyActive?: boolean; currentlyActive?: boolean;
// offline, online, etc // offline, online, etc
presenceState?: string; presenceState?: string;
// whether to apply colouring to the label
coloured?: boolean;
className?: string;
} }
export default class PresenceLabel extends React.Component<IProps> { export default class PresenceLabel extends React.Component<IProps> {
@ -62,7 +66,11 @@ export default class PresenceLabel extends React.Component<IProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<div className="mx_PresenceLabel"> <div
className={classNames("mx_PresenceLabel", this.props.className, {
mx_PresenceLabel_online: this.props.coloured && this.props.presenceState === "online",
})}
>
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)} {this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
</div> </div>
); );

View file

@ -3770,6 +3770,7 @@
"error_revoke_3pid_invite_title": "Failed to revoke invite", "error_revoke_3pid_invite_title": "Failed to revoke invite",
"hide_sessions": "Hide sessions", "hide_sessions": "Hide sessions",
"hide_verified_sessions": "Hide verified sessions", "hide_verified_sessions": "Hide verified sessions",
"ignore_button": "Ignore",
"ignore_confirm_description": "All messages and invites from this user will be hidden. Are you sure you want to ignore them?", "ignore_confirm_description": "All messages and invites from this user will be hidden. Are you sure you want to ignore them?",
"ignore_confirm_title": "Ignore %(user)s", "ignore_confirm_title": "Ignore %(user)s",
"invited_by": "Invited by %(sender)s", "invited_by": "Invited by %(sender)s",
@ -3797,20 +3798,21 @@
"no_recent_messages_description": "Try scrolling up in the timeline to see if there are any earlier ones.", "no_recent_messages_description": "Try scrolling up in the timeline to see if there are any earlier ones.",
"no_recent_messages_title": "No recent messages by %(user)s found" "no_recent_messages_title": "No recent messages by %(user)s found"
}, },
"redact_button": "Remove recent messages", "redact_button": "Remove messages",
"revoke_invite": "Revoke invite", "revoke_invite": "Revoke invite",
"role_label": "Role in <RoomName/>",
"room_encrypted": "Messages in this room are end-to-end encrypted.", "room_encrypted": "Messages in this room are end-to-end encrypted.",
"room_encrypted_detail": "Your messages are secured and only you and the recipient have the unique keys to unlock them.", "room_encrypted_detail": "Your messages are secured and only you and the recipient have the unique keys to unlock them.",
"room_unencrypted": "Messages in this room are not end-to-end encrypted.", "room_unencrypted": "Messages in this room are not end-to-end encrypted.",
"room_unencrypted_detail": "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.", "room_unencrypted_detail": "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.",
"share_button": "Share Link to User", "send_message": "Send message",
"share_button": "Share profile",
"unban_button_room": "Unban from room", "unban_button_room": "Unban from room",
"unban_button_space": "Unban from space", "unban_button_space": "Unban from space",
"unban_room_confirm_title": "Unban from %(roomName)s", "unban_room_confirm_title": "Unban from %(roomName)s",
"unban_space_everything": "Unban them from everything I'm able to", "unban_space_everything": "Unban them from everything I'm able to",
"unban_space_specific": "Unban them from specific things I'm able to", "unban_space_specific": "Unban them from specific things I'm able to",
"unban_space_warning": "They won't be able to access whatever you're not an admin of.", "unban_space_warning": "They won't be able to access whatever you're not an admin of.",
"unignore_button": "Unignore",
"verify_button": "Verify User", "verify_button": "Verify User",
"verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices." "verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices."
}, },

View file

@ -56,6 +56,9 @@ import { clearAllModals, flushPromises } from "../../../test-utils";
import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog"; import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog";
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents"; import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../src/settings/UIFeature"; import { UIComponent } from "../../../../src/settings/UIFeature";
import { Action } from "../../../../src/dispatcher/actions";
import ShareDialog from "../../../../src/components/views/dialogs/ShareDialog";
import BulkRedactDialog from "../../../../src/components/views/dialogs/BulkRedactDialog";
jest.mock("../../../../src/utils/direct-messages", () => ({ jest.mock("../../../../src/utils/direct-messages", () => ({
...jest.requireActual("../../../../src/utils/direct-messages"), ...jest.requireActual("../../../../src/utils/direct-messages"),
@ -323,7 +326,7 @@ describe("<UserInfo />", () => {
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
screen.getByRole("button", { name: "Message" }); screen.getByRole("button", { name: "Send message" });
}); });
it("hides the message button if the visibility customisation hides all create room features", () => { it("hides the message button if the visibility customisation hides all create room features", () => {
@ -342,6 +345,64 @@ describe("<UserInfo />", () => {
}, },
); );
}); });
describe("Ignore", () => {
const member = new RoomMember(defaultRoomId, defaultUserId);
it("shows block button when member userId does not match client userId", () => {
// call to client.getUserId returns undefined, which will not match member.userId
renderComponent();
expect(screen.getByRole("button", { name: "Ignore" })).toBeInTheDocument();
});
it("shows a modal before ignoring the user", async () => {
const originalCreateDialog = Modal.createDialog;
const modalSpy = (Modal.createDialog = jest.fn().mockReturnValue({
finished: Promise.resolve([true]),
close: () => {},
}));
try {
mockClient.getIgnoredUsers.mockReturnValue([]);
renderComponent();
await userEvent.click(screen.getByRole("button", { name: "Ignore" }));
expect(modalSpy).toHaveBeenCalled();
expect(mockClient.setIgnoredUsers).toHaveBeenLastCalledWith([member.userId]);
} finally {
Modal.createDialog = originalCreateDialog;
}
});
it("cancels ignoring the user", async () => {
const originalCreateDialog = Modal.createDialog;
const modalSpy = (Modal.createDialog = jest.fn().mockReturnValue({
finished: Promise.resolve([false]),
close: () => {},
}));
try {
mockClient.getIgnoredUsers.mockReturnValue([]);
renderComponent();
await userEvent.click(screen.getByRole("button", { name: "Ignore" }));
expect(modalSpy).toHaveBeenCalled();
expect(mockClient.setIgnoredUsers).not.toHaveBeenCalled();
} finally {
Modal.createDialog = originalCreateDialog;
}
});
it("unignores the user", async () => {
mockClient.isUserIgnored.mockReturnValue(true);
mockClient.getIgnoredUsers.mockReturnValue([member.userId]);
renderComponent();
await userEvent.click(screen.getByRole("button", { name: "Unignore" }));
expect(mockClient.setIgnoredUsers).toHaveBeenCalledWith([]);
});
});
}); });
describe("with crypto enabled", () => { describe("with crypto enabled", () => {
@ -801,7 +862,7 @@ describe("<DeviceItem />", () => {
describe("<UserOptionsSection />", () => { describe("<UserOptionsSection />", () => {
const member = new RoomMember(defaultRoomId, defaultUserId); const member = new RoomMember(defaultRoomId, defaultUserId);
const defaultProps = { member, isIgnored: false, canInvite: false, isSpace: false }; const defaultProps = { member, canInvite: false, isSpace: false };
const renderComponent = (props = {}) => { const renderComponent = (props = {}) => {
const Wrapper = (wrapperProps = {}) => { const Wrapper = (wrapperProps = {}) => {
@ -828,9 +889,13 @@ describe("<UserOptionsSection />", () => {
inviteSpy.mockRestore(); inviteSpy.mockRestore();
}); });
it("always shows share user button", () => { it("always shows share user button and clicking it should produce a ShareDialog", async () => {
const spy = jest.spyOn(Modal, "createDialog");
renderComponent(); renderComponent();
expect(screen.getByRole("button", { name: /share link to user/i })).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Share profile" }));
expect(spy).toHaveBeenCalledWith(ShareDialog, { target: defaultProps.member });
}); });
it("does not show ignore or direct message buttons when member userId matches client userId", () => { it("does not show ignore or direct message buttons when member userId matches client userId", () => {
@ -842,20 +907,31 @@ describe("<UserOptionsSection />", () => {
expect(screen.queryByRole("button", { name: /message/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /message/i })).not.toBeInTheDocument();
}); });
it("shows ignore, direct message and mention buttons when member userId does not match client userId", () => { it("shows direct message and mention buttons when member userId does not match client userId", () => {
// call to client.getUserId returns undefined, which will not match member.userId // call to client.getUserId returns undefined, which will not match member.userId
renderComponent(); renderComponent();
expect(screen.getByRole("button", { name: /ignore/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Send message" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /message/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Mention" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /mention/i })).toBeInTheDocument(); });
it("mention button fires ComposerInsert Action", async () => {
renderComponent();
const button = screen.getByRole("button", { name: "Mention" });
await userEvent.click(button);
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ComposerInsert,
timelineRenderingType: "Room",
userId: "@user:example.com",
});
}); });
it("when call to client.getRoom is null, does not show read receipt button", () => { it("when call to client.getRoom is null, does not show read receipt button", () => {
mockClient.getRoom.mockReturnValueOnce(null); mockClient.getRoom.mockReturnValueOnce(null);
renderComponent(); renderComponent();
expect(screen.queryByRole("button", { name: /jump to read receipt/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Jump to read receipt" })).not.toBeInTheDocument();
}); });
it("when call to client.getRoom is non-null and room.getEventReadUpTo is null, does not show read receipt button", () => { it("when call to client.getRoom is non-null and room.getEventReadUpTo is null, does not show read receipt button", () => {
@ -863,7 +939,7 @@ describe("<UserOptionsSection />", () => {
mockClient.getRoom.mockReturnValueOnce(mockRoom); mockClient.getRoom.mockReturnValueOnce(mockRoom);
renderComponent(); renderComponent();
expect(screen.queryByRole("button", { name: /jump to read receipt/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Jump to read receipt" })).not.toBeInTheDocument();
}); });
it("when calls to client.getRoom and room.getEventReadUpTo are non-null, shows read receipt button", () => { it("when calls to client.getRoom and room.getEventReadUpTo are non-null, shows read receipt button", () => {
@ -871,7 +947,7 @@ describe("<UserOptionsSection />", () => {
mockClient.getRoom.mockReturnValueOnce(mockRoom); mockClient.getRoom.mockReturnValueOnce(mockRoom);
renderComponent(); renderComponent();
expect(screen.getByRole("button", { name: /jump to read receipt/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Jump to read receipt" })).toBeInTheDocument();
}); });
it("clicking the read receipt button calls dispatch with correct event_id", async () => { it("clicking the read receipt button calls dispatch with correct event_id", async () => {
@ -880,7 +956,7 @@ describe("<UserOptionsSection />", () => {
mockClient.getRoom.mockReturnValue(mockRoom); mockClient.getRoom.mockReturnValue(mockRoom);
renderComponent(); renderComponent();
const readReceiptButton = screen.getByRole("button", { name: /jump to read receipt/i }); const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
expect(readReceiptButton).toBeInTheDocument(); expect(readReceiptButton).toBeInTheDocument();
await userEvent.click(readReceiptButton); await userEvent.click(readReceiptButton);
@ -904,7 +980,7 @@ describe("<UserOptionsSection />", () => {
mockClient.getRoom.mockReturnValue(mockRoom); mockClient.getRoom.mockReturnValue(mockRoom);
renderComponent(); renderComponent();
const readReceiptButton = screen.getByRole("button", { name: /jump to read receipt/i }); const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
expect(readReceiptButton).toBeInTheDocument(); expect(readReceiptButton).toBeInTheDocument();
await userEvent.click(readReceiptButton); await userEvent.click(readReceiptButton);
@ -964,52 +1040,6 @@ describe("<UserOptionsSection />", () => {
}); });
}); });
it("shows a modal before ignoring the user", async () => {
const originalCreateDialog = Modal.createDialog;
const modalSpy = (Modal.createDialog = jest.fn().mockReturnValue({
finished: Promise.resolve([true]),
close: () => {},
}));
try {
mockClient.getIgnoredUsers.mockReturnValue([]);
renderComponent({ isIgnored: false });
await userEvent.click(screen.getByRole("button", { name: "Ignore" }));
expect(modalSpy).toHaveBeenCalled();
expect(mockClient.setIgnoredUsers).toHaveBeenLastCalledWith([member.userId]);
} finally {
Modal.createDialog = originalCreateDialog;
}
});
it("cancels ignoring the user", async () => {
const originalCreateDialog = Modal.createDialog;
const modalSpy = (Modal.createDialog = jest.fn().mockReturnValue({
finished: Promise.resolve([false]),
close: () => {},
}));
try {
mockClient.getIgnoredUsers.mockReturnValue([]);
renderComponent({ isIgnored: false });
await userEvent.click(screen.getByRole("button", { name: "Ignore" }));
expect(modalSpy).toHaveBeenCalled();
expect(mockClient.setIgnoredUsers).not.toHaveBeenCalled();
} finally {
Modal.createDialog = originalCreateDialog;
}
});
it("unignores the user", async () => {
mockClient.getIgnoredUsers.mockReturnValue([member.userId]);
renderComponent({ isIgnored: true });
await userEvent.click(screen.getByRole("button", { name: "Unignore" }));
expect(mockClient.setIgnoredUsers).toHaveBeenCalledWith([]);
});
it.each([ it.each([
["for a RoomMember", member, member.getMxcAvatarUrl()], ["for a RoomMember", member, member.getMxcAvatarUrl()],
["for a User", defaultUser, defaultUser.avatarUrl], ["for a User", defaultUser, defaultUser.avatarUrl],
@ -1020,10 +1050,10 @@ describe("<UserOptionsSection />", () => {
mocked(startDmOnFirstMessage).mockReturnValue(deferred.promise); mocked(startDmOnFirstMessage).mockReturnValue(deferred.promise);
renderComponent({ member }); renderComponent({ member });
await userEvent.click(screen.getByText("Message")); await userEvent.click(screen.getByRole("button", { name: "Send message" }));
// Checking the attribute, because the button is a DIV and toBeDisabled() does not work. // Checking the attribute, because the button is a DIV and toBeDisabled() does not work.
expect(screen.getByText("Message")).toHaveAttribute("disabled"); expect(screen.getByRole("button", { name: "Send message" })).toBeDisabled();
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [ expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
new DirectoryMember({ new DirectoryMember({
@ -1039,7 +1069,7 @@ describe("<UserOptionsSection />", () => {
}); });
// Checking the attribute, because the button is a DIV and toBeDisabled() does not work. // Checking the attribute, because the button is a DIV and toBeDisabled() does not work.
expect(screen.getByText("Message")).not.toHaveAttribute("disabled"); expect(screen.getByRole("button", { name: "Send message" })).not.toBeDisabled();
}, },
); );
}); });
@ -1396,10 +1426,30 @@ describe("<RoomAdminToolsContainer />", () => {
renderComponent({ member: defaultMemberWithPowerLevel }); renderComponent({ member: defaultMemberWithPowerLevel });
expect(screen.getByRole("heading", { name: /admin tools/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Disinvite from room" })).toBeInTheDocument();
expect(screen.getByText(/disinvite from room/i)).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Ban from room" })).toBeInTheDocument();
expect(screen.getByText(/ban from room/i)).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Remove messages" })).toBeInTheDocument();
expect(screen.getByText(/remove recent messages/i)).toBeInTheDocument(); });
it("should show BulkRedactDialog upon clicking the Remove messages button", async () => {
const spy = jest.spyOn(Modal, "createDialog");
mockClient.getRoom.mockReturnValue(mockRoom);
mockClient.getUserId.mockReturnValue("@arbitraryId:server");
const mockMeMember = new RoomMember(mockRoom.roomId, mockClient.getUserId()!);
mockMeMember.powerLevel = 51; // defaults to 50
const defaultMemberWithPowerLevel = { ...defaultMember, powerLevel: 0 } as RoomMember;
mockRoom.getMember.mockImplementation((userId) =>
userId === mockClient.getUserId() ? mockMeMember : defaultMemberWithPowerLevel,
);
renderComponent({ member: defaultMemberWithPowerLevel });
await userEvent.click(screen.getByRole("button", { name: "Remove messages" }));
expect(spy).toHaveBeenCalledWith(
BulkRedactDialog,
expect.objectContaining({ member: defaultMemberWithPowerLevel }),
);
}); });
it("returns mute toggle button if conditions met", () => { it("returns mute toggle button if conditions met", () => {
@ -1441,10 +1491,9 @@ describe("<RoomAdminToolsContainer />", () => {
isUpdating: true, isUpdating: true,
}); });
const button = screen.getByText(/mute/i); const button = screen.getByRole("button", { name: "Mute" });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
expect(button).toHaveAttribute("disabled"); expect(button).toBeDisabled();
expect(button).toHaveAttribute("aria-disabled", "true");
}); });
it("should not show mute button for one's own member", () => { it("should not show mute button for one's own member", () => {

View file

@ -118,7 +118,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
data-testid="avatar-img" data-testid="avatar-img"
data-type="round" data-type="round"
role="button" role="button"
style="--cpd-avatar-size: 230.39999999999998px;" style="--cpd-avatar-size: 120px;"
> >
u u
</button> </button>
@ -126,44 +126,51 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
</div> </div>
</div> </div>
<div <div
class="mx_UserInfo_container mx_UserInfo_separator" class="mx_UserInfo_container"
> >
<div <div
class="mx_UserInfo_profile" class="mx_Flex mx_UserInfo_profile"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
> >
<div> <h1
<h2> class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
<span
aria-label="@user:example.com"
dir="auto" dir="auto"
title="@user:example.com" >
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
> >
@user:example.com @user:example.com
</span>
</h2>
</div> </div>
</h1>
<div <div
class="mx_UserInfo_profile_mxid" class="mx_PresenceLabel mx_UserInfo_profileStatus"
>
customUserIdentifier
</div>
<div
class="mx_UserInfo_profileStatus"
>
<div
class="mx_PresenceLabel"
> >
Unknown Unknown
</div> </div>
<p
class="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 mx_UserInfo_profile_mxid"
>
<div
class="mx_CopyableText"
>
customUserIdentifier
<div
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div> </div>
</p>
</div> </div>
</div> </div>
<div <div
class="mx_UserInfo_container" class="mx_UserInfo_container"
> >
<h3> <h2>
Security Security
</h3> </h2>
<p> <p>
Messages in this room are not end-to-end encrypted. Messages in this room are not end-to-end encrypted.
</p> </p>
@ -201,32 +208,100 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
<div <div
class="mx_UserInfo_container" class="mx_UserInfo_container"
> >
<h3> <button
Options class="_item_1gwvj_17 _interactive_1gwvj_36"
</h3> data-kind="primary"
<div>
<div
class="mx_AccessibleButton mx_UserInfo_field mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button" role="button"
tabindex="0"
> >
Message <div
aria-hidden="true"
class="_icon_1gwvj_44"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
>
Send message
</span>
<svg
aria-hidden="true"
class="_nav-hint_1gwvj_60"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</button>
<button
class="_item_1gwvj_17 _interactive_1gwvj_36"
data-kind="primary"
role="button"
>
<div
aria-hidden="true"
class="_icon_1gwvj_44"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
>
Share profile
</span>
<svg
aria-hidden="true"
class="_nav-hint_1gwvj_60"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</button>
</div> </div>
<div <div
class="mx_AccessibleButton mx_UserInfo_field mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link" class="mx_UserInfo_container"
role="button"
tabindex="0"
> >
Share Link to User <button
</div> class="_item_1gwvj_17 _interactive_1gwvj_36"
<div data-kind="critical"
class="mx_AccessibleButton mx_UserInfo_field mx_UserInfo_destructive mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button" role="button"
tabindex="0" >
<div
aria-hidden="true"
class="_icon_1gwvj_44"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
> >
Ignore Ignore
</div> </span>
</div> <svg
aria-hidden="true"
class="_nav-hint_1gwvj_60"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -282,7 +357,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
data-testid="avatar-img" data-testid="avatar-img"
data-type="round" data-type="round"
role="button" role="button"
style="--cpd-avatar-size: 230.39999999999998px;" style="--cpd-avatar-size: 120px;"
> >
u u
</button> </button>
@ -290,44 +365,51 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
</div> </div>
</div> </div>
<div <div
class="mx_UserInfo_container mx_UserInfo_separator" class="mx_UserInfo_container"
> >
<div <div
class="mx_UserInfo_profile" class="mx_Flex mx_UserInfo_profile"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
> >
<div> <h1
<h2> class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
<span
aria-label="@user:example.com"
dir="auto" dir="auto"
title="@user:example.com" >
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
> >
@user:example.com @user:example.com
</span>
</h2>
</div> </div>
</h1>
<div <div
class="mx_UserInfo_profile_mxid" class="mx_PresenceLabel mx_UserInfo_profileStatus"
>
customUserIdentifier
</div>
<div
class="mx_UserInfo_profileStatus"
>
<div
class="mx_PresenceLabel"
> >
Unknown Unknown
</div> </div>
<p
class="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 mx_UserInfo_profile_mxid"
>
<div
class="mx_CopyableText"
>
customUserIdentifier
<div
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div> </div>
</p>
</div> </div>
</div> </div>
<div <div
class="mx_UserInfo_container" class="mx_UserInfo_container"
> >
<h3> <h2>
Security Security
</h3> </h2>
<p> <p>
Messages in this room are not end-to-end encrypted. Messages in this room are not end-to-end encrypted.
</p> </p>
@ -365,50 +447,134 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
<div <div
class="mx_UserInfo_container" class="mx_UserInfo_container"
> >
<h3> <button
Options class="_item_1gwvj_17 _interactive_1gwvj_36"
</h3> data-kind="primary"
<div>
<div
class="mx_AccessibleButton mx_UserInfo_field mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button" role="button"
tabindex="0"
> >
Message
</div>
<div <div
class="mx_AccessibleButton mx_UserInfo_field mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link" aria-hidden="true"
role="button" class="_icon_1gwvj_44"
tabindex="0" height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
>
Send message
</span>
<svg
aria-hidden="true"
class="_nav-hint_1gwvj_60"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</button>
<button
class="_item_1gwvj_17 _interactive_1gwvj_36"
data-kind="primary"
role="button"
> >
Share Link to User
</div>
<div <div
class="mx_AccessibleButton mx_UserInfo_field mx_UserInfo_destructive mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link" aria-hidden="true"
role="button" class="_icon_1gwvj_44"
tabindex="0" height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
> >
Ignore Share profile
</div> </span>
</div> <svg
aria-hidden="true"
class="_nav-hint_1gwvj_60"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</button>
</div> </div>
<div <div
class="mx_UserInfo_container" class="mx_UserInfo_container"
> >
<h3> <button
Admin Tools class="_item_1gwvj_17 _interactive_1gwvj_36"
</h3> data-kind="critical"
<div role="button"
class="mx_UserInfo_buttons"
> >
<div <div
class="mx_AccessibleButton mx_UserInfo_field mx_UserInfo_destructive mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link" aria-hidden="true"
role="button" class="_icon_1gwvj_44"
tabindex="0" height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
> >
Deactivate user Deactivate user
</span>
<svg
aria-hidden="true"
class="_nav-hint_1gwvj_60"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</button>
</div> </div>
</div> <div
class="mx_UserInfo_container"
>
<button
class="_item_1gwvj_17 _interactive_1gwvj_36"
data-kind="critical"
role="button"
>
<div
aria-hidden="true"
class="_icon_1gwvj_44"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
>
Ignore
</span>
<svg
aria-hidden="true"
class="_nav-hint_1gwvj_60"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>