mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge pull request #6424 from matrix-org/t3chguy/fix/18071
						commit
						94af6db201
					
				|  | @ -88,6 +88,7 @@ | |||
| @import "./views/dialogs/_InviteDialog.scss"; | ||||
| @import "./views/dialogs/_JoinRuleDropdown.scss"; | ||||
| @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; | ||||
| @import "./views/dialogs/_LeaveSpaceDialog.scss"; | ||||
| @import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; | ||||
| @import "./views/dialogs/_MessageEditHistoryDialog.scss"; | ||||
| @import "./views/dialogs/_ModalWidgetDialog.scss"; | ||||
|  |  | |||
|  | @ -50,35 +50,6 @@ limitations under the License. | |||
|             line-height: $font-15px; | ||||
|         } | ||||
| 
 | ||||
|         .mx_AddExistingToSpace_entry { | ||||
|             display: flex; | ||||
|             margin-top: 12px; | ||||
| 
 | ||||
|             .mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling | ||||
|             .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom { | ||||
|                 margin-right: 12px; | ||||
|             } | ||||
| 
 | ||||
|             img.mx_RoomAvatar_isSpaceRoom, | ||||
|             .mx_RoomAvatar_isSpaceRoom img { | ||||
|                 border-radius: 8px; | ||||
|             } | ||||
| 
 | ||||
|             .mx_AddExistingToSpace_entry_name { | ||||
|                 font-size: $font-15px; | ||||
|                 line-height: 30px; | ||||
|                 flex-grow: 1; | ||||
|                 overflow: hidden; | ||||
|                 white-space: nowrap; | ||||
|                 text-overflow: ellipsis; | ||||
|                 margin-right: 12px; | ||||
|             } | ||||
| 
 | ||||
|             .mx_Checkbox { | ||||
|                 align-items: center; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .mx_AccessibleButton_kind_link { | ||||
|             font-size: $font-12px; | ||||
|             line-height: $font-15px; | ||||
|  | @ -255,3 +226,32 @@ limitations under the License. | |||
|         line-height: $font-24px; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_AddExistingToSpace_entry { | ||||
|     display: flex; | ||||
|     margin-top: 12px; | ||||
| 
 | ||||
|     .mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling | ||||
|     .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom { | ||||
|         margin-right: 12px; | ||||
|     } | ||||
| 
 | ||||
|     img.mx_RoomAvatar_isSpaceRoom, | ||||
|     .mx_RoomAvatar_isSpaceRoom img { | ||||
|         border-radius: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_AddExistingToSpace_entry_name { | ||||
|         font-size: $font-15px; | ||||
|         line-height: 30px; | ||||
|         flex-grow: 1; | ||||
|         overflow: hidden; | ||||
|         white-space: nowrap; | ||||
|         text-overflow: ellipsis; | ||||
|         margin-right: 12px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_Checkbox { | ||||
|         align-items: center; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,96 @@ | |||
| /* | ||||
| 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_LeaveSpaceDialog_wrapper { | ||||
|     .mx_Dialog { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         padding: 24px 32px; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_LeaveSpaceDialog { | ||||
|     width: 440px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex-wrap: nowrap; | ||||
|     max-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; | ||||
|             margin: 12px 0 0; | ||||
|             padding: 12px 8px 12px 42px; | ||||
|             background-color: $header-panel-bg-color; | ||||
| 
 | ||||
|             font-size: $font-12px; | ||||
|             line-height: $font-15px; | ||||
|             color: $secondary-fg-color; | ||||
| 
 | ||||
|             &::before { | ||||
|                 content: ''; | ||||
|                 position: absolute; | ||||
|                 left: 10px; | ||||
|                 top: calc(50% - 8px); // vertical centering | ||||
|                 height: 16px; | ||||
|                 width: 16px; | ||||
|                 background-color: $secondary-fg-color; | ||||
|                 mask-repeat: no-repeat; | ||||
|                 mask-size: contain; | ||||
|                 mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); | ||||
|                 mask-position: center; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         > p { | ||||
|             color: $primary-fg-color; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_Dialog_buttons { | ||||
|         margin-top: 20px; | ||||
| 
 | ||||
|         .mx_Dialog_primary { | ||||
|             background-color: $notice-primary-color !important; // override default colour | ||||
|             border-color: $notice-primary-color; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,197 @@ | |||
| /* | ||||
| 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 { JoinRule } from "matrix-js-sdk/src/@types/partials"; | ||||
| 
 | ||||
| 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} | ||||
|             autoComplete={true} | ||||
|             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.All); | ||||
| 
 | ||||
|     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.All, | ||||
|                     label: _t("Leave all rooms and spaces"), | ||||
|                 }, { | ||||
|                     value: RoomsToLeave.None, | ||||
|                     label: _t("Don't leave any"), | ||||
|                 }, { | ||||
|                     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>; | ||||
| }; | ||||
| 
 | ||||
| interface IProps { | ||||
|     space: Room; | ||||
|     onFinished(leave: boolean, rooms?: Room[]): void; | ||||
| } | ||||
| 
 | ||||
| const isOnlyAdmin = (room: Room): boolean => { | ||||
|     return !room.getJoinedMembers().some(member => { | ||||
|         return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100; | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => { | ||||
|     const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]); | ||||
|     const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]); | ||||
| 
 | ||||
|     let rejoinWarning; | ||||
|     if (space.getJoinRule() !== JoinRule.Public) { | ||||
|         rejoinWarning = _t("You won't be able to rejoin unless you are re-invited."); | ||||
|     } | ||||
| 
 | ||||
|     let onlyAdminWarning; | ||||
|     if (isOnlyAdmin(space)) { | ||||
|         onlyAdminWarning = _t("You're the only admin of this space. " + | ||||
|             "Leaving it will mean no one has control over it."); | ||||
|     } else { | ||||
|         const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length; | ||||
|         if (numChildrenOnlyAdminIn > 0) { | ||||
|             onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " + | ||||
|                 "Leaving them will leave them without any admins."); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return <BaseDialog | ||||
|         title={_t("Leave %(spaceName)s", { spaceName: space.name })} | ||||
|         className="mx_LeaveSpaceDialog" | ||||
|         contentId="mx_LeaveSpaceDialog" | ||||
|         onFinished={() => onFinished(false)} | ||||
|         fixedWidth={false} | ||||
|     > | ||||
|         <div className="mx_Dialog_content" id="mx_LeaveSpaceDialog"> | ||||
|             <p> | ||||
|                 { _t("Are you sure you want to leave <spaceName/>?", {}, { | ||||
|                     spaceName: () => <b>{ space.name }</b>, | ||||
|                 }) } | ||||
|                   | ||||
|                 { rejoinWarning } | ||||
|             </p> | ||||
| 
 | ||||
|             { spaceChildren.length > 0 && <LeaveRoomsPicker | ||||
|                 space={space} | ||||
|                 spaceChildren={spaceChildren} | ||||
|                 roomsToLeave={roomsToLeave} | ||||
|                 setRoomsToLeave={setRoomsToLeave} | ||||
|             /> } | ||||
| 
 | ||||
|             { onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning"> | ||||
|                 { onlyAdminWarning } | ||||
|             </div> } | ||||
|         </div> | ||||
|         <DialogButtons | ||||
|             primaryButton={_t("Leave space")} | ||||
|             onPrimaryButtonClick={() => onFinished(true, roomsToLeave)} | ||||
|             hasCancel={true} | ||||
|             onCancel={onFinished} | ||||
|         /> | ||||
|     </BaseDialog>; | ||||
| }; | ||||
| 
 | ||||
| export default LeaveSpaceDialog; | ||||
|  | @ -25,7 +25,7 @@ import SpaceBasicSettings from "./SpaceBasicSettings"; | |||
| import { avatarUrlForRoom } from "../../../Avatar"; | ||||
| import { IDialogProps } from "../dialogs/IDialogProps"; | ||||
| import { getTopic } from "../elements/RoomTopic"; | ||||
| import { defaultDispatcher } from "../../../dispatcher/dispatcher"; | ||||
| import { leaveSpace } from "../../../utils/space"; | ||||
| 
 | ||||
| interface IProps extends IDialogProps { | ||||
|     matrixClient: MatrixClient; | ||||
|  | @ -125,10 +125,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp | |||
|             <AccessibleButton | ||||
|                 kind="danger" | ||||
|                 onClick={() => { | ||||
|                     defaultDispatcher.dispatch({ | ||||
|                         action: "leave_room", | ||||
|                         room_id: space.roomId, | ||||
|                     }); | ||||
|                     leaveSpace(space); | ||||
|                 }} | ||||
|             > | ||||
|                 { _t("Leave Space") } | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ import { _t } from "../../../languageHandler"; | |||
| import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; | ||||
| import { toRightOf } from "../../structures/ContextMenu"; | ||||
| import { | ||||
|     leaveSpace, | ||||
|     shouldShowSpaceSettings, | ||||
|     showAddExistingRooms, | ||||
|     showCreateNewRoom, | ||||
|  | @ -213,10 +214,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> { | |||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
| 
 | ||||
|         defaultDispatcher.dispatch({ | ||||
|             action: "leave_room", | ||||
|             room_id: this.props.space.roomId, | ||||
|         }); | ||||
|         leaveSpace(this.props.space); | ||||
|         this.setState({ contextMenuPosition: null }); // also close the menu
 | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -2376,6 +2376,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", | ||||
|     "Leave all rooms and spaces": "Leave all rooms and spaces", | ||||
|     "Don't leave any": "Don't leave any", | ||||
|     "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/>?", | ||||
|     "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", | ||||
|     "I don't want my encrypted messages": "I don't want my encrypted messages", | ||||
|  |  | |||
|  | @ -329,7 +329,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|         }, roomId); | ||||
|     } | ||||
| 
 | ||||
|     private getChildren(spaceId: string): Room[] { | ||||
|     public getChildren(spaceId: string): Room[] { | ||||
|         const room = this.matrixClient?.getRoom(spaceId); | ||||
|         const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via); | ||||
|         return sortBy(childEvents, ev => { | ||||
|  |  | |||
|  | @ -33,6 +33,10 @@ import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSu | |||
| import defaultDispatcher from "../dispatcher/dispatcher"; | ||||
| import RoomViewStore from "../stores/RoomViewStore"; | ||||
| import { Action } from "../dispatcher/actions"; | ||||
| import { leaveRoomBehaviour } from "./membership"; | ||||
| import Spinner from "../components/views/elements/Spinner"; | ||||
| import dis from "../dispatcher/dispatcher"; | ||||
| import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog"; | ||||
| 
 | ||||
| export const shouldShowSpaceSettings = (space: Room) => { | ||||
|     const userId = space.client.getUserId(); | ||||
|  | @ -148,3 +152,24 @@ export const showCreateNewSubspace = (space: Room): void => { | |||
|         "mx_CreateSubspaceDialog_wrapper", | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| 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 { | ||||
|                 await Promise.all(rooms.map(r => leaveRoomBehaviour(r.roomId))); | ||||
|                 await leaveRoomBehaviour(space.roomId); | ||||
|             } finally { | ||||
|                 modal.close(); | ||||
|             } | ||||
| 
 | ||||
|             dis.dispatch({ | ||||
|                 action: "after_leave_room", | ||||
|                 room_id: space.roomId, | ||||
|             }); | ||||
|         }, | ||||
|     }, "mx_LeaveSpaceDialog_wrapper"); | ||||
| }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski