From 1858c63c4af98c9f976a818bd3cf20b7a184e3b3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 17 Sep 2021 15:34:49 +0100 Subject: [PATCH] Allow options to cascade kicks/bans throughout spaces --- res/css/_components.scss | 5 +- .../_ConfirmSpaceUserActionDialog.scss | 66 ++++++++ .../dialogs/_ConfirmUserActionDialog.scss | 2 +- res/css/views/dialogs/_LeaveSpaceDialog.scss | 22 +-- .../views/spaces/_SpaceChildrenPicker.scss | 35 ++++ .../dialogs/ConfirmSpaceUserActionDialog.tsx | 86 ++++++++++ .../views/dialogs/ConfirmUserActionDialog.tsx | 26 +-- .../views/dialogs/LeaveSpaceDialog.tsx | 123 ++------------ src/components/views/right_panel/UserInfo.tsx | 100 +++++++++--- .../views/spaces/SpaceChildrenPicker.tsx | 150 ++++++++++++++++++ src/i18n/strings/en_EN.json | 17 +- src/utils/space.tsx | 26 +-- 12 files changed, 481 insertions(+), 177 deletions(-) create mode 100644 res/css/views/dialogs/_ConfirmSpaceUserActionDialog.scss create mode 100644 res/css/views/spaces/_SpaceChildrenPicker.scss create mode 100644 src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx create mode 100644 src/components/views/spaces/SpaceChildrenPicker.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index ffaec43b68..adbe67b20e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -1,8 +1,10 @@ // autogenerated by rethemendex.sh +@import "./_animations.scss"; @import "./_common.scss"; @import "./_font-sizes.scss"; @import "./_font-weights.scss"; @import "./structures/_AutoHideScrollbar.scss"; +@import "./structures/_BackdropPanel.scss"; @import "./structures/_CompatibilityPage.scss"; @import "./structures/_ContextualMenu.scss"; @import "./structures/_CreateRoom.scss"; @@ -17,7 +19,6 @@ @import "./structures/_LeftPanelWidget.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; -@import "./structures/_BackdropPanel.scss"; @import "./structures/_MyGroups.scss"; @import "./structures/_NonUrgentToastContainer.scss"; @import "./structures/_NotificationPanel.scss"; @@ -72,6 +73,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"; @@ -266,6 +268,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"; diff --git a/res/css/views/dialogs/_ConfirmSpaceUserActionDialog.scss b/res/css/views/dialogs/_ConfirmSpaceUserActionDialog.scss new file mode 100644 index 0000000000..3b21ea74de --- /dev/null +++ b/res/css/views/dialogs/_ConfirmSpaceUserActionDialog.scss @@ -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; + } + } +} diff --git a/res/css/views/dialogs/_ConfirmUserActionDialog.scss b/res/css/views/dialogs/_ConfirmUserActionDialog.scss index 5ac0f07b14..777c9fdfd9 100644 --- a/res/css/views/dialogs/_ConfirmUserActionDialog.scss +++ b/res/css/views/dialogs/_ConfirmUserActionDialog.scss @@ -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; } diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.scss b/res/css/views/dialogs/_LeaveSpaceDialog.scss index 0d85a87faf..c0f24871c6 100644 --- a/res/css/views/dialogs/_LeaveSpaceDialog.scss +++ b/res/css/views/dialogs/_LeaveSpaceDialog.scss @@ -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; diff --git a/res/css/views/spaces/_SpaceChildrenPicker.scss b/res/css/views/spaces/_SpaceChildrenPicker.scss new file mode 100644 index 0000000000..e0fa6ef26d --- /dev/null +++ b/res/css/views/spaces/_SpaceChildrenPicker.scss @@ -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; + } +} diff --git a/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx b/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx new file mode 100644 index 0000000000..4059682ade --- /dev/null +++ b/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx @@ -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; +interface IProps extends Omit { + space: Room; + allLabel: string; + specificLabel: string; + noneLabel?: string; + onFinished(success: boolean, reason?: string, rooms?: Room[]): void; + spaceChildFilter?(child: Room): boolean; +} + +const ConfirmSpaceUserActionDialog: React.FC = ({ + 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([]); + const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]); + + let warning: JSX.Element; + if (!spaceChildren.length) { + warning =
+ { _t("You’re not an admin of anything they’re a member of in , " + + "so banning won’t remove them from any rooms or spaces in .", {}, { + SpaceName: () => { space.name }, + }) } +
; + } + + return ( + { + onFinished(success, reason, roomsToLeave); + }} + className="mx_ConfirmSpaceUserActionDialog" + > + { warning } + + + ); +}; + +export default ConfirmSpaceUserActionDialog; diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 7099556ac6..7468f400c6 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -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 { return (
-
- { avatar } +
+
+ { avatar } +
+
{ name }
+
{ userId }
-
{ name }
-
{ userId }
+ + { reasonBox } + { this.props.children }
- { reasonBox } { - const [query, setQuery] = useState(""); - const lcQuery = query.toLowerCase().trim(); - - const filteredRooms = useMemo(() => { - if (!lcQuery) { - return rooms; - } - - const matcher = new QueryMatcher(rooms, { - keys: ["name"], - funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], - shouldMatchWordsOnly: false, - }); - - return matcher.match(lcQuery); - }, [rooms, lcQuery]); - - return
- - - { filteredRooms.map(room => { - return { - onChange(checked, room); - }} - />; - }) } - { filteredRooms.length < 1 ? - { _t("No results") } - : undefined } - -
; -}; - -const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => { - const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]); - const [state, setState] = useState(RoomsToLeave.None); - - useEffect(() => { - if (state === RoomsToLeave.All) { - setRoomsToLeave(spaceChildren); - } else { - setRoomsToLeave([]); - } - }, [setRoomsToLeave, state, spaceChildren]); - - return
- - - { state === RoomsToLeave.Specific && ( - { - if (selected) { - setRoomsToLeave([room, ...roomsToLeave]); - } else { - setRoomsToLeave(roomsToLeave.filter(r => r !== room)); - } - }} - /> - ) } -
; -}; +import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; interface IProps { space: Room; @@ -139,6 +38,7 @@ const isOnlyAdmin = (room: Room): boolean => { const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]); const [roomsToLeave, setRoomsToLeave] = useState([]); + const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]); let rejoinWarning; if (space.getJoinRule() !== JoinRule.Public) { @@ -173,12 +73,17 @@ const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { { rejoinWarning }

- { spaceChildren.length > 0 && } + { spaceChildren.length > 0 && ( + + ) } { onlyAdminWarning &&
{ onlyAdminWarning } diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index f1807985ae..3a12c6478f 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -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"; export interface IDevice { deviceId: string; @@ -530,7 +532,7 @@ interface IBaseProps { stopUpdating(): void; } -const RoomKickButton: React.FC = ({ member, startUpdating, stopUpdating }) => { +const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit) => { const cli = useContext(MatrixClientContext); // check if user can be kicked/disinvited @@ -540,21 +542,35 @@ const RoomKickButton: React.FC = ({ 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?"), 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"), }, + 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! console.log("Kick success"); @@ -654,34 +670,64 @@ const RedactMessagesButton: React.FC = ({ member }) => { ; }; -const BanToggleButton: React.FC = ({ member, startUpdating, stopUpdating }) => { +const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit) => { 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 this user?") : _t("Ban this user?"), + 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"), }, + 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! console.log("Ban success"); @@ -697,12 +743,12 @@ const BanToggleButton: React.FC = ({ 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 @@ -816,7 +862,12 @@ const RoomAdminToolsContainer: React.FC = ({ const canAffectUser = member.powerLevel < me.powerLevel || isMe; if (canAffectUser && me.powerLevel >= kickPowerLevel) { - kickButton = ; + kickButton = ; } if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) { redactButton = ( @@ -824,7 +875,12 @@ const RoomAdminToolsContainer: React.FC = ({ ); } if (canAffectUser && me.powerLevel >= banPowerLevel) { - banButton = ; + banButton = ; } if (canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) { muteButton = ( diff --git a/src/components/views/spaces/SpaceChildrenPicker.tsx b/src/components/views/spaces/SpaceChildrenPicker.tsx new file mode 100644 index 0000000000..7b9e5534ca --- /dev/null +++ b/src/components/views/spaces/SpaceChildrenPicker.tsx @@ -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; + 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(rooms, { + keys: ["name"], + funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], + shouldMatchWordsOnly: false, + }); + + return matcher.match(lcQuery); + }, [rooms, lcQuery]); + + return
+ + + { filteredRooms.map(room => { + return { + onChange(checked, room); + }} + />; + }) } + { filteredRooms.length < 1 ? + { _t("No results") } + : undefined } + +
; +}; + +interface IProps { + space: Room; + spaceChildren: Room[]; + selected: Set; + noneLabel?: string; + allLabel: string; + specificLabel: string; + onChange(rooms: Room[]): void; +} + +const SpaceChildrenPicker = ({ + space, + spaceChildren, + selected, + onChange, + noneLabel, + allLabel, + specificLabel, +}: IProps) => { + const [state, setState] = useState(noneLabel ? Target.None : Target.All); + + useEffect(() => { + if (state === Target.All) { + onChange(spaceChildren); + } else { + onChange([]); + } + }, [onChange, state, spaceChildren]); + + return <> +
+ d.label)} + /> +
+ + { state === Target.Specific && ( + { + if (isSelected) { + onChange([room, ...selected]); + } else { + onChange([...selected].filter(r => r !== room)); + } + }} + /> + ) } + ; +}; + +export default SpaceChildrenPicker; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 28d0b2914b..5195b08f56 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1018,6 +1018,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", @@ -1847,6 +1849,8 @@ "Kick": "Kick", "Disinvite this user?": "Disinvite this user?", "Kick this user?": "Kick this user?", + "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", "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.", @@ -1860,6 +1864,10 @@ "Ban": "Ban", "Unban this user?": "Unban this user?", "Ban this user?": "Ban this user?", + "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", "Failed to ban user": "Failed to ban user", "Failed to mute user": "Failed to mute user", "Unmute": "Unmute", @@ -2050,7 +2058,6 @@ "Application window": "Application window", "Share content": "Share content", "Join": "Join", - "No results": "No results", "Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.", "collapse": "collapse", "expand": "expand", @@ -2217,6 +2224,7 @@ "Confirm Removal": "Confirm Removal", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.", "Reason (optional)": "Reason (optional)", + "You’re not an admin of anything they’re a member of in , so banning won’t remove them from any rooms or spaces in .": "You’re not an admin of anything they’re a member of in , so banning won’t remove them from any rooms or spaces in .", "Clear all data in this session?": "Clear all data in this session?", "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.", "Clear all data": "Clear all data", @@ -2430,15 +2438,14 @@ "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": "Don't leave any", - "Leave all rooms and spaces": "Leave all rooms and spaces", - "Leave specific rooms and spaces": "Leave specific rooms and spaces", - "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", "Are you sure you want to leave ?": "Are you sure you want to leave ?", + "Don't leave any": "Don't leave any", + "Leave all rooms and spaces": "Leave all rooms and spaces", + "Leave specific rooms and spaces": "Leave specific rooms and spaces", "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", diff --git a/src/utils/space.tsx b/src/utils/space.tsx index 5bbae369e7..aba9883ae7 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -155,20 +155,28 @@ export const showCreateNewSubspace = (space: Room): void => { ); }; +export const bulkSpaceBehaviour = async ( + space: Room, + children: Room[], + fn: (room: Room) => Promise, +): Promise => { + 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",