Merge pull request #6249 from SimonBrandner/ts/member-list

This commit is contained in:
Michael Telatynski 2021-06-24 13:25:59 +01:00 committed by GitHub
commit f965d449b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 211 additions and 137 deletions

View file

@ -172,7 +172,7 @@
"jest": { "jest": {
"testEnvironment": "./__test-utils__/environment.js", "testEnvironment": "./__test-utils__/environment.js",
"testMatch": [ "testMatch": [
"<rootDir>/test/**/*-test.[jt]s" "<rootDir>/test/**/*-test.[jt]s?(x)"
], ],
"setupFiles": [ "setupFiles": [
"jest-canvas-mock" "jest-canvas-mock"

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd Copyright 2017, 2018 New Vector Ltd
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -21,9 +22,8 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { isValid3pidInvite } from "../../../RoomInvite"; import { isValid3pidInvite } from "../../../RoomInvite";
import rate_limited_func from "../../../ratelimitedfunc"; import rateLimitedFunction from "../../../ratelimitedfunc";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import BaseCard from "../right_panel/BaseCard"; import BaseCard from "../right_panel/BaseCard";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
@ -31,6 +31,18 @@ import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
import { User } from "matrix-js-sdk/src/models/user";
import TruncatedList from '../elements/TruncatedList';
import Spinner from "../elements/Spinner";
import SearchBox from "../../structures/SearchBox";
import AccessibleButton from '../elements/AccessibleButton';
import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5; const INITIAL_LOAD_NUM_INVITED = 5;
@ -40,41 +52,59 @@ const SHOW_MORE_INCREMENT = 100;
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ // matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g; const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
interface IProps {
roomId: string;
onClose(): void;
}
interface IState {
loading: boolean;
members: Array<RoomMember>;
filteredJoinedMembers: Array<RoomMember>;
filteredInvitedMembers: Array<RoomMember | MatrixEvent>;
canInvite: boolean;
truncateAtJoined: number;
truncateAtInvited: number;
searchQuery: string;
}
@replaceableComponent("views.rooms.MemberList") @replaceableComponent("views.rooms.MemberList")
export default class MemberList extends React.Component { export default class MemberList extends React.Component<IProps, IState> {
private showPresence = true;
private mounted = false;
private collator: Intl.Collator;
private sortNames = new Map<RoomMember, string>(); // RoomMember -> sortName
constructor(props) { constructor(props) {
super(props); super(props);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) { if (cli.hasLazyLoadMembersEnabled()) {
// show an empty list // show an empty list
this.state = this._getMembersState([]); this.state = this.getMembersState([]);
} else { } else {
this.state = this._getMembersState(this.roomMembers()); this.state = this.getMembersState(this.roomMembers());
} }
cli.on("Room", this.onRoom); // invites & joining after peek cli.on("Room", this.onRoom); // invites & joining after peek
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = MatrixClientPeg.get().baseUrl; const hsUrl = MatrixClientPeg.get().baseUrl;
this._showPresence = true; this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
this._showPresence = enablePresenceByHsUrl[hsUrl];
}
} }
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
this._mounted = true; this.mounted = true;
if (cli.hasLazyLoadMembersEnabled()) { if (cli.hasLazyLoadMembersEnabled()) {
this._showMembersAccordingToMembershipWithLL(); this.showMembersAccordingToMembershipWithLL();
cli.on("Room.myMembership", this.onMyMembership); cli.on("Room.myMembership", this.onMyMembership);
} else { } else {
this._listenForMembersChanges(); this.listenForMembersChanges();
} }
} }
_listenForMembersChanges() { private listenForMembersChanges(): void {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomMember.name", this.onRoomMemberName);
@ -89,7 +119,7 @@ export default class MemberList extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
this._mounted = false; this.mounted = false;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener("RoomState.members", this.onRoomStateMember); cli.removeListener("RoomState.members", this.onRoomStateMember);
@ -103,7 +133,7 @@ export default class MemberList extends React.Component {
} }
// cancel any pending calls to the rate_limited_funcs // cancel any pending calls to the rate_limited_funcs
this._updateList.cancelPendingCall(); this.updateList.cancelPendingCall();
} }
/** /**
@ -111,7 +141,7 @@ export default class MemberList extends React.Component {
* show a spinner and load the members if the user is joined, * show a spinner and load the members if the user is joined,
* or show the members available so far if the user is invited * or show the members available so far if the user is invited
*/ */
async _showMembersAccordingToMembershipWithLL() { private async showMembersAccordingToMembershipWithLL(): Promise<void> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) { if (cli.hasLazyLoadMembersEnabled()) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -122,31 +152,31 @@ export default class MemberList extends React.Component {
try { try {
await room.loadMembersIfNeeded(); await room.loadMembersIfNeeded();
} catch (ex) {/* already logged in RoomView */} } catch (ex) {/* already logged in RoomView */}
if (this._mounted) { if (this.mounted) {
this.setState(this._getMembersState(this.roomMembers())); this.setState(this.getMembersState(this.roomMembers()));
this._listenForMembersChanges(); this.listenForMembersChanges();
} }
} else { } else {
// show the members we already have loaded // show the members we already have loaded
this.setState(this._getMembersState(this.roomMembers())); this.setState(this.getMembersState(this.roomMembers()));
} }
} }
} }
get canInvite() { private get canInvite(): boolean {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId); const room = cli.getRoom(this.props.roomId);
return room && room.canInvite(cli.getUserId()); return room && room.canInvite(cli.getUserId());
} }
_getMembersState(members) { private getMembersState(members: Array<RoomMember>): IState {
// set the state after determining _showPresence to make sure it's // set the state after determining showPresence to make sure it's
// taken into account while rerendering // taken into account while rendering
return { return {
loading: false, loading: false,
members: members, members: members,
filteredJoinedMembers: this._filterMembers(members, 'join'), filteredJoinedMembers: this.filterMembers(members, 'join'),
filteredInvitedMembers: this._filterMembers(members, 'invite'), filteredInvitedMembers: this.filterMembers(members, 'invite'),
canInvite: this.canInvite, canInvite: this.canInvite,
// ideally we'd size this to the page height, but // ideally we'd size this to the page height, but
@ -157,72 +187,72 @@ export default class MemberList extends React.Component {
}; };
} }
onUserPresenceChange = (event, user) => { private onUserPresenceChange = (event: MatrixEvent, user: User): void => {
// Attach a SINGLE listener for global presence changes then locate the // Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile // member tile and re-render it. This is more efficient than every tile
// ever attaching their own listener. // ever attaching their own listener.
const tile = this.refs[user.userId]; const tile = this.refs[user.userId];
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`); // console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
if (tile) { if (tile) {
this._updateList(); // reorder the membership list this.updateList(); // reorder the membership list
} }
}; };
onRoom = room => { private onRoom = (room: Room): void => {
if (room.roomId !== this.props.roomId) { if (room.roomId !== this.props.roomId) {
return; return;
} }
// We listen for room events because when we accept an invite // We listen for room events because when we accept an invite
// we need to wait till the room is fully populated with state // we need to wait till the room is fully populated with state
// before refreshing the member list else we get a stale list. // before refreshing the member list else we get a stale list.
this._showMembersAccordingToMembershipWithLL(); this.showMembersAccordingToMembershipWithLL();
}; };
onMyMembership = (room, membership, oldMembership) => { private onMyMembership = (room: Room, membership: string, oldMembership: string): void => {
if (room.roomId === this.props.roomId && membership === "join") { if (room.roomId === this.props.roomId && membership === "join") {
this._showMembersAccordingToMembershipWithLL(); this.showMembersAccordingToMembershipWithLL();
} }
}; };
onRoomStateMember = (ev, state, member) => { private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => {
if (member.roomId !== this.props.roomId) { if (member.roomId !== this.props.roomId) {
return; return;
} }
this._updateList(); this.updateList();
}; };
onRoomMemberName = (ev, member) => { private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
if (member.roomId !== this.props.roomId) { if (member.roomId !== this.props.roomId) {
return; return;
} }
this._updateList(); this.updateList();
}; };
onRoomStateEvent = (event, state) => { private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => {
if (event.getRoomId() === this.props.roomId && if (event.getRoomId() === this.props.roomId &&
event.getType() === "m.room.third_party_invite") { event.getType() === "m.room.third_party_invite") {
this._updateList(); this.updateList();
} }
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
}; };
_updateList = rate_limited_func(() => { private updateList = rateLimitedFunction(() => {
this._updateListNow(); this.updateListNow();
}, 500); }, 500);
_updateListNow() { private updateListNow(): void {
// console.log("Updating memberlist"); const members = this.roomMembers()
const newState = {
this.setState({
loading: false, loading: false,
members: this.roomMembers(), members: members,
}; filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery),
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery); filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery),
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery); });
this.setState(newState);
} }
getMembersWithUser() { private getMembersWithUser(): Array<RoomMember> {
if (!this.props.roomId) return []; if (!this.props.roomId) return [];
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId); const room = cli.getRoom(this.props.roomId);
@ -230,15 +260,18 @@ export default class MemberList extends React.Component {
const allMembers = Object.values(room.currentState.members); const allMembers = Object.values(room.currentState.members);
allMembers.forEach(function(member) { allMembers.forEach((member) => {
// work around a race where you might have a room member object // work around a race where you might have a room member object
// before the user object exists. This may or may not cause // before the user object exists. This may or may not cause
// https://github.com/vector-im/vector-web/issues/186 // https://github.com/vector-im/vector-web/issues/186
if (member.user === null) { if (!member.user) {
member.user = cli.getUser(member.userId); member.user = cli.getUser(member.userId);
} }
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""); this.sortNames.set(
member,
(member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""),
);
// XXX: this user may have no lastPresenceTs value! // XXX: this user may have no lastPresenceTs value!
// the right solution here is to fix the race rather than leave it as 0 // the right solution here is to fix the race rather than leave it as 0
@ -247,7 +280,7 @@ export default class MemberList extends React.Component {
return allMembers; return allMembers;
} }
roomMembers() { private roomMembers(): Array<RoomMember> {
const allMembers = this.getMembersWithUser(); const allMembers = this.getMembersWithUser();
const filteredAndSortedMembers = allMembers.filter((m) => { const filteredAndSortedMembers = allMembers.filter((m) => {
return ( return (
@ -255,23 +288,21 @@ export default class MemberList extends React.Component {
); );
}); });
const language = SettingsStore.getValue("language"); const language = SettingsStore.getValue("language");
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true }); this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false });
filteredAndSortedMembers.sort(this.memberSort); filteredAndSortedMembers.sort(this.memberSort);
return filteredAndSortedMembers; return filteredAndSortedMembers;
} }
_createOverflowTileJoined = (overflowCount, totalCount) => { private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList); return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
}; };
_createOverflowTileInvited = (overflowCount, totalCount) => { private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList); return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList);
}; };
_createOverflowTile = (overflowCount, totalCount, onClick) => { 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. // For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount }); const text = _t("and %(count)s others...", { count: overflowCount });
return ( return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ <EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
@ -281,31 +312,48 @@ export default class MemberList extends React.Component {
); );
}; };
_showMoreJoinedMemberList = () => { private showMoreJoinedMemberList = (): void => {
this.setState({ this.setState({
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT, truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
}); });
}; };
_showMoreInvitedMemberList = () => { private showMoreInvitedMemberList = (): void => {
this.setState({ this.setState({
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT, truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
}); });
}; };
memberString(member) { /**
* SHOULD ONLY BE USED BY TESTS
*/
public memberString(member: RoomMember): string {
if (!member) { if (!member) {
return "(null)"; return "(null)";
} else { } else {
const u = member.user; 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>") + ")"; return (
"(" +
member.name +
", " +
member.powerLevel +
", " +
(u ? u.lastActiveAgo : "<null>") +
", " +
(u ? u.getLastActiveTs() : "<null>") +
", " +
(u ? u.currentlyActive : "<null>") +
", " +
(u ? u.presence : "<null>") +
")"
);
} }
} }
// returns negative if a comes before b, // returns negative if a comes before b,
// returns 0 if a and b are equivalent in ordering // returns 0 if a and b are equivalent in ordering
// returns positive if a comes after b. // returns positive if a comes after b.
memberSort = (memberA, memberB) => { private memberSort = (memberA: RoomMember, memberB: RoomMember): number => {
// order by presence, with "active now" first. // order by presence, with "active now" first.
// ...and then by power level // ...and then by power level
// ...and then by last active // ...and then by last active
@ -325,7 +373,7 @@ export default class MemberList extends React.Component {
if (!userA && userB) return 1; if (!userA && userB) return 1;
// First by presence // First by presence
if (this._showPresence) { if (this.showPresence) {
const convertPresence = (p) => p === 'unavailable' ? 'online' : p; const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
const presenceIndex = p => { const presenceIndex = p => {
const order = ['active', 'online', 'offline']; const order = ['active', 'online', 'offline'];
@ -349,31 +397,31 @@ export default class MemberList extends React.Component {
} }
// Third by last active // Third by last active
if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
// console.log("Comparing on last active timestamp - returning"); // console.log("Comparing on last active timestamp - returning");
return userB.getLastActiveTs() - userA.getLastActiveTs(); return userB.getLastActiveTs() - userA.getLastActiveTs();
} }
// Fourth by name (alphabetical) // Fourth by name (alphabetical)
return this.collator.compare(memberA.sortName, memberB.sortName); return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB));
}; };
onSearchQueryChanged = searchQuery => { private onSearchQueryChanged = (searchQuery: string): void => {
this.setState({ this.setState({
searchQuery, searchQuery,
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery), filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery), filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
}); });
}; };
_onPending3pidInviteClick = inviteEvent => { private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
dis.dispatch({ dis.dispatch({
action: 'view_3pid_invite', action: 'view_3pid_invite',
event: inviteEvent, event: inviteEvent,
}); });
}; };
_filterMembers(members, membership, query) { private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> {
return members.filter((m) => { return members.filter((m) => {
if (query) { if (query) {
query = query.toLowerCase(); query = query.toLowerCase();
@ -389,7 +437,7 @@ export default class MemberList extends React.Component {
}); });
} }
_getPending3PidInvites() { private getPending3PidInvites(): Array<MatrixEvent> {
// include 3pid invites (m.room.third_party_invite) state events. // include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so // 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 // we shouldn't add them if the 3pid invite state key (token) is in the
@ -409,42 +457,40 @@ export default class MemberList extends React.Component {
} }
} }
_makeMemberTiles(members) { private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) {
const MemberTile = sdk.getComponent("rooms.MemberTile");
const EntityTile = sdk.getComponent("rooms.EntityTile");
return members.map((m) => { return members.map((m) => {
if (m.userId) { if (m instanceof RoomMember) {
// Is a Matrix invite // Is a Matrix invite
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />; return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
} else { } else {
// Is a 3pid invite // Is a 3pid invite
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true} return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
onClick={() => this._onPending3pidInviteClick(m)} />; onClick={() => this.onPending3pidInviteClick(m)} />;
} }
}); });
} }
_getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end)); private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end))
_getChildCountJoined = () => this.state.filteredJoinedMembers.length;
_getChildrenInvited = (start, end) => {
let targets = this.state.filteredInvitedMembers;
if (end > this.state.filteredInvitedMembers.length) {
targets = targets.concat(this._getPending3PidInvites());
}
return this._makeMemberTiles(targets.slice(start, end));
}; };
_getChildCountInvited = () => { private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).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;
} }
render() { render() {
if (this.state.loading) { if (this.state.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return <BaseCard return <BaseCard
className="mx_MemberList" className="mx_MemberList"
onClose={this.props.onClose} onClose={this.props.onClose}
@ -454,9 +500,6 @@ export default class MemberList extends React.Component {
</BaseCard>; </BaseCard>;
} }
const SearchBox = sdk.getComponent('structures.SearchBox');
const TruncatedList = sdk.getComponent("elements.TruncatedList");
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId); const room = cli.getRoom(this.props.roomId);
let inviteButton; let inviteButton;
@ -470,22 +513,30 @@ export default class MemberList extends React.Component {
inviteButtonText = _t("Invite to this space"); inviteButtonText = _t("Invite to this space");
} }
const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); inviteButton = (
inviteButton = <AccessibleButton
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}> className="mx_MemberList_invite"
onClick={this.onInviteButtonClick}
disabled={!this.state.canInvite}
>
<span>{ inviteButtonText }</span> <span>{ inviteButtonText }</span>
</AccessibleButton>; </AccessibleButton>
);
} }
let invitedHeader; let invitedHeader;
let invitedSection; let invitedSection;
if (this._getChildCountInvited() > 0) { if (this.getChildCountInvited() > 0) {
invitedHeader = <h2>{ _t("Invited") }</h2>; invitedHeader = <h2>{ _t("Invited") }</h2>;
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited} invitedSection = (
createOverflowElement={this._createOverflowTileInvited} <TruncatedList
getChildren={this._getChildrenInvited} className="mx_MemberList_section mx_MemberList_invited"
getChildCount={this._getChildCountInvited} truncateAt={this.state.truncateAtInvited}
/>; createOverflowElement={this.createOverflowTileInvited}
getChildren={this.getChildrenInvited}
getChildCount={this.getChildCountInvited}
/>
);
} }
const footer = ( const footer = (
@ -517,17 +568,19 @@ export default class MemberList extends React.Component {
previousPhase={previousPhase} previousPhase={previousPhase}
> >
<div className="mx_MemberList_wrapper"> <div className="mx_MemberList_wrapper">
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined} <TruncatedList
createOverflowElement={this._createOverflowTileJoined} className="mx_MemberList_section mx_MemberList_joined"
getChildren={this._getChildrenJoined} truncateAt={this.state.truncateAtJoined}
getChildCount={this._getChildCountJoined} /> createOverflowElement={this.createOverflowTileJoined}
getChildren={this.getChildrenJoined}
getChildCount={this.getChildCountJoined} />
{ invitedHeader } { invitedHeader }
{ invitedSection } { invitedSection }
</div> </div>
</BaseCard>; </BaseCard>;
} }
onInviteButtonClick = () => { onInviteButtonClick = (): void => {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'}); dis.dispatch({action: 'require_registration'});
return; return;

View file

@ -1,21 +1,36 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; import React from 'react';
import ReactTestUtils from 'react-dom/test-utils'; import ReactTestUtils from 'react-dom/test-utils';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import * as TestUtils from '../../../test-utils'; import * as TestUtils from '../../../test-utils';
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
import sdk from '../../../skinned-sdk'; import sdk from '../../../skinned-sdk';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
import {Room, RoomMember, User} from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { User } from "matrix-js-sdk/src/models/user";
import { compare } from "../../../../src/utils/strings"; import { compare } from "../../../../src/utils/strings";
import MemberList from "../../../../src/components/views/rooms/MemberList";
function generateRoomId() { function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain'; return '!' + Math.random().toString().slice(2, 10) + ':domain';
} }
describe('MemberList', () => { describe('MemberList', () => {
function createRoom(opts) { function createRoom(opts) {
const room = new Room(generateRoomId(), null, client.getUserId()); const room = new Room(generateRoomId(), null, client.getUserId());
@ -97,13 +112,19 @@ describe('MemberList', () => {
memberListRoom.currentState.members[member.userId] = member; memberListRoom.currentState.members[member.userId] = member;
} }
const MemberList = sdk.getComponent('views.rooms.MemberList');
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList); const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
const gatherWrappedRef = (r) => { const gatherWrappedRef = (r) => {
memberList = r; memberList = r;
}; };
root = ReactDOM.render(<WrappedMemberList roomId={memberListRoom.roomId} root = ReactDOM.render(
wrappedRef={gatherWrappedRef} />, parentDiv); (
<WrappedMemberList
roomId={memberListRoom.roomId}
wrappedRef={gatherWrappedRef}
/>
),
parentDiv,
);
}); });
afterEach((done) => { afterEach((done) => {
@ -213,8 +234,8 @@ describe('MemberList', () => {
}); });
// Bypass all the event listeners and skip to the good part // Bypass all the event listeners and skip to the good part
memberList._showPresence = enablePresence; memberList.showPresence = enablePresence;
memberList._updateListNow(); memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
@ -225,7 +246,7 @@ describe('MemberList', () => {
// Bypass all the event listeners and skip to the good part // Bypass all the event listeners and skip to the good part
memberList._showPresence = enablePresence; memberList._showPresence = enablePresence;
memberList._updateListNow(); memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
@ -254,8 +275,8 @@ describe('MemberList', () => {
}); });
// Bypass all the event listeners and skip to the good part // Bypass all the event listeners and skip to the good part
memberList._showPresence = enablePresence; memberList.showPresence = enablePresence;
memberList._updateListNow(); memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
@ -273,8 +294,8 @@ describe('MemberList', () => {
}); });
// Bypass all the event listeners and skip to the good part // Bypass all the event listeners and skip to the good part
memberList._showPresence = enablePresence; memberList.showPresence = enablePresence;
memberList._updateListNow(); memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);