From 716268b2f953e54345389d08c2a95825e7590a7a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 2 Mar 2021 14:34:47 +0000 Subject: [PATCH] Add context menu to spaces in the space panel --- res/css/structures/_SpacePanel.scss | 75 ++++++ src/accessibility/context_menu/MenuItem.tsx | 11 +- src/components/views/dialogs/InfoDialog.js | 6 +- .../views/spaces/SpaceTreeLevel.tsx | 216 ++++++++++++++++++ src/i18n/strings/en_EN.json | 16 +- 5 files changed, 314 insertions(+), 10 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 24d2243912..9937117086 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -212,6 +212,30 @@ $activeBorderColor: $secondary-fg-color; border-radius: 8px; } } + + .mx_SpaceButton_menuButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin-top: auto; + margin-bottom: auto; + position: relative; + display: none; + + &::before { + top: 2px; + left: 2px; + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/context-menu.svg'); + background: $primary-fg-color; + } + } } .mx_SpacePanel_badgeContainer { @@ -254,6 +278,10 @@ $activeBorderColor: $secondary-fg-color; height: 0; display: none; } + + .mx_SpaceButton_menuButton { + display: block; + } } } @@ -272,3 +300,50 @@ $activeBorderColor: $secondary-fg-color; } } } + +.mx_SpacePanel_contextMenu { + .mx_SpacePanel_contextMenu_header { + margin: 12px 16px 12px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + } + + .mx_IconizedContextMenu_optionList .mx_AccessibleButton.mx_SpacePanel_contextMenu_inviteButton { + color: $accent-color; + + .mx_SpacePanel_iconInvite::before { + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + .mx_SpacePanel_iconSettings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_SpacePanel_iconLeave::before { + mask-image: url('$(res)/img/element-icons/leave.svg'); + } + + .mx_SpacePanel_iconHome::before { + mask-image: url('$(res)/img/element-icons/roomlist/home.svg'); + } + + .mx_SpacePanel_iconMembers::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SpacePanel_iconPlus::before { + mask-image: url('$(res)/img/element-icons/plus.svg'); + } + + .mx_SpacePanel_iconExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + } +} + + +.mx_SpacePanel_sharePublicSpace { + margin: 0; +} diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 0bb169abf8..9a7c1d1f0a 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -19,14 +19,23 @@ limitations under the License. import React from "react"; import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; interface IProps extends React.ComponentProps { label?: string; + tooltip?: string; } // Semantic component for representing a role=menuitem -export const MenuItem: React.FC = ({children, label, ...props}) => { +export const MenuItem: React.FC = ({children, label, tooltip, ...props}) => { const ariaLabel = props["aria-label"] || label; + + if (tooltip) { + return + { children } + ; + } + return ( { children } diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index 97ae968ff3..6dc9fc01b0 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component { className: PropTypes.string, title: PropTypes.string, description: PropTypes.node, - button: PropTypes.string, + button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool), onFinished: PropTypes.func, hasCloseButton: PropTypes.bool, onKeyDown: PropTypes.func, @@ -60,11 +60,11 @@ export default class InfoDialog extends React.Component {
{ this.props.description }
- - + } ); } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index f94798433f..04d6c02208 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -23,7 +23,27 @@ import SpaceStore from "../../../stores/SpaceStore"; import NotificationBadge from "../rooms/NotificationBadge"; import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton"; import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../context_menus/IconizedContextMenu"; +import {_t} from "../../../languageHandler"; +import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import {toRightOf} from "../../structures/ContextMenu"; +import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {ButtonEvent} from "../elements/AccessibleButton"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import Modal from "../../../Modal"; +import SpacePublicShare from "./SpacePublicShare"; +import {Action} from "../../../dispatcher/actions"; +import RoomViewStore from "../../../stores/RoomViewStore"; +import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import {showRoomInviteDialog} from "../../../RoomInvite"; +import InfoDialog from "../dialogs/InfoDialog"; +import {EventType} from "matrix-js-sdk/src/@types/event"; +import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory"; interface IItemProps { space?: Room; @@ -78,6 +98,200 @@ export class SpaceItem extends React.PureComponent { SpaceStore.instance.setActiveSpace(this.props.space); }; + private onMenuOpenClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + const target = ev.target as HTMLButtonElement; + this.setState({contextMenuPosition: target.getBoundingClientRect()}); + }; + + private onMenuClose = () => { + this.setState({contextMenuPosition: null}); + }; + + private onHomeClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "view_room", + room_id: this.props.space.roomId, + }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.props.space.getJoinRule() === "public") { + const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { + title: _t("Invite members"), + description: + { _t("Share your public space") } + modal.close()} /> + , + fixedWidth: false, + button: false, + className: "mx_SpacePanel_sharePublicSpace", + hasCloseButton: true, + }); + } else { + showRoomInviteDialog(this.props.space.roomId); + } + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onSettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceSettings(this.context, this.props.space); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onLeaveClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "leave_room", + room_id: this.props.space.roomId, + }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onNewRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewRoom(this.context, this.props.space); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (!RoomViewStore.getRoomId()) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: this.props.space.roomId, + }, true); + } + + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.SpaceMemberList, + refireParams: { space: this.props.space }, + }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onExploreRoomsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, { + space: this.props.space, + }, "mx_SpaceRoomDirectory_dialogWrapper", false, true); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private renderContextMenu(): React.ReactElement { + let contextMenu = null; + if (this.state.contextMenuPosition) { + const userId = this.context.getUserId(); + + let inviteOption; + if (this.props.space.canInvite(userId)) { + inviteOption = ( + + ); + } + + let settingsOption; + let leaveSection; + if (shouldShowSpaceSettings(this.context, this.props.space)) { + settingsOption = ( + + ); + } else { + leaveSection = + + ; + } + + let newRoomOption; + if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + newRoomOption = ( + + ); + } + + contextMenu = +
+ { this.props.space.name } +
+ + { inviteOption } + + + { settingsOption } + + { newRoomOption } + + { leaveSection } +
; + } + + return ( + + + { contextMenu } + + ); + } + render() { const {space, activeSpaces, isNested} = this.props; @@ -133,6 +347,7 @@ export class SpaceItem extends React.PureComponent {
{ notifBadge } + { this.renderContextMenu() }
); @@ -149,6 +364,7 @@ export class SpaceItem extends React.PureComponent { { space.name } { notifBadge } + { this.renderContextMenu() } ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6d2f41ceae..19324e1540 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1003,6 +1003,16 @@ "Failed to copy": "Failed to copy", "Share invite link": "Share invite link", "Invite by email or username": "Invite by email or username", + "Invite members": "Invite members", + "Share your public space": "Share your public space", + "Invite people": "Invite people", + "Settings": "Settings", + "Leave space": "Leave space", + "New room": "New room", + "Space Home": "Space Home", + "Members": "Members", + "Explore rooms": "Explore rooms", + "Space options": "Space options", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", @@ -1583,7 +1593,6 @@ "Favourited": "Favourited", "Favourite": "Favourite", "Low Priority": "Low Priority", - "Settings": "Settings", "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", @@ -1672,7 +1681,6 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", - "Members": "Members", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin": "Unpin", @@ -2510,13 +2518,11 @@ "Explore Public Rooms": "Explore Public Rooms", "Create a Group Chat": "Create a Group Chat", "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", - "Explore rooms": "Explore rooms", "Failed to reject invitation": "Failed to reject invitation", "Cannot create rooms in this community": "Cannot create rooms in this community", "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.", "This space is not public. You will not be able to rejoin without an invite.": "This space is not public. You will not be able to rejoin without an invite.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", - "Leave space": "Leave space", "Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", @@ -2592,7 +2598,6 @@ "Manage rooms": "Manage rooms", "Find a room...": "Find a room...", "Accept Invite": "Accept Invite", - "Invite people": "Invite people", "Add existing rooms & spaces": "Add existing rooms & spaces", "%(count)s members|other": "%(count)s members", "%(count)s members|one": "%(count)s member", @@ -2607,7 +2612,6 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", - "Share your public space": "Share your public space", "At the moment only you can see it.": "At the moment only you can see it.", "Finish": "Finish", "Who are you working with?": "Who are you working with?",