Remove old code
parent
3561aecaa4
commit
c27e2fb00f
|
@ -1,128 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_EntityTile {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: $primary-content;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.mx_E2EIcon {
|
|
||||||
margin: 0;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
right: 7px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile:hover {
|
|
||||||
padding-right: 30px;
|
|
||||||
position: relative; /* to keep the chevron aligned */
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile:hover::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: calc(50% - 8px); /* center */
|
|
||||||
right: -8px;
|
|
||||||
mask: url("@vector-im/compound-design-tokens/icons/chevron-right.svg");
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-color: $header-panel-text-primary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile:not(.mx_EntityTile_unreachable) .mx_PresenceLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile:hover .mx_PresenceLabel {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_invite {
|
|
||||||
display: table-cell;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-left: 10px;
|
|
||||||
width: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_avatar {
|
|
||||||
padding-left: 3px;
|
|
||||||
padding-right: 12px;
|
|
||||||
padding-top: 4px;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
position: relative;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_name {
|
|
||||||
flex: 1 1 0;
|
|
||||||
overflow: hidden;
|
|
||||||
font: var(--cpd-font-body-md-regular);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_details {
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_ellipsis .mx_EntityTile_name {
|
|
||||||
font-style: italic;
|
|
||||||
color: $primary-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_invitePlaceholder .mx_EntityTile_name {
|
|
||||||
font-style: italic;
|
|
||||||
color: $primary-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_unavailable .mx_EntityTile_avatar,
|
|
||||||
.mx_EntityTile_unavailable .mx_EntityTile_name,
|
|
||||||
.mx_EntityTile_offline_beenactive .mx_EntityTile_avatar,
|
|
||||||
.mx_EntityTile_offline_beenactive .mx_EntityTile_name {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_offline_neveractive .mx_EntityTile_avatar,
|
|
||||||
.mx_EntityTile_offline_neveractive .mx_EntityTile_name {
|
|
||||||
opacity: 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_unknown .mx_EntityTile_avatar,
|
|
||||||
.mx_EntityTile_unknown .mx_EntityTile_name,
|
|
||||||
.mx_EntityTile_unreachable .mx_EntityTile_avatar,
|
|
||||||
.mx_EntityTile_unreachable .mx_EntityTile_name {
|
|
||||||
opacity: 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_subtext {
|
|
||||||
font-size: $font-11px;
|
|
||||||
opacity: 0.5;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile_power {
|
|
||||||
padding-inline-start: 6px;
|
|
||||||
font-size: $font-10px;
|
|
||||||
color: $secondary-content;
|
|
||||||
max-width: 6em;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EntityTile:hover .mx_EntityTile_power {
|
|
||||||
display: none;
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_MemberList {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
.mx_Spinner {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SearchBox {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $h3-color;
|
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
|
||||||
font-size: $font-13px;
|
|
||||||
padding-left: 3px;
|
|
||||||
padding-right: 12px;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AutoHideScrollbar {
|
|
||||||
flex: 1 1 0;
|
|
||||||
margin-top: var(--cpd-space-3x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MemberList_chevron {
|
|
||||||
position: absolute;
|
|
||||||
right: 35px;
|
|
||||||
margin-top: -15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MemberList_border {
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
order: 1;
|
|
||||||
flex: 1 1 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MemberList_query {
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
/* stricter rule to override the one in _common.pcss */
|
|
||||||
&[type="text"] {
|
|
||||||
font-size: $font-12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MemberList_wrapper {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MemberList_invite {
|
|
||||||
margin: 0 var(--cpd-space-2x);
|
|
||||||
width: calc(100% - var(--cpd-space-4x));
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import { _t, _td, TranslationKey } from "../../../languageHandler";
|
|
||||||
import E2EIcon, { E2EState } from "./E2EIcon";
|
|
||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
|
||||||
import PresenceLabel from "./PresenceLabel";
|
|
||||||
|
|
||||||
export enum PowerStatus {
|
|
||||||
Admin = "admin",
|
|
||||||
Moderator = "moderator",
|
|
||||||
}
|
|
||||||
|
|
||||||
const PowerLabel: Record<PowerStatus, TranslationKey> = {
|
|
||||||
[PowerStatus.Admin]: _td("power_level|admin"),
|
|
||||||
[PowerStatus.Moderator]: _td("power_level|mod"),
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PresenceState = "offline" | "online" | "unavailable" | "io.element.unreachable";
|
|
||||||
|
|
||||||
const PRESENCE_CLASS: Record<PresenceState, string> = {
|
|
||||||
"offline": "mx_EntityTile_offline",
|
|
||||||
"online": "mx_EntityTile_online",
|
|
||||||
"unavailable": "mx_EntityTile_unavailable",
|
|
||||||
"io.element.unreachable": "mx_EntityTile_unreachable",
|
|
||||||
};
|
|
||||||
|
|
||||||
function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string {
|
|
||||||
if (showPresence === false) {
|
|
||||||
return "mx_EntityTile_online_beenactive";
|
|
||||||
}
|
|
||||||
|
|
||||||
// offline is split into two categories depending on whether we have
|
|
||||||
// a last_active_ago for them.
|
|
||||||
if (presenceState === "offline") {
|
|
||||||
if (lastActiveAgo) {
|
|
||||||
return PRESENCE_CLASS["offline"] + "_beenactive";
|
|
||||||
} else {
|
|
||||||
return PRESENCE_CLASS["offline"] + "_neveractive";
|
|
||||||
}
|
|
||||||
} else if (presenceState) {
|
|
||||||
return PRESENCE_CLASS[presenceState];
|
|
||||||
} else {
|
|
||||||
return PRESENCE_CLASS["offline"] + "_neveractive";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
name?: string;
|
|
||||||
nameJSX?: JSX.Element;
|
|
||||||
title?: string;
|
|
||||||
avatarJsx?: JSX.Element; // <BaseAvatar />
|
|
||||||
className?: string;
|
|
||||||
presenceState: PresenceState;
|
|
||||||
presenceLastActiveAgo: number;
|
|
||||||
presenceLastTs: number;
|
|
||||||
presenceCurrentlyActive?: boolean;
|
|
||||||
onClick(): void;
|
|
||||||
showPresence: boolean;
|
|
||||||
subtextLabel?: string;
|
|
||||||
e2eStatus?: E2EState;
|
|
||||||
powerStatus?: PowerStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
hover: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class EntityTile extends React.PureComponent<IProps, IState> {
|
|
||||||
public static defaultProps = {
|
|
||||||
onClick: () => {},
|
|
||||||
presenceState: "offline",
|
|
||||||
presenceLastActiveAgo: 0,
|
|
||||||
presenceLastTs: 0,
|
|
||||||
showInviteButton: false,
|
|
||||||
showPresence: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hover: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the PresenceLabel component if needed
|
|
||||||
* @returns The PresenceLabel component if we need to render it, undefined otherwise
|
|
||||||
*/
|
|
||||||
private getPresenceLabel(): JSX.Element | undefined {
|
|
||||||
if (!this.props.showPresence) return;
|
|
||||||
const activeAgo = this.props.presenceLastActiveAgo
|
|
||||||
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
|
|
||||||
: -1;
|
|
||||||
return (
|
|
||||||
<PresenceLabel
|
|
||||||
activeAgo={activeAgo}
|
|
||||||
currentlyActive={this.props.presenceCurrentlyActive}
|
|
||||||
presenceState={this.props.presenceState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const mainClassNames: Record<string, boolean> = {
|
|
||||||
mx_EntityTile: true,
|
|
||||||
};
|
|
||||||
if (this.props.className) mainClassNames[this.props.className] = true;
|
|
||||||
|
|
||||||
const presenceClass = presenceClassForMember(
|
|
||||||
this.props.presenceState,
|
|
||||||
this.props.presenceLastActiveAgo,
|
|
||||||
this.props.showPresence,
|
|
||||||
);
|
|
||||||
mainClassNames[presenceClass] = true;
|
|
||||||
|
|
||||||
const name = this.props.nameJSX || this.props.name;
|
|
||||||
const nameAndPresence = (
|
|
||||||
<div className="mx_EntityTile_details">
|
|
||||||
<div className="mx_EntityTile_name">{name}</div>
|
|
||||||
{this.getPresenceLabel()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
let powerLabel;
|
|
||||||
const powerStatus = this.props.powerStatus;
|
|
||||||
if (powerStatus) {
|
|
||||||
const powerText = _t(PowerLabel[powerStatus]);
|
|
||||||
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let e2eIcon;
|
|
||||||
const { e2eStatus } = this.props;
|
|
||||||
if (e2eStatus) {
|
|
||||||
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} size="36px" aria-hidden="true" />;
|
|
||||||
|
|
||||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<AccessibleButton
|
|
||||||
className={classNames(mainClassNames)}
|
|
||||||
title={this.props.title}
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
<div className="mx_EntityTile_avatar">
|
|
||||||
{av}
|
|
||||||
{e2eIcon}
|
|
||||||
</div>
|
|
||||||
{nameAndPresence}
|
|
||||||
{powerLabel}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,450 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
MatrixEvent,
|
|
||||||
Room,
|
|
||||||
RoomEvent,
|
|
||||||
RoomMember,
|
|
||||||
RoomMemberEvent,
|
|
||||||
RoomState,
|
|
||||||
RoomStateEvent,
|
|
||||||
User,
|
|
||||||
UserEvent,
|
|
||||||
EventType,
|
|
||||||
ClientEvent,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
|
||||||
import { throttle } from "lodash";
|
|
||||||
import { Button, Tooltip } from "@vector-im/compound-web";
|
|
||||||
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
|
|
||||||
// eslint-disable-next-line no-restricted-imports
|
|
||||||
import OverflowHorizontalSvg from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import BaseCard from "../right_panel/BaseCard";
|
|
||||||
import TruncatedList from "../elements/TruncatedList";
|
|
||||||
import Spinner from "../elements/Spinner";
|
|
||||||
import SearchBox from "../../structures/SearchBox";
|
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
|
||||||
import EntityTile from "./EntityTile";
|
|
||||||
import MemberTile from "./MemberTile";
|
|
||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
|
||||||
import { UIComponent } from "../../../settings/UIFeature";
|
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
|
||||||
import { SDKContext } from "../../../contexts/SDKContext";
|
|
||||||
import { canInviteTo } from "../../../utils/room/canInviteTo";
|
|
||||||
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
|
||||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
|
||||||
const SHOW_MORE_INCREMENT = 100;
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
roomId: string;
|
|
||||||
searchQuery: string;
|
|
||||||
onClose(): void;
|
|
||||||
onSearchQueryChanged: (query: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
loading: boolean;
|
|
||||||
filteredJoinedMembers: Array<RoomMember>;
|
|
||||||
filteredInvitedMembers: Array<RoomMember | MatrixEvent>;
|
|
||||||
canInvite: boolean;
|
|
||||||
truncateAtJoined: number;
|
|
||||||
truncateAtInvited: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MemberList extends React.Component<IProps, IState> {
|
|
||||||
private readonly showPresence: boolean;
|
|
||||||
private unmounted = false;
|
|
||||||
|
|
||||||
public static contextType = SDKContext;
|
|
||||||
declare public context: React.ContextType<typeof SDKContext>;
|
|
||||||
private tiles: Map<string, MemberTile> = new Map();
|
|
||||||
|
|
||||||
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
|
||||||
super(props, context);
|
|
||||||
this.state = this.getMembersState([], []);
|
|
||||||
this.showPresence = context?.memberListStore.isPresenceEnabled() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private listenForMembersChanges(): void {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
cli.on(RoomStateEvent.Update, this.onRoomStateUpdate);
|
|
||||||
cli.on(RoomMemberEvent.Name, this.onRoomMemberName);
|
|
||||||
cli.on(RoomStateEvent.Events, this.onRoomStateEvent);
|
|
||||||
// We listen for changes to the lastPresenceTs which is essentially
|
|
||||||
// listening for all presence events (we display most of not all of
|
|
||||||
// the information contained in presence events).
|
|
||||||
cli.on(UserEvent.LastPresenceTs, this.onUserPresenceChange);
|
|
||||||
cli.on(UserEvent.Presence, this.onUserPresenceChange);
|
|
||||||
cli.on(UserEvent.CurrentlyActive, this.onUserPresenceChange);
|
|
||||||
cli.on(ClientEvent.Room, this.onRoom); // invites & joining after peek
|
|
||||||
cli.on(RoomEvent.MyMembership, this.onMyMembership);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
this.unmounted = false;
|
|
||||||
this.listenForMembersChanges();
|
|
||||||
this.updateListNow(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
|
||||||
this.unmounted = true;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
if (cli) {
|
|
||||||
cli.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
|
|
||||||
cli.removeListener(RoomMemberEvent.Name, this.onRoomMemberName);
|
|
||||||
cli.removeListener(RoomEvent.MyMembership, this.onMyMembership);
|
|
||||||
cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvent);
|
|
||||||
cli.removeListener(ClientEvent.Room, this.onRoom);
|
|
||||||
cli.removeListener(UserEvent.LastPresenceTs, this.onUserPresenceChange);
|
|
||||||
cli.removeListener(UserEvent.Presence, this.onUserPresenceChange);
|
|
||||||
cli.removeListener(UserEvent.CurrentlyActive, this.onUserPresenceChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
|
||||||
this.updateList.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private get canInvite(): boolean {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const room = cli.getRoom(this.props.roomId);
|
|
||||||
|
|
||||||
return !!room && canInviteTo(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMembersState(invitedMembers: Array<RoomMember>, joinedMembers: Array<RoomMember>): IState {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
filteredJoinedMembers: joinedMembers,
|
|
||||||
filteredInvitedMembers: invitedMembers,
|
|
||||||
canInvite: this.canInvite,
|
|
||||||
|
|
||||||
// ideally we'd size this to the page height, but
|
|
||||||
// in practice I find that a little constraining
|
|
||||||
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
|
|
||||||
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onUserPresenceChange = (event: MatrixEvent | undefined, user: User): void => {
|
|
||||||
// Attach a SINGLE listener for global presence changes then locate the
|
|
||||||
// member tile and re-render it. This is more efficient than every tile
|
|
||||||
// ever attaching their own listener.
|
|
||||||
const tile = this.tiles.get(user.userId);
|
|
||||||
if (tile) {
|
|
||||||
this.updateList(); // reorder the membership list
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoom = (room: Room): void => {
|
|
||||||
if (room.roomId !== this.props.roomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We listen for room events because when we accept an invite
|
|
||||||
// we need to wait till the room is fully populated with state
|
|
||||||
// before refreshing the member list else we get a stale list.
|
|
||||||
this.updateListNow(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMyMembership = (room: Room, membership: string, oldMembership?: string): void => {
|
|
||||||
if (
|
|
||||||
room.roomId === this.props.roomId &&
|
|
||||||
membership === KnownMembership.Join &&
|
|
||||||
oldMembership !== KnownMembership.Join
|
|
||||||
) {
|
|
||||||
// we just joined the room, load the member list
|
|
||||||
this.updateListNow(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomStateUpdate = (state: RoomState): void => {
|
|
||||||
if (state.roomId !== this.props.roomId) return;
|
|
||||||
this.updateList();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
|
|
||||||
if (member.roomId !== this.props.roomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateList();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomStateEvent = (event: MatrixEvent): void => {
|
|
||||||
if (event.getRoomId() === this.props.roomId && event.getType() === EventType.RoomThirdPartyInvite) {
|
|
||||||
this.updateList();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateList = throttle(
|
|
||||||
() => {
|
|
||||||
this.updateListNow(false);
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
{ leading: true, trailing: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// XXX: exported for tests
|
|
||||||
public async updateListNow(showLoadingSpinner?: boolean): Promise<void> {
|
|
||||||
if (this.unmounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (showLoadingSpinner) {
|
|
||||||
this.setState({ loading: true });
|
|
||||||
}
|
|
||||||
const { joined, invited } = await this.context.memberListStore.loadMemberList(
|
|
||||||
this.props.roomId,
|
|
||||||
this.props.searchQuery,
|
|
||||||
);
|
|
||||||
if (this.unmounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
filteredJoinedMembers: joined,
|
|
||||||
filteredInvitedMembers: invited,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
|
|
||||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
|
|
||||||
};
|
|
||||||
|
|
||||||
private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
|
|
||||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList);
|
|
||||||
};
|
|
||||||
|
|
||||||
private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element => {
|
|
||||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
|
||||||
const text = _t("common|and_n_others", { count: overflowCount });
|
|
||||||
return (
|
|
||||||
<EntityTile
|
|
||||||
className="mx_EntityTile_ellipsis"
|
|
||||||
avatarJsx={<BaseAvatar url={OverflowHorizontalSvg} name="..." size="36px" />}
|
|
||||||
name={text}
|
|
||||||
showPresence={false}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private showMoreJoinedMemberList = (): void => {
|
|
||||||
this.setState({
|
|
||||||
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private showMoreInvitedMemberList = (): void => {
|
|
||||||
this.setState({
|
|
||||||
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>, snapshot?: any): void {
|
|
||||||
if (prevProps.searchQuery !== this.props.searchQuery) {
|
|
||||||
this.updateListNow(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onSearchQueryChanged = (searchQuery: string): void => {
|
|
||||||
this.props.onSearchQueryChanged(searchQuery);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: Action.View3pidInvite,
|
|
||||||
event: inviteEvent,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private getPending3PidInvites(): MatrixEvent[] {
|
|
||||||
// include 3pid invites (m.room.third_party_invite) state events.
|
|
||||||
// The HS may have already converted these into m.room.member invites so
|
|
||||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
|
||||||
// member invite (content.third_party_invite.signed.token)
|
|
||||||
const room = MatrixClientPeg.safeGet().getRoom(this.props.roomId);
|
|
||||||
|
|
||||||
if (room) {
|
|
||||||
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function (e) {
|
|
||||||
if (!isValid3pidInvite(e)) return false;
|
|
||||||
|
|
||||||
// discard all invites which have a m.room.member event since we've
|
|
||||||
// already added them.
|
|
||||||
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey()!);
|
|
||||||
if (memberEvent) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private makeMemberTiles(members: Array<RoomMember | MatrixEvent>): JSX.Element[] {
|
|
||||||
return members.map((m) => {
|
|
||||||
if (m instanceof RoomMember) {
|
|
||||||
// Is a Matrix invite
|
|
||||||
return (
|
|
||||||
<MemberTile
|
|
||||||
key={m.userId}
|
|
||||||
member={m}
|
|
||||||
ref={(tile) => {
|
|
||||||
if (tile) this.tiles.set(m.userId, tile);
|
|
||||||
else this.tiles.delete(m.userId);
|
|
||||||
}}
|
|
||||||
showPresence={this.showPresence}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Is a 3pid invite
|
|
||||||
return (
|
|
||||||
<EntityTile
|
|
||||||
key={m.getStateKey()}
|
|
||||||
name={m.getContent().display_name}
|
|
||||||
showPresence={false}
|
|
||||||
onClick={() => this.onPending3pidInviteClick(m)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
|
|
||||||
return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
|
|
||||||
};
|
|
||||||
|
|
||||||
private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
|
|
||||||
|
|
||||||
private getChildrenInvited = (start: number, end: number): Array<JSX.Element> => {
|
|
||||||
let targets = this.state.filteredInvitedMembers;
|
|
||||||
if (end > this.state.filteredInvitedMembers.length) {
|
|
||||||
targets = targets.concat(this.getPending3PidInvites());
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.makeMemberTiles(targets.slice(start, end));
|
|
||||||
};
|
|
||||||
|
|
||||||
private getChildCountInvited = (): number => {
|
|
||||||
return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
if (this.state.loading) {
|
|
||||||
return (
|
|
||||||
<BaseCard
|
|
||||||
id="memberlist-panel"
|
|
||||||
className="mx_MemberList"
|
|
||||||
ariaLabelledBy="memberlist-panel-tab"
|
|
||||||
role="tabpanel"
|
|
||||||
header={_t("common|people")}
|
|
||||||
onClose={this.props.onClose}
|
|
||||||
>
|
|
||||||
<Spinner />
|
|
||||||
</BaseCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const room = cli.getRoom(this.props.roomId);
|
|
||||||
let inviteButton: JSX.Element | undefined;
|
|
||||||
|
|
||||||
if (room?.getMyMembership() === KnownMembership.Join && shouldShowComponent(UIComponent.InviteUsers)) {
|
|
||||||
const inviteButtonText = room.isSpaceRoom() ? _t("space|invite_this_space") : _t("room|invite_this_room");
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
kind="secondary"
|
|
||||||
className="mx_MemberList_invite"
|
|
||||||
onClick={this.onInviteButtonClick}
|
|
||||||
disabled={!this.state.canInvite}
|
|
||||||
>
|
|
||||||
<UserAddIcon width="1em" height="1em" />
|
|
||||||
{inviteButtonText}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.state.canInvite) {
|
|
||||||
inviteButton = button;
|
|
||||||
} else {
|
|
||||||
inviteButton = <Tooltip label={_t("member_list|invite_button_no_perms_tooltip")}>{button}</Tooltip>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let invitedHeader;
|
|
||||||
let invitedSection;
|
|
||||||
if (this.getChildCountInvited() > 0) {
|
|
||||||
invitedHeader = <h2>{_t("member_list|invited_list_heading")}</h2>;
|
|
||||||
invitedSection = (
|
|
||||||
<TruncatedList
|
|
||||||
className="mx_MemberList_section mx_MemberList_invited"
|
|
||||||
truncateAt={this.state.truncateAtInvited}
|
|
||||||
createOverflowElement={this.createOverflowTileInvited}
|
|
||||||
getChildren={this.getChildrenInvited}
|
|
||||||
getChildCount={this.getChildCountInvited}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const footer = (
|
|
||||||
<SearchBox
|
|
||||||
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
|
|
||||||
placeholder={_t("member_list|filter_placeholder")}
|
|
||||||
onSearch={this.onSearchQueryChanged}
|
|
||||||
initialValue={this.props.searchQuery}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseCard
|
|
||||||
id="memberlist-panel"
|
|
||||||
className="mx_MemberList"
|
|
||||||
ariaLabelledBy="memberlist-panel-tab"
|
|
||||||
role="tabpanel"
|
|
||||||
header={_t("common|people")}
|
|
||||||
footer={footer}
|
|
||||||
onClose={this.props.onClose}
|
|
||||||
>
|
|
||||||
{inviteButton}
|
|
||||||
<div className="mx_MemberList_wrapper">
|
|
||||||
<TruncatedList
|
|
||||||
className="mx_MemberList_section mx_MemberList_joined"
|
|
||||||
truncateAt={this.state.truncateAtJoined}
|
|
||||||
createOverflowElement={this.createOverflowTileJoined}
|
|
||||||
getChildren={this.getChildrenJoined}
|
|
||||||
getChildCount={this.getChildCountJoined}
|
|
||||||
/>
|
|
||||||
{invitedHeader}
|
|
||||||
{invitedSection}
|
|
||||||
</div>
|
|
||||||
</BaseCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onInviteButtonClick = (ev: ButtonEvent): void => {
|
|
||||||
PosthogTrackers.trackInteraction("WebRightPanelMemberListInviteButton", ev);
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const room = cli.getRoom(this.props.roomId)!;
|
|
||||||
|
|
||||||
inviteToRoom(room);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,220 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { RoomMember, RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
|
||||||
import EntityTile, { PowerStatus, PresenceState } from "./EntityTile";
|
|
||||||
import MemberAvatar from "./../avatars/MemberAvatar";
|
|
||||||
import DisambiguatedProfile from "../messages/DisambiguatedProfile";
|
|
||||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
|
||||||
import { E2EState } from "./E2EIcon";
|
|
||||||
import { asyncSome } from "../../../utils/arrays";
|
|
||||||
import { getUserDeviceIds } from "../../../utils/crypto/deviceInfo";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
member: RoomMember;
|
|
||||||
showPresence?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
isRoomEncrypted: boolean;
|
|
||||||
e2eStatus?: E2EState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MemberTile extends React.Component<IProps, IState> {
|
|
||||||
private userLastModifiedTime?: number;
|
|
||||||
private memberLastModifiedTime?: number;
|
|
||||||
|
|
||||||
public static defaultProps = {
|
|
||||||
showPresence: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isRoomEncrypted: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
|
|
||||||
const { roomId } = this.props.member;
|
|
||||||
if (roomId) {
|
|
||||||
const isRoomEncrypted = cli.isRoomEncrypted(roomId);
|
|
||||||
this.setState({
|
|
||||||
isRoomEncrypted,
|
|
||||||
});
|
|
||||||
if (isRoomEncrypted) {
|
|
||||||
cli.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
|
||||||
this.updateE2EStatus();
|
|
||||||
} else {
|
|
||||||
// Listen for room to become encrypted
|
|
||||||
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
if (cli) {
|
|
||||||
cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
|
||||||
cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomStateEvents = (ev: MatrixEvent): void => {
|
|
||||||
if (ev.getType() !== EventType.RoomEncryption) return;
|
|
||||||
const { roomId } = this.props.member;
|
|
||||||
if (ev.getRoomId() !== roomId) return;
|
|
||||||
|
|
||||||
// The room is encrypted now.
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
|
||||||
this.setState({
|
|
||||||
isRoomEncrypted: true,
|
|
||||||
});
|
|
||||||
this.updateE2EStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => {
|
|
||||||
if (userId !== this.props.member.userId) return;
|
|
||||||
this.updateE2EStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
private async updateE2EStatus(): Promise<void> {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const { userId } = this.props.member;
|
|
||||||
const isMe = userId === cli.getUserId();
|
|
||||||
const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId);
|
|
||||||
if (!userTrust?.isCrossSigningVerified()) {
|
|
||||||
this.setState({
|
|
||||||
e2eStatus: userTrust?.wasCrossSigningVerified() ? E2EState.Warning : E2EState.Normal,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceIDs = await getUserDeviceIds(cli, userId);
|
|
||||||
const anyDeviceUnverified = await asyncSome(deviceIDs, async (deviceId) => {
|
|
||||||
// For your own devices, we use the stricter check of cross-signing
|
|
||||||
// verification to encourage everyone to trust their own devices via
|
|
||||||
// cross-signing so that other users can then safely trust you.
|
|
||||||
// For other people's devices, the more general verified check that
|
|
||||||
// includes locally verified devices can be used.
|
|
||||||
const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
|
|
||||||
return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified());
|
|
||||||
});
|
|
||||||
this.setState({
|
|
||||||
e2eStatus: anyDeviceUnverified ? E2EState.Warning : E2EState.Verified,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
|
|
||||||
if (
|
|
||||||
this.memberLastModifiedTime === undefined ||
|
|
||||||
this.memberLastModifiedTime < nextProps.member.getLastModifiedTime()
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
nextProps.member.user &&
|
|
||||||
(this.userLastModifiedTime === undefined ||
|
|
||||||
this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.isRoomEncrypted !== this.state.isRoomEncrypted || nextState.e2eStatus !== this.state.e2eStatus) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClick = (): void => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: Action.ViewUser,
|
|
||||||
member: this.props.member,
|
|
||||||
push: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private getDisplayName(): string {
|
|
||||||
return this.props.member.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPowerLabel(): string {
|
|
||||||
return _t("member_list|power_label", {
|
|
||||||
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, {
|
|
||||||
roomId: this.props.member.roomId,
|
|
||||||
}),
|
|
||||||
powerLevelNumber: this.props.member.powerLevel,
|
|
||||||
}).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const member = this.props.member;
|
|
||||||
const name = this.getDisplayName();
|
|
||||||
const presenceState = member.user?.presence as PresenceState | undefined;
|
|
||||||
|
|
||||||
const av = <MemberAvatar member={member} size="36px" aria-hidden="true" />;
|
|
||||||
|
|
||||||
if (member.user) {
|
|
||||||
this.userLastModifiedTime = member.user.getLastModifiedTime();
|
|
||||||
}
|
|
||||||
this.memberLastModifiedTime = member.getLastModifiedTime();
|
|
||||||
|
|
||||||
const powerStatusMap = new Map([
|
|
||||||
[100, PowerStatus.Admin],
|
|
||||||
[50, PowerStatus.Moderator],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Find the nearest power level with a badge
|
|
||||||
let powerLevel = this.props.member.powerLevel;
|
|
||||||
for (const [pl] of powerStatusMap) {
|
|
||||||
if (this.props.member.powerLevel >= pl) {
|
|
||||||
powerLevel = pl;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const powerStatus = powerStatusMap.get(powerLevel);
|
|
||||||
|
|
||||||
let e2eStatus: E2EState | undefined;
|
|
||||||
if (this.state.isRoomEncrypted) {
|
|
||||||
e2eStatus = this.state.e2eStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EntityTile
|
|
||||||
{...this.props}
|
|
||||||
presenceState={presenceState}
|
|
||||||
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
|
||||||
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
|
||||||
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
|
||||||
avatarJsx={av}
|
|
||||||
title={this.getPowerLabel()}
|
|
||||||
name={name}
|
|
||||||
nameJSX={nameJSX}
|
|
||||||
powerStatus={powerStatus}
|
|
||||||
showPresence={this.props.showPresence}
|
|
||||||
e2eStatus={e2eStatus}
|
|
||||||
onClick={this.onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,452 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
act,
|
|
||||||
fireEvent,
|
|
||||||
render,
|
|
||||||
RenderResult,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
waitForElementToBeRemoved,
|
|
||||||
cleanup,
|
|
||||||
} from "jest-matrix-react";
|
|
||||||
import { Room, MatrixClient, RoomState, RoomMember, User, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
|
||||||
import { mocked, MockedObject } from "jest-mock";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
|
||||||
import * as TestUtils from "../../../../test-utils";
|
|
||||||
import MemberList from "../../../../../src/components/views/rooms/MemberList";
|
|
||||||
import { SDKContext } from "../../../../../src/contexts/SDKContext";
|
|
||||||
import { TestSdkContext } from "../../../TestSdkContext";
|
|
||||||
import {
|
|
||||||
filterConsole,
|
|
||||||
flushPromises,
|
|
||||||
getMockClientWithEventEmitter,
|
|
||||||
mockClientMethodsRooms,
|
|
||||||
mockClientMethodsUser,
|
|
||||||
} from "../../../../test-utils";
|
|
||||||
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
|
|
||||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
|
||||||
|
|
||||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
|
||||||
shouldShowComponent: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function generateRoomId() {
|
|
||||||
return "!" + Math.random().toString().slice(2, 10) + ":domain";
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("MemberList", () => {
|
|
||||||
filterConsole(
|
|
||||||
"Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur.",
|
|
||||||
);
|
|
||||||
function createRoom(opts = {}) {
|
|
||||||
const room = new Room(generateRoomId(), client, client.getUserId()!);
|
|
||||||
if (opts) {
|
|
||||||
Object.assign(room, opts);
|
|
||||||
}
|
|
||||||
return room;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client: MatrixClient;
|
|
||||||
let root: RenderResult;
|
|
||||||
let memberListRoom: Room;
|
|
||||||
let memberList: MemberList;
|
|
||||||
|
|
||||||
let adminUsers: RoomMember[] = [];
|
|
||||||
let moderatorUsers: RoomMember[] = [];
|
|
||||||
let defaultUsers: RoomMember[] = [];
|
|
||||||
|
|
||||||
function memberString(member: RoomMember): string {
|
|
||||||
if (!member) {
|
|
||||||
return "(null)";
|
|
||||||
} else {
|
|
||||||
const u = member.user;
|
|
||||||
return (
|
|
||||||
"(" +
|
|
||||||
member.name +
|
|
||||||
", " +
|
|
||||||
member.powerLevel +
|
|
||||||
", " +
|
|
||||||
(u ? u.lastActiveAgo : "<null>") +
|
|
||||||
", " +
|
|
||||||
(u ? u.getLastActiveTs() : "<null>") +
|
|
||||||
", " +
|
|
||||||
(u ? u.currentlyActive : "<null>") +
|
|
||||||
", " +
|
|
||||||
(u ? u.presence : "<null>") +
|
|
||||||
")"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectOrderedByPresenceAndPowerLevel(memberTiles: NodeListOf<Element>, isPresenceEnabled: boolean) {
|
|
||||||
let prevMember: RoomMember | undefined;
|
|
||||||
for (const tile of memberTiles) {
|
|
||||||
const memberA = prevMember;
|
|
||||||
const memberB = memberListRoom.currentState.members[tile.getAttribute("aria-label")!.split(" ")[0]];
|
|
||||||
prevMember = memberB; // just in case an expect fails, set this early
|
|
||||||
if (!memberA) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("COMPARING A VS B:", memberString(memberA), memberString(memberB));
|
|
||||||
|
|
||||||
const userA = memberA.user!;
|
|
||||||
const userB = memberB.user!;
|
|
||||||
|
|
||||||
let groupChange = false;
|
|
||||||
|
|
||||||
if (isPresenceEnabled) {
|
|
||||||
const convertPresence = (p: string) => (p === "unavailable" ? "online" : p);
|
|
||||||
const presenceIndex = (p: string) => {
|
|
||||||
const order = ["active", "online", "offline"];
|
|
||||||
const idx = order.indexOf(convertPresence(p));
|
|
||||||
return idx === -1 ? order.length : idx; // unknown states at the end
|
|
||||||
};
|
|
||||||
|
|
||||||
const idxA = presenceIndex(userA.currentlyActive ? "active" : userA.presence);
|
|
||||||
const idxB = presenceIndex(userB.currentlyActive ? "active" : userB.presence);
|
|
||||||
console.log("Comparing presence groups...");
|
|
||||||
expect(idxB).toBeGreaterThanOrEqual(idxA);
|
|
||||||
groupChange = idxA !== idxB;
|
|
||||||
} else {
|
|
||||||
console.log("Skipped presence groups");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!groupChange) {
|
|
||||||
console.log("Comparing power levels...");
|
|
||||||
expect(memberA.powerLevel).toBeGreaterThanOrEqual(memberB.powerLevel);
|
|
||||||
groupChange = memberA.powerLevel !== memberB.powerLevel;
|
|
||||||
} else {
|
|
||||||
console.log("Skipping power level check due to group change");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!groupChange) {
|
|
||||||
if (isPresenceEnabled) {
|
|
||||||
console.log("Comparing last active timestamp...");
|
|
||||||
expect(userB.getLastActiveTs()).toBeLessThanOrEqual(userA.getLastActiveTs());
|
|
||||||
groupChange = userA.getLastActiveTs() !== userB.getLastActiveTs();
|
|
||||||
} else {
|
|
||||||
console.log("Skipping last active timestamp");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("Skipping last active timestamp check due to group change");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!groupChange) {
|
|
||||||
const nameA = memberA.name[0] === "@" ? memberA.name.slice(1) : memberA.name;
|
|
||||||
const nameB = memberB.name[0] === "@" ? memberB.name.slice(1) : memberB.name;
|
|
||||||
const collator = new Intl.Collator();
|
|
||||||
const nameCompare = collator.compare(nameB, nameA);
|
|
||||||
console.log("Comparing name");
|
|
||||||
expect(nameCompare).toBeGreaterThanOrEqual(0);
|
|
||||||
} else {
|
|
||||||
console.log("Skipping name check due to group change");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMemberList(enablePresence: boolean): void {
|
|
||||||
TestUtils.stubClient();
|
|
||||||
client = MatrixClientPeg.safeGet();
|
|
||||||
client.hasLazyLoadMembersEnabled = () => false;
|
|
||||||
|
|
||||||
// Make room
|
|
||||||
memberListRoom = createRoom();
|
|
||||||
expect(memberListRoom.roomId).toBeTruthy();
|
|
||||||
|
|
||||||
// Make users
|
|
||||||
adminUsers = [];
|
|
||||||
moderatorUsers = [];
|
|
||||||
defaultUsers = [];
|
|
||||||
const usersPerLevel = 2;
|
|
||||||
for (let i = 0; i < usersPerLevel; i++) {
|
|
||||||
const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`);
|
|
||||||
adminUser.membership = KnownMembership.Join;
|
|
||||||
adminUser.powerLevel = 100;
|
|
||||||
adminUser.user = User.createUser(adminUser.userId, client);
|
|
||||||
adminUser.user.currentlyActive = true;
|
|
||||||
adminUser.user.presence = "online";
|
|
||||||
adminUser.user.lastPresenceTs = 1000;
|
|
||||||
adminUser.user.lastActiveAgo = 10;
|
|
||||||
adminUsers.push(adminUser);
|
|
||||||
|
|
||||||
const moderatorUser = new RoomMember(memberListRoom.roomId, `@moderator${i}:localhost`);
|
|
||||||
moderatorUser.membership = KnownMembership.Join;
|
|
||||||
moderatorUser.powerLevel = 50;
|
|
||||||
moderatorUser.user = User.createUser(moderatorUser.userId, client);
|
|
||||||
moderatorUser.user.currentlyActive = true;
|
|
||||||
moderatorUser.user.presence = "online";
|
|
||||||
moderatorUser.user.lastPresenceTs = 1000;
|
|
||||||
moderatorUser.user.lastActiveAgo = 10;
|
|
||||||
moderatorUsers.push(moderatorUser);
|
|
||||||
|
|
||||||
const defaultUser = new RoomMember(memberListRoom.roomId, `@default${i}:localhost`);
|
|
||||||
defaultUser.membership = KnownMembership.Join;
|
|
||||||
defaultUser.powerLevel = 0;
|
|
||||||
defaultUser.user = User.createUser(defaultUser.userId, client);
|
|
||||||
defaultUser.user.currentlyActive = true;
|
|
||||||
defaultUser.user.presence = "online";
|
|
||||||
defaultUser.user.lastPresenceTs = 1000;
|
|
||||||
defaultUser.user.lastActiveAgo = 10;
|
|
||||||
defaultUsers.push(defaultUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.getRoom = (roomId) => {
|
|
||||||
if (roomId === memberListRoom.roomId) return memberListRoom;
|
|
||||||
else return null;
|
|
||||||
};
|
|
||||||
memberListRoom.currentState = {
|
|
||||||
members: {},
|
|
||||||
getMember: jest.fn(),
|
|
||||||
getStateEvents: ((eventType, stateKey) =>
|
|
||||||
stateKey === undefined ? [] : null) as RoomState["getStateEvents"], // ignore 3pid invites
|
|
||||||
} as unknown as RoomState;
|
|
||||||
for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) {
|
|
||||||
memberListRoom.currentState.members[member.userId] = member;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gatherWrappedRef = (r: MemberList) => {
|
|
||||||
memberList = r;
|
|
||||||
};
|
|
||||||
const context = new TestSdkContext();
|
|
||||||
context.client = client;
|
|
||||||
context.memberListStore.isPresenceEnabled = jest.fn().mockReturnValue(enablePresence);
|
|
||||||
root = render(
|
|
||||||
<SDKContext.Provider value={context}>
|
|
||||||
<MemberList
|
|
||||||
searchQuery=""
|
|
||||||
onClose={jest.fn()}
|
|
||||||
onSearchQueryChanged={jest.fn()}
|
|
||||||
roomId={memberListRoom.roomId}
|
|
||||||
ref={gatherWrappedRef}
|
|
||||||
/>
|
|
||||||
</SDKContext.Provider>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe.each([false, true])("does order members correctly (presence %s)", (enablePresence) => {
|
|
||||||
beforeEach(function () {
|
|
||||||
renderMemberList(enablePresence);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("does order members correctly", () => {
|
|
||||||
// Note: even if presence is disabled, we still expect that the presence
|
|
||||||
// tests will pass. All expectOrderedByPresenceAndPowerLevel does is ensure
|
|
||||||
// the order is perceived correctly, regardless of what we did to the members.
|
|
||||||
|
|
||||||
// Each of the 4 tests here is done to prove that the member list can meet
|
|
||||||
// all 4 criteria independently. Together, they should work.
|
|
||||||
|
|
||||||
it("by presence state", async () => {
|
|
||||||
// Intentionally pick users that will confuse the power level sorting
|
|
||||||
const activeUsers = [defaultUsers[0]];
|
|
||||||
const onlineUsers = [adminUsers[0]];
|
|
||||||
const offlineUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)];
|
|
||||||
activeUsers.forEach((u) => {
|
|
||||||
u.user!.currentlyActive = true;
|
|
||||||
u.user!.presence = "online";
|
|
||||||
});
|
|
||||||
onlineUsers.forEach((u) => {
|
|
||||||
u.user!.currentlyActive = false;
|
|
||||||
u.user!.presence = "online";
|
|
||||||
});
|
|
||||||
offlineUsers.forEach((u) => {
|
|
||||||
u.user!.currentlyActive = false;
|
|
||||||
u.user!.presence = "offline";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
|
||||||
await act(() => memberList.updateListNow(true));
|
|
||||||
|
|
||||||
const tiles = root.container.querySelectorAll(".mx_EntityTile");
|
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("by power level", async () => {
|
|
||||||
// We already have admin, moderator, and default users so leave them alone
|
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
|
||||||
await act(() => memberList.updateListNow(true));
|
|
||||||
|
|
||||||
const tiles = root.container.querySelectorAll(".mx_EntityTile");
|
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("by last active timestamp", async () => {
|
|
||||||
// Intentionally pick users that will confuse the power level sorting
|
|
||||||
// lastActiveAgoTs == lastPresenceTs - lastActiveAgo
|
|
||||||
const activeUsers = [defaultUsers[0]];
|
|
||||||
const semiActiveUsers = [adminUsers[0]];
|
|
||||||
const inactiveUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)];
|
|
||||||
activeUsers.forEach((u) => {
|
|
||||||
u.powerLevel = 100; // set everyone to the same PL to avoid running that check
|
|
||||||
u.user!.lastPresenceTs = 1000;
|
|
||||||
u.user!.lastActiveAgo = 0;
|
|
||||||
});
|
|
||||||
semiActiveUsers.forEach((u) => {
|
|
||||||
u.powerLevel = 100;
|
|
||||||
u.user!.lastPresenceTs = 1000;
|
|
||||||
u.user!.lastActiveAgo = 50;
|
|
||||||
});
|
|
||||||
inactiveUsers.forEach((u) => {
|
|
||||||
u.powerLevel = 100;
|
|
||||||
u.user!.lastPresenceTs = 1000;
|
|
||||||
u.user!.lastActiveAgo = 100;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
|
||||||
await act(() => memberList.updateListNow(true));
|
|
||||||
|
|
||||||
const tiles = root.container.querySelectorAll(".mx_EntityTile");
|
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("by name", async () => {
|
|
||||||
// Intentionally put everyone on the same level to force a name comparison
|
|
||||||
const allUsers = [...adminUsers, ...moderatorUsers, ...defaultUsers];
|
|
||||||
allUsers.forEach((u) => {
|
|
||||||
u.user!.currentlyActive = true;
|
|
||||||
u.user!.presence = "online";
|
|
||||||
u.user!.lastPresenceTs = 1000;
|
|
||||||
u.user!.lastActiveAgo = 0;
|
|
||||||
u.powerLevel = 100;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
|
||||||
await act(() => memberList.updateListNow(true));
|
|
||||||
|
|
||||||
const tiles = root.container.querySelectorAll(".mx_EntityTile");
|
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("memberlist is rendered correctly", () => {
|
|
||||||
beforeEach(function () {
|
|
||||||
renderMemberList(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("memberlist is re-rendered on unreachable presence event", async () => {
|
|
||||||
defaultUsers[0].user?.setPresenceEvent(
|
|
||||||
new MatrixEvent({
|
|
||||||
type: "m.presence",
|
|
||||||
sender: defaultUsers[0].userId,
|
|
||||||
content: {
|
|
||||||
presence: "io.element.unreachable",
|
|
||||||
currently_active: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(await screen.findByText(/User's server unreachable/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Invite button", () => {
|
|
||||||
const roomId = "!room:server.org";
|
|
||||||
let client!: MockedObject<MatrixClient>;
|
|
||||||
let room!: Room;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mocked(shouldShowComponent).mockReturnValue(true);
|
|
||||||
client = getMockClientWithEventEmitter({
|
|
||||||
...mockClientMethodsUser(),
|
|
||||||
...mockClientMethodsRooms(),
|
|
||||||
getRoom: jest.fn(),
|
|
||||||
hasLazyLoadMembersEnabled: jest.fn(),
|
|
||||||
});
|
|
||||||
room = new Room(roomId, client, client.getSafeUserId());
|
|
||||||
client.getRoom.mockReturnValue(room);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderComponent = () => {
|
|
||||||
const context = new TestSdkContext();
|
|
||||||
context.client = client;
|
|
||||||
return render(
|
|
||||||
<SDKContext.Provider value={context}>
|
|
||||||
<MemberList
|
|
||||||
searchQuery=""
|
|
||||||
onClose={jest.fn()}
|
|
||||||
onSearchQueryChanged={jest.fn()}
|
|
||||||
roomId={room.roomId}
|
|
||||||
/>
|
|
||||||
</SDKContext.Provider>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("does not render invite button when current user is not a member", async () => {
|
|
||||||
renderComponent();
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(screen.queryByText("Invite to this room")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not render invite button UI customisation hides invites", async () => {
|
|
||||||
mocked(shouldShowComponent).mockReturnValue(false);
|
|
||||||
renderComponent();
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(screen.queryByText("Invite to this room")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders disabled invite button when current user is a member but does not have rights to invite", async () => {
|
|
||||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
|
||||||
jest.spyOn(room, "canInvite").mockReturnValue(false);
|
|
||||||
|
|
||||||
const { findByLabelText } = renderComponent();
|
|
||||||
|
|
||||||
// button rendered but disabled
|
|
||||||
await expect(findByLabelText("You do not have permission to invite users")).resolves.toHaveAttribute(
|
|
||||||
"aria-disabled",
|
|
||||||
"true",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders enabled invite button when current user is a member and has rights to invite", async () => {
|
|
||||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
|
||||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
|
||||||
|
|
||||||
const { findByText } = renderComponent();
|
|
||||||
|
|
||||||
await expect(findByText("Invite to this room")).resolves.not.toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens room inviter on button click", async () => {
|
|
||||||
jest.spyOn(defaultDispatcher, "dispatch");
|
|
||||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
|
||||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
|
||||||
|
|
||||||
const { getByRole } = renderComponent();
|
|
||||||
await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"));
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(getByRole("button", { name: "Invite to this room" })).not.toHaveAttribute(
|
|
||||||
"aria-disabled",
|
|
||||||
"true",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(getByRole("button", { name: "Invite to this room" }));
|
|
||||||
|
|
||||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
|
||||||
action: "view_invite",
|
|
||||||
roomId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
* Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { render, screen, waitFor } from "jest-matrix-react";
|
|
||||||
import { MatrixClient, RoomMember, Device } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { UserVerificationStatus, DeviceVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
|
|
||||||
import * as TestUtils from "../../../../test-utils";
|
|
||||||
import MemberTile from "../../../../../src/components/views/rooms/MemberTile";
|
|
||||||
|
|
||||||
describe("MemberTile", () => {
|
|
||||||
let matrixClient: MatrixClient;
|
|
||||||
let member: RoomMember;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
matrixClient = TestUtils.stubClient();
|
|
||||||
mocked(matrixClient.isRoomEncrypted).mockReturnValue(true);
|
|
||||||
member = new RoomMember("roomId", matrixClient.getUserId()!);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not display an E2EIcon when the e2E status = normal", () => {
|
|
||||||
const { container } = render(<MemberTile member={member} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display an warning E2EIcon when the e2E status = Warning", async () => {
|
|
||||||
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
|
|
||||||
isCrossSigningVerified: jest.fn().mockReturnValue(false),
|
|
||||||
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
|
|
||||||
} as unknown as UserVerificationStatus);
|
|
||||||
|
|
||||||
const { container } = render(<MemberTile member={member} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
await waitFor(async () => {
|
|
||||||
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
|
|
||||||
expect(screen.getByText("This user has not verified all of their sessions.")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display an verified E2EIcon when the e2E status = Verified", async () => {
|
|
||||||
// Mock all the required crypto methods
|
|
||||||
const deviceMap = new Map<string, Map<string, Device>>();
|
|
||||||
deviceMap.set(member.userId, new Map([["deviceId", {} as Device]]));
|
|
||||||
// Return a DeviceMap = Map<string, Map<string, Device>>
|
|
||||||
mocked(matrixClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(deviceMap);
|
|
||||||
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
|
|
||||||
isCrossSigningVerified: jest.fn().mockReturnValue(true),
|
|
||||||
} as unknown as UserVerificationStatus);
|
|
||||||
mocked(matrixClient.getCrypto()!.getDeviceVerificationStatus).mockResolvedValue({
|
|
||||||
crossSigningVerified: true,
|
|
||||||
} as DeviceVerificationStatus);
|
|
||||||
|
|
||||||
const { container } = render(<MemberTile member={member} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
await waitFor(async () => {
|
|
||||||
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
|
|
||||||
expect(
|
|
||||||
screen.getByText("You have verified this user. This user has verified all of their sessions."),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,160 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`MemberTile should display an verified E2EIcon when the e2E status = Verified 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
aria-label="@userId:matrix.org (power 0)"
|
|
||||||
class="mx_AccessibleButton mx_EntityTile mx_EntityTile_offline_neveractive"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_avatar"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
|
||||||
data-color="2"
|
|
||||||
data-testid="avatar-img"
|
|
||||||
data-type="round"
|
|
||||||
role="presentation"
|
|
||||||
style="--cpd-avatar-size: 36px;"
|
|
||||||
title="@userId:matrix.org"
|
|
||||||
>
|
|
||||||
u
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_details"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_name"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_DisambiguatedProfile"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class=""
|
|
||||||
dir="auto"
|
|
||||||
>
|
|
||||||
@userId:matrix.org
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_PresenceLabel"
|
|
||||||
>
|
|
||||||
Offline
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`MemberTile should display an warning E2EIcon when the e2E status = Warning 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
aria-label="@userId:matrix.org (power 0)"
|
|
||||||
class="mx_AccessibleButton mx_EntityTile mx_EntityTile_offline_neveractive"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_avatar"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
|
||||||
data-color="2"
|
|
||||||
data-testid="avatar-img"
|
|
||||||
data-type="round"
|
|
||||||
role="presentation"
|
|
||||||
style="--cpd-avatar-size: 36px;"
|
|
||||||
title="@userId:matrix.org"
|
|
||||||
>
|
|
||||||
u
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_details"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_name"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_DisambiguatedProfile"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class=""
|
|
||||||
dir="auto"
|
|
||||||
>
|
|
||||||
@userId:matrix.org
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_PresenceLabel"
|
|
||||||
>
|
|
||||||
Offline
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`MemberTile should not display an E2EIcon when the e2E status = normal 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
aria-label="@userId:matrix.org (power 0)"
|
|
||||||
class="mx_AccessibleButton mx_EntityTile mx_EntityTile_offline_neveractive"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_avatar"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
|
||||||
data-color="2"
|
|
||||||
data-testid="avatar-img"
|
|
||||||
data-type="round"
|
|
||||||
role="presentation"
|
|
||||||
style="--cpd-avatar-size: 36px;"
|
|
||||||
title="@userId:matrix.org"
|
|
||||||
>
|
|
||||||
u
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_details"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_EntityTile_name"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_DisambiguatedProfile"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class=""
|
|
||||||
dir="auto"
|
|
||||||
>
|
|
||||||
@userId:matrix.org
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_PresenceLabel"
|
|
||||||
>
|
|
||||||
Offline
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
Loading…
Reference in New Issue