Merge pull request from matrix-org/t3chguy/fix/18969

This commit is contained in:
Michael Telatynski 2021-10-14 09:59:34 +01:00 committed by GitHub
commit 4118d13846
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 526 additions and 209 deletions

View file

@ -74,6 +74,7 @@
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.scss";
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
@import "./views/dialogs/_CreateGroupDialog.scss";
@ -270,6 +271,7 @@
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
@import "./views/spaces/_SpaceBasicSettings.scss";
@import "./views/spaces/_SpaceChildrenPicker.scss";
@import "./views/spaces/_SpaceCreateMenu.scss";
@import "./views/spaces/_SpacePublicShare.scss";
@import "./views/terms/_InlineTermsAgreement.scss";

View file

@ -0,0 +1,66 @@
/*
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.
*/
.mx_ConfirmSpaceUserActionDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
padding: 24px 32px;
}
}
.mx_ConfirmSpaceUserActionDialog {
width: 440px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
height: 520px;
.mx_Dialog_content {
margin: 12px 0;
flex-grow: 1;
overflow-y: auto;
}
.mx_ConfirmUserActionDialog_reasonField {
margin-bottom: 12px;
}
.mx_ConfirmSpaceUserActionDialog_warning {
position: relative;
border-radius: 8px;
padding: 12px 8px 12px 42px;
background-color: $header-panel-bg-color;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-content;
&::before {
content: '';
position: absolute;
left: 10px;
top: calc(50% - 8px); // vertical centering
height: 16px;
width: 16px;
background-color: $secondary-content;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ConfirmUserActionDialog .mx_Dialog_content {
.mx_ConfirmUserActionDialog .mx_Dialog_content .mx_ConfirmUserActionDialog_user {
min-height: 48px;
margin-bottom: 24px;
}
@ -22,10 +22,10 @@ limitations under the License.
.mx_ConfirmUserActionDialog_avatar {
float: left;
margin-right: 20px;
margin-top: -2px;
}
.mx_ConfirmUserActionDialog_name {
padding-top: 2px;
font-size: $font-18px;
}
@ -37,16 +37,4 @@ limitations under the License.
font-size: $font-14px;
color: $primary-content;
background-color: $background;
border-radius: 3px;
border: solid 1px $input-border-color;
line-height: $font-36px;
padding-left: 16px;
padding-right: 16px;
padding-top: 1px;
padding-bottom: 1px;
margin-bottom: 24px;
width: 90%;
}

View file

@ -27,33 +27,13 @@ limitations under the License.
display: flex;
flex-direction: column;
flex-wrap: nowrap;
max-height: 520px;
height: 520px;
.mx_Dialog_content {
flex-grow: 1;
margin: 0;
overflow-y: auto;
.mx_RadioButton + .mx_RadioButton {
margin-top: 16px;
}
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
border-radius: 8px;
}
.mx_LeaveSpaceDialog_noResults {
display: block;
margin-top: 24px;
}
.mx_LeaveSpaceDialog_section {
margin: 16px 0;
}
.mx_LeaveSpaceDialog_section_warning {
position: relative;
border-radius: 8px;

View file

@ -0,0 +1,35 @@
/*
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.
*/
.mx_SpaceChildrenPicker {
margin: 16px 0;
.mx_RadioButton + .mx_RadioButton {
margin-top: 16px;
}
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
border-radius: 8px;
}
.mx_SpaceChildrenPicker_noResults {
display: block;
margin-top: 24px;
}
}

View file

@ -0,0 +1,85 @@
/*
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";
type BaseProps = ComponentProps<typeof ConfirmUserActionDialog>;
interface IProps extends Omit<BaseProps, "groupMember" | "matrixClient" | "children" | "onFinished"> {
space: Room;
allLabel: string;
specificLabel: string;
noneLabel?: string;
warningMessage?: string;
onFinished(success: boolean, reason?: string, rooms?: Room[]): void;
spaceChildFilter?(child: Room): boolean;
}
const ConfirmSpaceUserActionDialog: React.FC<IProps> = ({
space,
spaceChildFilter,
allLabel,
specificLabel,
noneLabel,
warningMessage,
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 (warningMessage) {
warning = <div className="mx_ConfirmSpaceUserActionDialog_warning">
{ warningMessage }
</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, { ChangeEvent, 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";
@ -25,12 +27,13 @@ import MemberAvatar from '../avatars/MemberAvatar';
import BaseAvatar from '../avatars/BaseAvatar';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import Field from '../elements/Field';
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,9 +44,15 @@ interface IProps {
// be the string entered.
askReason?: boolean;
danger?: boolean;
children?: ReactNode;
className?: string;
onFinished: (success: boolean, reason?: string) => void;
}
interface IState {
reason: string;
}
/*
* A dialog for confirming an operation on another user.
* Takes a user ID and a verb, displays the target user prominently
@ -53,37 +62,50 @@ interface IProps {
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
export default class ConfirmUserActionDialog extends React.Component<IProps> {
private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
export default class ConfirmUserActionDialog extends React.Component<IProps, IState> {
static defaultProps = {
danger: false,
askReason: false,
};
public onOk = (): void => {
this.props.onFinished(true, this.reasonField.current?.value);
constructor(props: IProps) {
super(props);
this.state = {
reason: "",
};
}
private onOk = (): void => {
this.props.onFinished(true, this.state.reason);
};
public onCancel = (): void => {
private onCancel = (): void => {
this.props.onFinished(false);
};
private onReasonChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({
reason: ev.target.value,
});
};
public render() {
const confirmButtonClass = this.props.danger ? 'danger' : '';
let reasonBox;
if (this.props.askReason) {
reasonBox = (
<div>
<form onSubmit={this.onOk}>
<input className="mx_ConfirmUserActionDialog_reasonField"
ref={this.reasonField}
placeholder={_t("Reason")}
autoFocus={true}
/>
</form>
</div>
<form onSubmit={this.onOk}>
<Field
type="text"
onChange={this.onReasonChange}
value={this.state.reason}
className="mx_ConfirmUserActionDialog_reasonField"
label={_t("Reason")}
autoFocus={true}
/>
</form>
);
}
@ -105,19 +127,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 rooms"),
}, {
value: RoomsToLeave.All,
label: _t("Leave all rooms"),
}, {
value: RoomsToLeave.Specific,
label: _t("Leave some rooms"),
},
]}
/>
{ 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;
@ -144,6 +43,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) {
@ -180,12 +80,17 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
</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 rooms")}
allLabel={_t("Leave all rooms")}
specificLabel={_t("Leave some rooms")}
/>
) }
{ onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning">
{ onlyAdminWarning }

View file

@ -70,6 +70,8 @@ import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import SpaceStore from "../../../stores/SpaceStore";
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
import { bulkSpaceBehaviour } from "../../../utils/space";
import { logger } from "matrix-js-sdk/src/logger";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
@ -534,7 +536,7 @@ interface IBaseProps {
stopUpdating(): void;
}
const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdating }) => {
const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
const cli = useContext(MatrixClientContext);
// check if user can be kicked/disinvited
@ -544,21 +546,38 @@ const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdat
const { finished } = Modal.createTrackedDialog(
'Confirm User Action Dialog',
'onKick',
ConfirmUserActionDialog,
room.isSpaceRoom() ? ConfirmSpaceUserActionDialog : ConfirmUserActionDialog,
{
member,
action: member.membership === "invite" ? _t("Disinvite") : _t("Kick"),
title: member.membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"),
title: member.membership === "invite"
? _t("Disinvite from %(roomName)s", { roomName: room.name })
: _t("Kick from %(roomName)s", { roomName: room.name }),
askReason: member.membership === "join",
danger: true,
// space-specific props
space: room,
spaceChildFilter: (child: Room) => {
// Return true if the target member is not banned and we have sufficient PL to ban them
const myMember = child.getMember(cli.credentials.userId);
const theirMember = child.getMember(member.userId);
return myMember && theirMember && theirMember.membership === member.membership &&
myMember.powerLevel > theirMember.powerLevel &&
child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel);
},
allLabel: _t("Kick them from everything I'm able to"),
specificLabel: _t("Kick them from specific things I'm able to"),
warningMessage: _t("They'll still be able to access whatever you're not an admin of."),
},
room.isSpaceRoom() ? "mx_ConfirmSpaceUserActionDialog_wrapper" : undefined,
);
const [proceed, reason] = await finished;
const [proceed, reason, rooms = []] = await finished;
if (!proceed) return;
startUpdating();
cli.kick(member.roomId, member.userId, reason || undefined).then(() => {
bulkSpaceBehaviour(room, rooms, room => cli.kick(room.roomId, member.userId, reason || undefined)).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
logger.log("Kick success");
@ -658,34 +677,69 @@ const RedactMessagesButton: React.FC<IBaseProps> = ({ member }) => {
</AccessibleButton>;
};
const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdating }) => {
const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
const cli = useContext(MatrixClientContext);
const isBanned = member.membership === "ban";
const onBanOrUnban = async () => {
const { finished } = Modal.createTrackedDialog(
'Confirm User Action Dialog',
'onBanOrUnban',
ConfirmUserActionDialog,
room.isSpaceRoom() ? ConfirmSpaceUserActionDialog : ConfirmUserActionDialog,
{
member,
action: member.membership === 'ban' ? _t("Unban") : _t("Ban"),
title: member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"),
askReason: member.membership !== 'ban',
danger: member.membership !== 'ban',
action: isBanned ? _t("Unban") : _t("Ban"),
title: isBanned
? _t("Unban from %(roomName)s", { roomName: room.name })
: _t("Ban from %(roomName)s", { roomName: room.name }),
askReason: !isBanned,
danger: !isBanned,
// space-specific props
space: room,
spaceChildFilter: isBanned
? (child: Room) => {
// Return true if the target member is banned and we have sufficient PL to unban
const myMember = child.getMember(cli.credentials.userId);
const theirMember = child.getMember(member.userId);
return myMember && theirMember && theirMember.membership === "ban" &&
myMember.powerLevel > theirMember.powerLevel &&
child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel);
}
: (child: Room) => {
// Return true if the target member isn't banned and we have sufficient PL to ban
const myMember = child.getMember(cli.credentials.userId);
const theirMember = child.getMember(member.userId);
return myMember && theirMember && theirMember.membership !== "ban" &&
myMember.powerLevel > theirMember.powerLevel &&
child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel);
},
allLabel: isBanned
? _t("Unban them from everything I'm able to")
: _t("Ban them from everything I'm able to"),
specificLabel: isBanned
? _t("Unban them from specific things I'm able to")
: _t("Ban them from specific things I'm able to"),
warningMessage: isBanned
? _t("They won't be able to access whatever you're not an admin of.")
: _t("They'll still be able to access whatever you're not an admin of."),
},
room.isSpaceRoom() ? "mx_ConfirmSpaceUserActionDialog_wrapper" : undefined,
);
const [proceed, reason] = await finished;
const [proceed, reason, rooms = []] = await finished;
if (!proceed) return;
startUpdating();
let promise;
if (member.membership === 'ban') {
promise = cli.unban(member.roomId, member.userId);
} else {
promise = cli.ban(member.roomId, member.userId, reason || undefined);
}
promise.then(() => {
const fn = (roomId: string) => {
if (isBanned) {
return cli.unban(roomId, member.userId);
} else {
return cli.ban(roomId, member.userId, reason || undefined);
}
};
bulkSpaceBehaviour(room, rooms, room => fn(room.roomId)).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
logger.log("Ban success");
@ -701,12 +755,12 @@ const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpda
};
let label = _t("Ban");
if (member.membership === 'ban') {
if (isBanned) {
label = _t("Unban");
}
const classes = classNames("mx_UserInfo_field", {
mx_UserInfo_destructive: member.membership !== 'ban',
mx_UserInfo_destructive: !isBanned,
});
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
@ -820,7 +874,12 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
if (!isMe && canAffectUser && me.powerLevel >= kickPowerLevel) {
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
kickButton = <RoomKickButton
room={room}
member={member}
startUpdating={startUpdating}
stopUpdating={stopUpdating}
/>;
}
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
redactButton = (
@ -828,7 +887,12 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
);
}
if (!isMe && canAffectUser && me.powerLevel >= banPowerLevel) {
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
banButton = <BanToggleButton
room={room}
member={member}
startUpdating={startUpdating}
stopUpdating={stopUpdating}
/>;
}
if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
muteButton = (

View file

@ -0,0 +1,150 @@
/*
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, { useEffect, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from "../../../languageHandler";
import StyledRadioGroup from "../elements/StyledRadioGroup";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import SearchBox from "../../structures/SearchBox";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Entry } from "../dialogs/AddExistingToSpaceDialog";
enum Target {
All = "All",
Specific = "Specific",
None = "None",
}
interface ISpecificChildrenPickerProps {
filterPlaceholder: string;
rooms: Room[];
selected: Set<Room>;
onChange(selected: boolean, room: Room): void;
}
const SpecificChildrenPicker = ({ filterPlaceholder, rooms, selected, onChange }: ISpecificChildrenPickerProps) => {
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_SpaceChildrenPicker">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoFocus={true}
/>
<AutoHideScrollbar>
{ 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_SpaceChildrenPicker_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
interface IProps {
space: Room;
spaceChildren: Room[];
selected: Set<Room>;
noneLabel?: string;
allLabel: string;
specificLabel: string;
onChange(rooms: Room[]): void;
}
const SpaceChildrenPicker = ({
space,
spaceChildren,
selected,
onChange,
noneLabel,
allLabel,
specificLabel,
}: IProps) => {
const [state, setState] = useState<string>(noneLabel ? Target.None : Target.All);
useEffect(() => {
if (state === Target.All) {
onChange(spaceChildren);
} else {
onChange([]);
}
}, [onChange, state, spaceChildren]);
return <>
<div className="mx_SpaceChildrenPicker">
<StyledRadioGroup
name="roomsToLeave"
value={state}
onChange={setState}
definitions={[
{
value: Target.None,
label: noneLabel,
}, {
value: Target.All,
label: allLabel,
}, {
value: Target.Specific,
label: specificLabel,
},
].filter(d => d.label)}
/>
</div>
{ state === Target.Specific && (
<SpecificChildrenPicker
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
rooms={spaceChildren}
selected={selected}
onChange={(isSelected: boolean, room: Room) => {
if (isSelected) {
onChange([room, ...selected]);
} else {
onChange([...selected].filter(r => r !== room));
}
}}
/>
) }
</>;
};
export default SpaceChildrenPicker;

View file

@ -1028,6 +1028,8 @@
"Upload": "Upload",
"Name": "Name",
"Description": "Description",
"No results": "No results",
"Search %(spaceName)s": "Search %(spaceName)s",
"Please enter a name for the space": "Please enter a name for the space",
"Spaces are a new feature.": "Spaces are a new feature.",
"Spaces feedback": "Spaces feedback",
@ -1864,8 +1866,11 @@
"Demote": "Demote",
"Disinvite": "Disinvite",
"Kick": "Kick",
"Disinvite this user?": "Disinvite this user?",
"Kick this user?": "Kick this user?",
"Disinvite from %(roomName)s": "Disinvite from %(roomName)s",
"Kick from %(roomName)s": "Kick from %(roomName)s",
"Kick them from everything I'm able to": "Kick them from everything I'm able to",
"Kick them from specific things I'm able to": "Kick them from specific things I'm able to",
"They'll still be able to access whatever you're not an admin of.": "They'll still be able to access whatever you're not an admin of.",
"Failed to kick": "Failed to kick",
"No recent messages by %(user)s found": "No recent messages by %(user)s found",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.",
@ -1877,8 +1882,13 @@
"Remove %(count)s messages|one": "Remove 1 message",
"Remove recent messages": "Remove recent messages",
"Ban": "Ban",
"Unban this user?": "Unban this user?",
"Ban this user?": "Ban this user?",
"Unban from %(roomName)s": "Unban from %(roomName)s",
"Ban from %(roomName)s": "Ban from %(roomName)s",
"Unban them from everything I'm able to": "Unban them from everything I'm able to",
"Ban them from everything I'm able to": "Ban them from everything I'm able to",
"Unban them from specific things I'm able to": "Unban them from specific things I'm able to",
"Ban them from specific things I'm able to": "Ban them from specific things I'm able to",
"They won't be able to access whatever you're not an admin of.": "They won't be able to access whatever you're not an admin of.",
"Failed to ban user": "Failed to ban user",
"Failed to mute user": "Failed to mute user",
"Unmute": "Unmute",
@ -2069,7 +2079,6 @@
"Application window": "Application window",
"Share content": "Share content",
"Join": "Join",
"No results": "No results",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"collapse": "collapse",
"expand": "expand",
@ -2470,16 +2479,15 @@
"Clear cache and resync": "Clear cache and resync",
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating %(brand)s": "Updating %(brand)s",
"Don't leave any rooms": "Don't leave any rooms",
"Leave all rooms": "Leave all rooms",
"Leave some rooms": "Leave some rooms",
"Search %(spaceName)s": "Search %(spaceName)s",
"You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
"You're the only admin of this space. Leaving it will mean no one has control over it.": "You're the only admin of this space. Leaving it will mean no one has control over it.",
"You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.",
"Leave %(spaceName)s": "Leave %(spaceName)s",
"You are about to leave <spaceName/>.": "You are about to leave <spaceName/>.",
"Would you like to leave the rooms in this space?": "Would you like to leave the rooms in this space?",
"Don't leave any rooms": "Don't leave any rooms",
"Leave all rooms": "Leave all rooms",
"Leave some rooms": "Leave some rooms",
"Leave space": "Leave space",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Start using Key Backup": "Start using Key Backup",

View file

@ -155,20 +155,28 @@ export const showCreateNewSubspace = (space: Room): void => {
);
};
export const bulkSpaceBehaviour = async (
space: Room,
children: Room[],
fn: (room: Room) => Promise<unknown>,
): Promise<void> => {
const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
try {
for (const room of children) {
await fn(room);
}
await fn(space);
} finally {
modal.close();
}
};
export const leaveSpace = (space: Room) => {
Modal.createTrackedDialog("Leave Space", "", LeaveSpaceDialog, {
space,
onFinished: async (leave: boolean, rooms: Room[]) => {
if (!leave) return;
const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
try {
for (const room of rooms) {
await leaveRoomBehaviour(room.roomId);
}
await leaveRoomBehaviour(space.roomId);
} finally {
modal.close();
}
await bulkSpaceBehaviour(space, rooms, room => leaveRoomBehaviour(room.roomId));
dis.dispatch({
action: "after_leave_room",