mirror of https://github.com/vector-im/riot-web
				
				
				
			Allow options to cascade kicks/bans throughout spaces
							parent
							
								
									cf8100ad17
								
							
						
					
					
						commit
						1858c63c4a
					
				|  | @ -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"; | ||||
|  |  | |||
|  | @ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
|  | @ -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("You’re not an admin of anything they’re a member of in <SpaceName/>, " + | ||||
|                 "so banning won’t 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; | ||||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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 } | ||||
|  |  | |||
|  | @ -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<IBaseProps> = ({ member, startUpdating, stopUpdating }) => { | ||||
| const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => { | ||||
|     const cli = useContext(MatrixClientContext); | ||||
| 
 | ||||
|     // check if user can be kicked/disinvited
 | ||||
|  | @ -540,21 +542,35 @@ 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?"), | ||||
|                 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<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 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<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}> | ||||
|  | @ -816,7 +862,12 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({ | |||
|     const canAffectUser = member.powerLevel < me.powerLevel || isMe; | ||||
| 
 | ||||
|     if (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 = ( | ||||
|  | @ -824,7 +875,12 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({ | |||
|         ); | ||||
|     } | ||||
|     if (canAffectUser && me.powerLevel >= banPowerLevel) { | ||||
|         banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />; | ||||
|         banButton = <BanToggleButton | ||||
|             room={room} | ||||
|             member={member} | ||||
|             startUpdating={startUpdating} | ||||
|             stopUpdating={stopUpdating} | ||||
|         />; | ||||
|     } | ||||
|     if (canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) { | ||||
|         muteButton = ( | ||||
|  |  | |||
|  | @ -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; | ||||
|  | @ -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 <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", | ||||
|  | @ -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 <SpaceName/>, so banning won’t remove them from any rooms or spaces in <SpaceName/>.": "You’re not an admin of anything they’re a member of in <SpaceName/>, so banning won’t remove them from any rooms or spaces in <SpaceName/>.", | ||||
|     "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 <spaceName/>?": "Are you sure you want to leave <spaceName/>?", | ||||
|     "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", | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski