Merge pull request #6249 from SimonBrandner/ts/member-list
This commit is contained in:
commit
f965d449b8
3 changed files with 211 additions and 137 deletions
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
@ -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);
|
Loading…
Reference in a new issue