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:
parent
2920e76b64
commit
f706ac4fa1
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 |
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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."
|
||||||
},
|
},
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue