Allow options to cascade kicks/bans throughout spaces

This commit is contained in:
Michael Telatynski 2021-09-17 15:34:49 +01:00
parent cf8100ad17
commit 1858c63c4a
12 changed files with 481 additions and 177 deletions

View file

@ -0,0 +1,86 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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, { ComponentProps, useMemo, useState } from 'react';
import ConfirmUserActionDialog from "./ConfirmUserActionDialog";
import SpaceStore from "../../../stores/SpaceStore";
import { Room } from "matrix-js-sdk/src/models/room";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
import { _t } from '../../../languageHandler';
type BaseProps = ComponentProps<typeof ConfirmUserActionDialog>;
interface IProps extends Omit<BaseProps, "groupMember" | "matrixClient" | "children" | "onFinished"> {
space: Room;
allLabel: string;
specificLabel: string;
noneLabel?: string;
onFinished(success: boolean, reason?: string, rooms?: Room[]): void;
spaceChildFilter?(child: Room): boolean;
}
const ConfirmSpaceUserActionDialog: React.FC<IProps> = ({
space,
spaceChildFilter,
allLabel,
specificLabel,
noneLabel,
onFinished,
...props
}) => {
const spaceChildren = useMemo(() => {
const children = SpaceStore.instance.getChildren(space.roomId);
if (spaceChildFilter) {
return children.filter(spaceChildFilter);
}
return children;
}, [space.roomId, spaceChildFilter]);
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
let warning: JSX.Element;
if (!spaceChildren.length) {
warning = <div className="mx_ConfirmSpaceUserActionDialog_warning">
{ _t("Youre not an admin of anything theyre a member of in <SpaceName/>, " +
"so banning wont remove them from any rooms or spaces in <SpaceName/>.", {}, {
SpaceName: () => <b>{ space.name }</b>,
}) }
</div>;
}
return (
<ConfirmUserActionDialog
{...props}
onFinished={(success: boolean, reason?: string) => {
onFinished(success, reason, roomsToLeave);
}}
className="mx_ConfirmSpaceUserActionDialog"
>
{ warning }
<SpaceChildrenPicker
space={space}
spaceChildren={spaceChildren}
selected={selectedRooms}
allLabel={allLabel}
specificLabel={specificLabel}
noneLabel={noneLabel}
onChange={setRoomsToLeave}
/>
</ConfirmUserActionDialog>
);
};
export default ConfirmSpaceUserActionDialog;

View file

@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { ReactNode } from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -28,9 +30,9 @@ import DialogButtons from "../elements/DialogButtons";
interface IProps {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: RoomMember;
member?: RoomMember;
// group member object. Supply either this or 'member'
groupMember: GroupMemberType;
groupMember?: GroupMemberType;
// needed if a group member is specified
matrixClient?: MatrixClient;
action: string; // eg. 'Ban'
@ -41,6 +43,8 @@ interface IProps {
// be the string entered.
askReason?: boolean;
danger?: boolean;
children?: ReactNode;
className?: string;
onFinished: (success: boolean, reason?: string) => void;
}
@ -105,19 +109,23 @@ export default class ConfirmUserActionDialog extends React.Component<IProps> {
return (
<BaseDialog
className="mx_ConfirmUserActionDialog"
className={classNames("mx_ConfirmUserActionDialog", this.props.className)}
onFinished={this.props.onFinished}
title={this.props.title}
contentId='mx_Dialog_content'
>
<div id="mx_Dialog_content" className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar">
{ avatar }
<div className="mx_ConfirmUserActionDialog_user">
<div className="mx_ConfirmUserActionDialog_avatar">
{ avatar }
</div>
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
</div>
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
{ reasonBox }
{ this.props.children }
</div>
{ reasonBox }
<DialogButtons primaryButton={this.props.action}
onPrimaryButtonClick={this.onOk}
primaryButtonClass={confirmButtonClass}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useMemo, useState } from "react";
import React, { useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
@ -22,108 +22,7 @@ import { _t } from '../../../languageHandler';
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Entry } from "./AddExistingToSpaceDialog";
import SearchBox from "../../structures/SearchBox";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import StyledRadioGroup from "../elements/StyledRadioGroup";
enum RoomsToLeave {
All = "All",
Specific = "Specific",
None = "None",
}
const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim();
const filteredRooms = useMemo(() => {
if (!lcQuery) {
return rooms;
}
const matcher = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
});
return matcher.match(lcQuery);
}, [rooms, lcQuery]);
return <div className="mx_LeaveSpaceDialog_section">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
{ filteredRooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={(checked) => {
onChange(checked, room);
}}
/>;
}) }
{ filteredRooms.length < 1 ? <span className="mx_LeaveSpaceDialog_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => {
const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
const [state, setState] = useState<string>(RoomsToLeave.None);
useEffect(() => {
if (state === RoomsToLeave.All) {
setRoomsToLeave(spaceChildren);
} else {
setRoomsToLeave([]);
}
}, [setRoomsToLeave, state, spaceChildren]);
return <div className="mx_LeaveSpaceDialog_section">
<StyledRadioGroup
name="roomsToLeave"
value={state}
onChange={setState}
definitions={[
{
value: RoomsToLeave.None,
label: _t("Don't leave any"),
}, {
value: RoomsToLeave.All,
label: _t("Leave all rooms and spaces"),
}, {
value: RoomsToLeave.Specific,
label: _t("Leave specific rooms and spaces"),
},
]}
/>
{ state === RoomsToLeave.Specific && (
<SpaceChildPicker
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
rooms={spaceChildren}
selected={selected}
onChange={(selected: boolean, room: Room) => {
if (selected) {
setRoomsToLeave([room, ...roomsToLeave]);
} else {
setRoomsToLeave(roomsToLeave.filter(r => r !== room));
}
}}
/>
) }
</div>;
};
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
interface IProps {
space: Room;
@ -139,6 +38,7 @@ const isOnlyAdmin = (room: Room): boolean => {
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
let rejoinWarning;
if (space.getJoinRule() !== JoinRule.Public) {
@ -173,12 +73,17 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
{ rejoinWarning }
</p>
{ spaceChildren.length > 0 && <LeaveRoomsPicker
space={space}
spaceChildren={spaceChildren}
roomsToLeave={roomsToLeave}
setRoomsToLeave={setRoomsToLeave}
/> }
{ spaceChildren.length > 0 && (
<SpaceChildrenPicker
space={space}
spaceChildren={spaceChildren}
selected={selectedRooms}
onChange={setRoomsToLeave}
noneLabel={_t("Don't leave any")}
allLabel={_t("Leave all rooms and spaces")}
specificLabel={_t("Leave specific rooms and spaces")}
/>
) }
{ onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning">
{ onlyAdminWarning }