2015-06-23 17:41:25 +02:00
|
|
|
/*
|
2016-01-07 05:06:39 +01:00
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2017-07-25 16:56:16 +02:00
|
|
|
Copyright 2017 Vector Creations Ltd
|
2018-03-19 17:47:12 +01:00
|
|
|
Copyright 2017, 2018 New Vector Ltd
|
2021-06-23 13:45:55 +02:00
|
|
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
2015-06-23 17:41:25 +02:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2017-09-22 14:15:02 +02:00
|
|
|
|
|
|
|
import React from 'react';
|
2017-05-25 12:39:08 +02:00
|
|
|
import { _t } from '../../../languageHandler';
|
2018-03-19 17:47:12 +01:00
|
|
|
import SdkConfig from '../../../SdkConfig';
|
2020-05-14 04:41:41 +02:00
|
|
|
import dis from '../../../dispatcher/dispatcher';
|
2021-06-23 13:45:55 +02:00
|
|
|
import { isValid3pidInvite } from "../../../RoomInvite";
|
|
|
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
|
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
2020-09-08 11:19:51 +02:00
|
|
|
import BaseCard from "../right_panel/BaseCard";
|
2021-06-23 13:45:55 +02:00
|
|
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
2021-03-02 10:51:11 +01:00
|
|
|
import RoomAvatar from "../avatars/RoomAvatar";
|
|
|
|
import RoomName from "../elements/RoomName";
|
2021-06-23 13:45:55 +02:00
|
|
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
2021-05-06 00:59:07 +02:00
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2021-06-23 13:45:55 +02:00
|
|
|
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';
|
2021-07-01 19:35:38 +02:00
|
|
|
import { throttle } from 'lodash';
|
2021-11-11 14:07:41 +01:00
|
|
|
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
2021-10-09 00:04:26 +02:00
|
|
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
|
|
|
import { UIComponent } from "../../../settings/UIFeature";
|
2021-09-29 15:04:02 +02:00
|
|
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
2015-06-22 12:42:09 +02:00
|
|
|
|
2017-09-22 14:15:02 +02:00
|
|
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
2017-09-22 17:50:54 +02:00
|
|
|
const INITIAL_LOAD_NUM_INVITED = 5;
|
2017-09-22 18:01:14 +02:00
|
|
|
const SHOW_MORE_INCREMENT = 100;
|
2016-01-15 16:18:55 +01:00
|
|
|
|
2019-12-27 18:04:14 +01:00
|
|
|
// Regex applied to filter our punctuation in member names before applying sort, to fuzzy it a little
|
|
|
|
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
|
|
|
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
2019-12-26 20:27:04 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
interface IProps {
|
|
|
|
roomId: string;
|
2021-11-08 10:57:14 +01:00
|
|
|
searchQuery: string;
|
2021-06-23 13:45:55 +02:00
|
|
|
onClose(): void;
|
2021-11-08 10:57:14 +01:00
|
|
|
onSearchQueryChanged: (query: string) => void;
|
2021-06-23 13:45:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface IState {
|
|
|
|
loading: boolean;
|
|
|
|
members: Array<RoomMember>;
|
|
|
|
filteredJoinedMembers: Array<RoomMember>;
|
|
|
|
filteredInvitedMembers: Array<RoomMember | MatrixEvent>;
|
|
|
|
canInvite: boolean;
|
|
|
|
truncateAtJoined: number;
|
|
|
|
truncateAtInvited: number;
|
|
|
|
}
|
|
|
|
|
2021-03-09 04:12:00 +01:00
|
|
|
@replaceableComponent("views.rooms.MemberList")
|
2021-06-23 13:45:55 +02:00
|
|
|
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
|
|
|
|
|
2020-08-29 13:14:16 +02:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2016-02-14 23:37:59 +01:00
|
|
|
|
2018-09-03 11:13:56 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
if (cli.hasLazyLoadMembersEnabled()) {
|
2018-09-17 19:18:55 +02:00
|
|
|
// show an empty list
|
2021-06-23 13:45:55 +02:00
|
|
|
this.state = this.getMembersState([]);
|
2018-09-03 11:13:56 +02:00
|
|
|
} else {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.state = this.getMembersState(this.roomMembers());
|
2018-09-03 11:13:56 +02:00
|
|
|
}
|
2018-08-29 10:30:25 +02:00
|
|
|
|
2018-09-13 18:43:24 +02:00
|
|
|
cli.on("Room", this.onRoom); // invites & joining after peek
|
2018-09-07 12:00:19 +02:00
|
|
|
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
|
|
|
const hsUrl = MatrixClientPeg.get().baseUrl;
|
2021-06-24 14:15:44 +02:00
|
|
|
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2018-09-07 12:00:19 +02:00
|
|
|
|
2021-07-19 23:43:11 +02:00
|
|
|
// eslint-disable-next-line
|
2020-09-03 11:34:19 +02:00
|
|
|
UNSAFE_componentWillMount() {
|
2020-08-29 13:51:37 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
2021-06-23 13:45:55 +02:00
|
|
|
this.mounted = true;
|
2020-08-29 13:51:37 +02:00
|
|
|
if (cli.hasLazyLoadMembersEnabled()) {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.showMembersAccordingToMembershipWithLL();
|
2020-08-29 13:51:37 +02:00
|
|
|
cli.on("Room.myMembership", this.onMyMembership);
|
|
|
|
} else {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.listenForMembersChanges();
|
2020-08-29 13:51:37 +02:00
|
|
|
}
|
|
|
|
}
|
2018-09-07 12:00:19 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private listenForMembersChanges(): void {
|
2017-10-11 18:56:17 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
2015-06-22 12:42:09 +02:00
|
|
|
cli.on("RoomState.members", this.onRoomStateMember);
|
2015-10-28 16:15:35 +01:00
|
|
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
2016-01-22 16:11:36 +01:00
|
|
|
cli.on("RoomState.events", this.onRoomStateEvent);
|
2016-07-14 11:05:40 +02:00
|
|
|
// 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).
|
2018-12-20 22:56:18 +01:00
|
|
|
cli.on("User.lastPresenceTs", this.onUserPresenceChange);
|
|
|
|
cli.on("User.presence", this.onUserPresenceChange);
|
|
|
|
cli.on("User.currentlyActive", this.onUserPresenceChange);
|
2016-04-18 02:35:40 +02:00
|
|
|
// cli.on("Room.timeline", this.onRoomTimeline);
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2018-08-29 11:09:55 +02:00
|
|
|
|
2020-08-29 13:14:16 +02:00
|
|
|
componentWillUnmount() {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.mounted = false;
|
2017-10-11 18:56:17 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
2016-04-18 02:35:40 +02:00
|
|
|
if (cli) {
|
|
|
|
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
|
|
|
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
2018-09-17 19:14:52 +02:00
|
|
|
cli.removeListener("Room.myMembership", this.onMyMembership);
|
2016-04-18 02:35:40 +02:00
|
|
|
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
|
|
|
cli.removeListener("Room", this.onRoom);
|
2018-12-20 22:56:18 +01:00
|
|
|
cli.removeListener("User.lastPresenceTs", this.onUserPresenceChange);
|
|
|
|
cli.removeListener("User.presence", this.onUserPresenceChange);
|
|
|
|
cli.removeListener("User.currentlyActive", this.onUserPresenceChange);
|
2015-06-22 12:42:09 +02:00
|
|
|
}
|
2016-08-10 14:39:47 +02:00
|
|
|
|
|
|
|
// cancel any pending calls to the rate_limited_funcs
|
2021-07-01 19:35:38 +02:00
|
|
|
this.updateList.cancel();
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2015-06-22 12:42:09 +02:00
|
|
|
|
2018-09-17 19:20:26 +02:00
|
|
|
/**
|
|
|
|
* If lazy loading is enabled, either:
|
|
|
|
* show a spinner and load the members if the user is joined,
|
2018-09-17 20:03:01 +02:00
|
|
|
* or show the members available so far if the user is invited
|
2018-09-17 19:20:26 +02:00
|
|
|
*/
|
2021-06-23 13:45:55 +02:00
|
|
|
private async showMembersAccordingToMembershipWithLL(): Promise<void> {
|
2018-08-29 10:30:25 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
2018-09-13 18:43:24 +02:00
|
|
|
if (cli.hasLazyLoadMembersEnabled()) {
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
const room = cli.getRoom(this.props.roomId);
|
2018-09-17 20:03:01 +02:00
|
|
|
const membership = room && room.getMyMembership();
|
|
|
|
if (membership === "join") {
|
2021-06-29 14:11:58 +02:00
|
|
|
this.setState({ loading: true });
|
2018-09-13 18:43:24 +02:00
|
|
|
try {
|
|
|
|
await room.loadMembersIfNeeded();
|
2018-09-17 20:01:55 +02:00
|
|
|
} catch (ex) {/* already logged in RoomView */}
|
2021-06-23 13:45:55 +02:00
|
|
|
if (this.mounted) {
|
|
|
|
this.setState(this.getMembersState(this.roomMembers()));
|
|
|
|
this.listenForMembersChanges();
|
2018-09-13 18:43:24 +02:00
|
|
|
}
|
2020-10-01 13:30:41 +02:00
|
|
|
} else {
|
|
|
|
// show the members we already have loaded
|
2021-06-23 13:45:55 +02:00
|
|
|
this.setState(this.getMembersState(this.roomMembers()));
|
2018-09-13 18:43:24 +02:00
|
|
|
}
|
2018-08-29 10:30:25 +02:00
|
|
|
}
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2018-08-29 10:30:25 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private get canInvite(): boolean {
|
2021-05-19 14:16:10 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
const room = cli.getRoom(this.props.roomId);
|
2021-09-29 15:04:02 +02:00
|
|
|
|
|
|
|
return (
|
|
|
|
room?.canInvite(cli.getUserId()) ||
|
|
|
|
(room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public)
|
|
|
|
);
|
2021-05-19 14:16:10 +02:00
|
|
|
}
|
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private getMembersState(members: Array<RoomMember>): IState {
|
|
|
|
// set the state after determining showPresence to make sure it's
|
|
|
|
// taken into account while rendering
|
2018-09-03 11:13:56 +02:00
|
|
|
return {
|
|
|
|
loading: false,
|
|
|
|
members: members,
|
2021-11-08 10:57:14 +01:00
|
|
|
filteredJoinedMembers: this.filterMembers(members, 'join', this.props.searchQuery),
|
|
|
|
filteredInvitedMembers: this.filterMembers(members, 'invite', this.props.searchQuery),
|
2021-05-19 14:16:10 +02:00
|
|
|
canInvite: this.canInvite,
|
2018-09-03 11:13:56 +02:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
};
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2018-09-03 11:13:56 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onUserPresenceChange = (event: MatrixEvent, user: User): void => {
|
2015-09-18 19:39:16 +02:00
|
|
|
// Attach a SINGLE listener for global presence changes then locate the
|
|
|
|
// member tile and re-render it. This is more efficient than every tile
|
2018-12-20 22:56:18 +01:00
|
|
|
// ever attaching their own listener.
|
2017-10-11 18:56:17 +02:00
|
|
|
const tile = this.refs[user.userId];
|
2018-12-20 22:57:27 +01:00
|
|
|
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
2016-04-18 02:35:40 +02:00
|
|
|
if (tile) {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.updateList(); // reorder the membership list
|
2015-09-18 19:39:16 +02:00
|
|
|
}
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2016-01-20 15:41:48 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onRoom = (room: Room): void => {
|
2015-09-18 19:39:16 +02:00
|
|
|
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.
|
2021-06-23 13:45:55 +02:00
|
|
|
this.showMembersAccordingToMembershipWithLL();
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2018-09-13 18:43:24 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onMyMembership = (room: Room, membership: string, oldMembership: string): void => {
|
2018-09-17 19:14:52 +02:00
|
|
|
if (room.roomId === this.props.roomId && membership === "join") {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.showMembersAccordingToMembershipWithLL();
|
2018-09-13 18:43:24 +02:00
|
|
|
}
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2015-09-18 19:39:16 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => {
|
2018-09-07 14:05:26 +02:00
|
|
|
if (member.roomId !== this.props.roomId) {
|
|
|
|
return;
|
|
|
|
}
|
2021-06-23 13:45:55 +02:00
|
|
|
this.updateList();
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2015-09-18 19:39:16 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
|
2018-09-07 14:05:26 +02:00
|
|
|
if (member.roomId !== this.props.roomId) {
|
|
|
|
return;
|
|
|
|
}
|
2021-06-23 13:45:55 +02:00
|
|
|
this.updateList();
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2015-10-28 16:15:35 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => {
|
2018-09-07 14:05:26 +02:00
|
|
|
if (event.getRoomId() === this.props.roomId &&
|
|
|
|
event.getType() === "m.room.third_party_invite") {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.updateList();
|
2016-01-22 16:11:36 +01:00
|
|
|
}
|
2021-05-19 11:01:04 +02:00
|
|
|
|
2021-05-19 14:16:10 +02:00
|
|
|
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2016-01-22 16:11:36 +01:00
|
|
|
|
2021-07-01 19:35:38 +02:00
|
|
|
private updateList = throttle(() => {
|
2021-06-23 13:45:55 +02:00
|
|
|
this.updateListNow();
|
2021-07-01 19:35:38 +02:00
|
|
|
}, 500, { leading: true, trailing: true });
|
2019-01-04 05:55:52 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private updateListNow(): void {
|
2021-06-29 14:11:58 +02:00
|
|
|
const members = this.roomMembers();
|
2021-06-23 13:45:55 +02:00
|
|
|
|
|
|
|
this.setState({
|
2018-09-13 18:43:24 +02:00
|
|
|
loading: false,
|
2021-06-23 13:45:55 +02:00
|
|
|
members: members,
|
2021-11-08 10:57:14 +01:00
|
|
|
filteredJoinedMembers: this.filterMembers(members, 'join', this.props.searchQuery),
|
|
|
|
filteredInvitedMembers: this.filterMembers(members, 'invite', this.props.searchQuery),
|
2021-06-23 13:45:55 +02:00
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2015-06-24 14:48:39 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private getMembersWithUser(): Array<RoomMember> {
|
2018-09-07 23:44:20 +02:00
|
|
|
if (!this.props.roomId) return [];
|
2017-10-11 18:56:17 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
const room = cli.getRoom(this.props.roomId);
|
2018-09-10 17:32:40 +02:00
|
|
|
if (!room) return [];
|
2015-10-09 18:24:48 +02:00
|
|
|
|
2018-09-07 23:44:20 +02:00
|
|
|
const allMembers = Object.values(room.currentState.members);
|
2015-09-18 19:39:16 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
allMembers.forEach((member) => {
|
2016-03-04 17:57:20 +01:00
|
|
|
// work around a race where you might have a room member object
|
2021-06-23 13:45:55 +02:00
|
|
|
// before the user object exists. This may or may not cause
|
2016-03-04 17:57:20 +01:00
|
|
|
// https://github.com/vector-im/vector-web/issues/186
|
2021-06-23 13:45:55 +02:00
|
|
|
if (!member.user) {
|
2018-09-05 15:25:08 +02:00
|
|
|
member.user = cli.getUser(member.userId);
|
2016-03-04 17:57:20 +01:00
|
|
|
}
|
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
this.sortNames.set(
|
|
|
|
member,
|
|
|
|
(member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""),
|
|
|
|
);
|
2021-06-02 11:42:17 +02:00
|
|
|
|
2016-04-18 02:35:40 +02:00
|
|
|
// XXX: this user may have no lastPresenceTs value!
|
|
|
|
// the right solution here is to fix the race rather than leave it as 0
|
2015-09-28 12:32:00 +02:00
|
|
|
});
|
|
|
|
|
2018-09-07 23:44:20 +02:00
|
|
|
return allMembers;
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2015-09-18 19:39:16 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private roomMembers(): Array<RoomMember> {
|
2018-09-07 23:44:20 +02:00
|
|
|
const allMembers = this.getMembersWithUser();
|
2018-09-05 15:25:08 +02:00
|
|
|
const filteredAndSortedMembers = allMembers.filter((m) => {
|
|
|
|
return (
|
|
|
|
m.membership === 'join' || m.membership === 'invite'
|
|
|
|
);
|
|
|
|
});
|
2021-06-02 11:42:17 +02:00
|
|
|
const language = SettingsStore.getValue("language");
|
2021-06-23 13:45:55 +02:00
|
|
|
this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false });
|
2018-09-05 15:25:08 +02:00
|
|
|
filteredAndSortedMembers.sort(this.memberSort);
|
|
|
|
return filteredAndSortedMembers;
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2015-11-30 16:13:28 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
|
|
|
|
return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
|
2020-09-04 10:24:12 +02:00
|
|
|
};
|
2017-09-22 17:50:54 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
|
|
|
|
return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList);
|
2020-09-04 10:24:12 +02:00
|
|
|
};
|
2017-09-22 17:50:54 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> {
|
2016-01-21 12:41:28 +01:00
|
|
|
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
2017-07-25 16:56:16 +02:00
|
|
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
2016-01-21 12:41:28 +01:00
|
|
|
return (
|
2021-07-23 11:23:45 +02:00
|
|
|
<EntityTile
|
|
|
|
className="mx_EntityTile_ellipsis"
|
|
|
|
avatarJsx={
|
|
|
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
|
|
|
}
|
|
|
|
name={text}
|
|
|
|
presenceState="online"
|
|
|
|
suppressOnHover={true}
|
|
|
|
onClick={onClick}
|
|
|
|
/>
|
2016-01-21 12:41:28 +01:00
|
|
|
);
|
2020-09-04 10:24:12 +02:00
|
|
|
};
|
2015-11-30 16:13:28 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private showMoreJoinedMemberList = (): void => {
|
2017-09-22 17:50:54 +02:00
|
|
|
this.setState({
|
2017-09-22 18:01:14 +02:00
|
|
|
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
2017-09-22 17:50:54 +02:00
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2017-09-22 17:50:54 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private showMoreInvitedMemberList = (): void => {
|
2016-01-21 16:57:59 +01:00
|
|
|
this.setState({
|
2017-09-22 18:01:14 +02:00
|
|
|
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
2016-01-21 16:57:59 +01:00
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2015-11-30 16:13:28 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
/**
|
|
|
|
* SHOULD ONLY BE USED BY TESTS
|
|
|
|
*/
|
|
|
|
public memberString(member: RoomMember): string {
|
2016-04-18 02:35:40 +02:00
|
|
|
if (!member) {
|
|
|
|
return "(null)";
|
2017-10-11 18:56:17 +02:00
|
|
|
} else {
|
2018-12-20 22:56:18 +01:00
|
|
|
const u = member.user;
|
2021-06-23 13:45:55 +02:00
|
|
|
return (
|
|
|
|
"(" +
|
|
|
|
member.name +
|
|
|
|
", " +
|
|
|
|
member.powerLevel +
|
|
|
|
", " +
|
|
|
|
(u ? u.lastActiveAgo : "<null>") +
|
|
|
|
", " +
|
|
|
|
(u ? u.getLastActiveTs() : "<null>") +
|
|
|
|
", " +
|
|
|
|
(u ? u.currentlyActive : "<null>") +
|
|
|
|
", " +
|
|
|
|
(u ? u.presence : "<null>") +
|
|
|
|
")"
|
|
|
|
);
|
2016-04-18 02:35:40 +02:00
|
|
|
}
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2015-11-30 16:13:28 +01:00
|
|
|
|
2016-04-18 15:48:55 +02:00
|
|
|
// returns negative if a comes before b,
|
|
|
|
// returns 0 if a and b are equivalent in ordering
|
|
|
|
// returns positive if a comes after b.
|
2021-06-23 13:45:55 +02:00
|
|
|
private memberSort = (memberA: RoomMember, memberB: RoomMember): number => {
|
2018-12-20 22:56:18 +01:00
|
|
|
// order by presence, with "active now" first.
|
|
|
|
// ...and then by power level
|
|
|
|
// ...and then by last active
|
|
|
|
// ...and then alphabetically.
|
|
|
|
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
|
|
|
|
|
2018-12-20 22:57:27 +01:00
|
|
|
// console.log(`Comparing userA=${this.memberString(memberA)} userB=${this.memberString(memberB)}`);
|
2018-12-20 22:56:18 +01:00
|
|
|
|
|
|
|
const userA = memberA.user;
|
|
|
|
const userB = memberB.user;
|
|
|
|
|
2018-12-20 22:57:27 +01:00
|
|
|
// if (!userA) console.log("!! MISSING USER FOR A-SIDE: " + memberA.name + " !!");
|
|
|
|
// if (!userB) console.log("!! MISSING USER FOR B-SIDE: " + memberB.name + " !!");
|
2018-12-20 22:56:18 +01:00
|
|
|
|
|
|
|
if (!userA && !userB) return 0;
|
|
|
|
if (userA && !userB) return -1;
|
|
|
|
if (!userA && userB) return 1;
|
|
|
|
|
|
|
|
// First by presence
|
2021-06-23 13:45:55 +02:00
|
|
|
if (this.showPresence) {
|
2018-12-20 22:56:18 +01:00
|
|
|
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
|
|
|
const presenceIndex = p => {
|
|
|
|
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);
|
2018-12-20 22:57:27 +01:00
|
|
|
// console.log(`userA_presenceGroup=${idxA} userB_presenceGroup=${idxB}`);
|
2018-12-20 22:56:18 +01:00
|
|
|
if (idxA !== idxB) {
|
2018-12-20 22:57:27 +01:00
|
|
|
// console.log("Comparing on presence group - returning");
|
2018-12-20 22:56:18 +01:00
|
|
|
return idxA - idxB;
|
2016-04-18 02:35:40 +02:00
|
|
|
}
|
2018-12-20 22:56:18 +01:00
|
|
|
}
|
2016-04-18 02:35:40 +02:00
|
|
|
|
2018-12-20 22:56:18 +01:00
|
|
|
// Second by power level
|
|
|
|
if (memberA.powerLevel !== memberB.powerLevel) {
|
2018-12-20 22:57:27 +01:00
|
|
|
// console.log("Comparing on power level - returning");
|
2018-12-20 22:56:18 +01:00
|
|
|
return memberB.powerLevel - memberA.powerLevel;
|
|
|
|
}
|
2015-11-30 16:13:28 +01:00
|
|
|
|
2018-12-20 22:56:18 +01:00
|
|
|
// Third by last active
|
2021-06-23 13:45:55 +02:00
|
|
|
if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
2018-12-20 22:57:27 +01:00
|
|
|
// console.log("Comparing on last active timestamp - returning");
|
2016-06-29 10:45:24 +02:00
|
|
|
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
2018-12-20 22:56:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fourth by name (alphabetical)
|
2021-06-23 13:45:55 +02:00
|
|
|
return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB));
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2015-11-30 16:13:28 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onSearchQueryChanged = (searchQuery: string): void => {
|
2021-11-08 10:57:14 +01:00
|
|
|
this.props.onSearchQueryChanged(searchQuery);
|
2017-09-22 14:15:02 +02:00
|
|
|
this.setState({
|
2021-06-23 13:45:55 +02:00
|
|
|
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
|
|
|
|
filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
|
2017-09-22 14:15:02 +02:00
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2016-01-19 15:51:26 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
2019-03-29 03:38:15 +01:00
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_3pid_invite',
|
|
|
|
event: inviteEvent,
|
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
2019-03-29 03:38:15 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> {
|
2018-09-05 15:25:08 +02:00
|
|
|
return members.filter((m) => {
|
2016-07-13 12:56:01 +02:00
|
|
|
if (query) {
|
2017-10-14 18:09:58 +02:00
|
|
|
query = query.toLowerCase();
|
2016-07-13 12:56:01 +02:00
|
|
|
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
|
|
|
const matchesId = m.userId.toLowerCase().indexOf(query) !== -1;
|
|
|
|
|
|
|
|
if (!matchesName && !matchesId) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-01-19 15:51:26 +01:00
|
|
|
}
|
2016-07-13 12:56:01 +02:00
|
|
|
|
2017-10-14 18:10:51 +02:00
|
|
|
return m.membership === membership;
|
2017-09-22 14:15:02 +02:00
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2017-09-22 14:15:02 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private getPending3PidInvites(): Array<MatrixEvent> {
|
2018-01-25 13:18:02 +01:00
|
|
|
// 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.get().getRoom(this.props.roomId);
|
|
|
|
|
|
|
|
if (room) {
|
|
|
|
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
|
2019-03-29 18:45:07 +01:00
|
|
|
if (!isValid3pidInvite(e)) return false;
|
2018-01-25 13:18:02 +01:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
});
|
|
|
|
}
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2018-01-25 13:18:02 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) {
|
2019-07-03 09:38:08 +02:00
|
|
|
return members.map((m) => {
|
2021-06-23 13:45:55 +02:00
|
|
|
if (m instanceof RoomMember) {
|
2019-07-03 09:38:08 +02:00
|
|
|
// Is a Matrix invite
|
2021-06-23 13:45:55 +02:00
|
|
|
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
2019-07-03 09:38:08 +02:00
|
|
|
} else {
|
|
|
|
// Is a 3pid invite
|
2021-07-23 11:23:45 +02:00
|
|
|
return <EntityTile
|
|
|
|
key={m.getStateKey()}
|
|
|
|
name={m.getContent().display_name}
|
|
|
|
suppressOnHover={true}
|
|
|
|
onClick={() => this.onPending3pidInviteClick(m)}
|
|
|
|
/>;
|
2019-07-03 09:38:08 +02:00
|
|
|
}
|
2015-11-30 16:13:28 +01:00
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2015-11-30 16:13:28 +01:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
|
2021-06-29 14:11:58 +02:00
|
|
|
return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
|
2021-06-23 13:45:55 +02:00
|
|
|
};
|
2017-09-22 14:15:02 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
|
2017-09-22 14:15:02 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private getChildrenInvited = (start: number, end: number): Array<JSX.Element> => {
|
2019-07-03 09:38:08 +02:00
|
|
|
let targets = this.state.filteredInvitedMembers;
|
|
|
|
if (end > this.state.filteredInvitedMembers.length) {
|
2021-06-23 13:45:55 +02:00
|
|
|
targets = targets.concat(this.getPending3PidInvites());
|
2019-07-03 09:38:08 +02:00
|
|
|
}
|
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
return this.makeMemberTiles(targets.slice(start, end));
|
2020-08-29 13:51:37 +02:00
|
|
|
};
|
2017-09-22 17:50:54 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
private getChildCountInvited = (): number => {
|
|
|
|
return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length;
|
2021-06-29 14:11:58 +02:00
|
|
|
};
|
2017-09-22 17:50:54 +02:00
|
|
|
|
2020-08-29 13:14:16 +02:00
|
|
|
render() {
|
2018-08-29 10:30:25 +02:00
|
|
|
if (this.state.loading) {
|
2020-09-08 11:19:51 +02:00
|
|
|
return <BaseCard
|
|
|
|
className="mx_MemberList"
|
|
|
|
onClose={this.props.onClose}
|
|
|
|
previousPhase={RightPanelPhases.RoomSummary}
|
|
|
|
>
|
|
|
|
<Spinner />
|
|
|
|
</BaseCard>;
|
2018-08-29 10:30:25 +02:00
|
|
|
}
|
|
|
|
|
2018-10-24 13:50:58 +02:00
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
const room = cli.getRoom(this.props.roomId);
|
|
|
|
let inviteButton;
|
2019-05-11 20:45:24 +02:00
|
|
|
|
2021-10-09 00:04:26 +02:00
|
|
|
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
|
2020-08-31 18:12:12 +02:00
|
|
|
let inviteButtonText = _t("Invite to this room");
|
2020-08-31 18:19:05 +02:00
|
|
|
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
|
|
|
if (chat && chat.roomId === this.props.roomId) {
|
|
|
|
inviteButtonText = _t("Invite to this community");
|
2021-07-15 09:26:49 +02:00
|
|
|
} else if (SpaceStore.spacesEnabled && room.isSpaceRoom()) {
|
2021-03-02 10:51:11 +01:00
|
|
|
inviteButtonText = _t("Invite to this space");
|
2020-08-31 18:12:12 +02:00
|
|
|
}
|
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
inviteButton = (
|
|
|
|
<AccessibleButton
|
|
|
|
className="mx_MemberList_invite"
|
|
|
|
onClick={this.onInviteButtonClick}
|
|
|
|
disabled={!this.state.canInvite}
|
|
|
|
>
|
2020-08-31 18:12:12 +02:00
|
|
|
<span>{ inviteButtonText }</span>
|
2021-06-23 13:45:55 +02:00
|
|
|
</AccessibleButton>
|
|
|
|
);
|
2018-10-24 13:50:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
let invitedHeader;
|
|
|
|
let invitedSection;
|
2021-06-23 13:45:55 +02:00
|
|
|
if (this.getChildCountInvited() > 0) {
|
2018-10-24 13:50:58 +02:00
|
|
|
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
2021-06-23 13:45:55 +02:00
|
|
|
invitedSection = (
|
|
|
|
<TruncatedList
|
|
|
|
className="mx_MemberList_section mx_MemberList_invited"
|
|
|
|
truncateAt={this.state.truncateAtInvited}
|
|
|
|
createOverflowElement={this.createOverflowTileInvited}
|
|
|
|
getChildren={this.getChildrenInvited}
|
|
|
|
getChildCount={this.getChildCountInvited}
|
|
|
|
/>
|
|
|
|
);
|
2015-11-30 16:13:28 +01:00
|
|
|
}
|
2016-02-08 16:05:35 +01:00
|
|
|
|
2020-09-08 11:19:51 +02:00
|
|
|
const footer = (
|
|
|
|
<SearchBox
|
|
|
|
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
|
2021-07-19 23:43:11 +02:00
|
|
|
placeholder={_t('Filter room members')}
|
2021-08-19 15:48:14 +02:00
|
|
|
onSearch={this.onSearchQueryChanged}
|
2021-11-08 10:57:14 +01:00
|
|
|
initialValue={this.props.searchQuery}
|
2021-08-19 15:48:14 +02:00
|
|
|
/>
|
2015-11-30 16:13:28 +01:00
|
|
|
);
|
2020-09-08 11:19:51 +02:00
|
|
|
|
2021-03-02 10:51:11 +01:00
|
|
|
let previousPhase = RightPanelPhases.RoomSummary;
|
|
|
|
// We have no previousPhase for when viewing a MemberList from a Space
|
|
|
|
let scopeHeader;
|
2021-07-15 09:26:49 +02:00
|
|
|
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
|
2021-03-02 10:51:11 +01:00
|
|
|
previousPhase = undefined;
|
|
|
|
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
|
|
|
<RoomAvatar room={room} height={32} width={32} />
|
|
|
|
<RoomName room={room} />
|
|
|
|
</div>;
|
|
|
|
}
|
|
|
|
|
2020-09-08 11:19:51 +02:00
|
|
|
return <BaseCard
|
|
|
|
className="mx_MemberList"
|
2021-03-02 10:51:11 +01:00
|
|
|
header={<React.Fragment>
|
|
|
|
{ scopeHeader }
|
|
|
|
{ inviteButton }
|
|
|
|
</React.Fragment>}
|
2020-09-08 11:19:51 +02:00
|
|
|
footer={footer}
|
|
|
|
onClose={this.props.onClose}
|
2021-03-02 10:51:11 +01:00
|
|
|
previousPhase={previousPhase}
|
2020-09-08 11:19:51 +02:00
|
|
|
>
|
|
|
|
<div className="mx_MemberList_wrapper">
|
2021-06-23 13:45:55 +02:00
|
|
|
<TruncatedList
|
|
|
|
className="mx_MemberList_section mx_MemberList_joined"
|
|
|
|
truncateAt={this.state.truncateAtJoined}
|
|
|
|
createOverflowElement={this.createOverflowTileJoined}
|
|
|
|
getChildren={this.getChildrenJoined}
|
|
|
|
getChildCount={this.getChildCountJoined} />
|
2020-09-08 11:19:51 +02:00
|
|
|
{ invitedHeader }
|
|
|
|
{ invitedSection }
|
|
|
|
</div>
|
|
|
|
</BaseCard>;
|
2020-08-29 13:14:16 +02:00
|
|
|
}
|
2018-10-24 13:50:58 +02:00
|
|
|
|
2021-06-23 13:45:55 +02:00
|
|
|
onInviteButtonClick = (): void => {
|
2018-10-24 13:50:58 +02:00
|
|
|
if (MatrixClientPeg.get().isGuest()) {
|
2021-06-29 14:11:58 +02:00
|
|
|
dis.dispatch({ action: 'require_registration' });
|
2018-10-24 13:50:58 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// call AddressPickerDialog
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_invite',
|
|
|
|
roomId: this.props.roomId,
|
|
|
|
});
|
2020-08-29 13:14:16 +02:00
|
|
|
};
|
|
|
|
}
|