mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 18:55:58 +03:00
Add context menu to spaces in the space panel
This commit is contained in:
parent
ca1bd78921
commit
716268b2f9
5 changed files with 314 additions and 10 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<typeof AccessibleButton> {
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
// Semantic component for representing a role=menuitem
|
||||
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
|
||||
export const MenuItem: React.FC<IProps> = ({children, label, tooltip, ...props}) => {
|
||||
const ariaLabel = props["aria-label"] || label;
|
||||
|
||||
if (tooltip) {
|
||||
return <AccessibleTooltipButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel} title={tooltip}>
|
||||
{ children }
|
||||
</AccessibleTooltipButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
|
||||
{ children }
|
||||
|
|
|
@ -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 {
|
|||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
{ this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onFinished}
|
||||
hasCancel={false}
|
||||
>
|
||||
</DialogButtons>
|
||||
</DialogButtons> }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<IItemProps, IItemState> {
|
|||
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: <React.Fragment>
|
||||
<span>{ _t("Share your public space") }</span>
|
||||
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
|
||||
</React.Fragment>,
|
||||
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<SetRightPanelPhasePayload>({
|
||||
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 = (
|
||||
<IconizedContextMenuOption
|
||||
className="mx_SpacePanel_contextMenu_inviteButton"
|
||||
iconClassName="mx_SpacePanel_iconInvite"
|
||||
label={_t("Invite people")}
|
||||
onClick={this.onInviteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let settingsOption;
|
||||
let leaveSection;
|
||||
if (shouldShowSpaceSettings(this.context, this.props.space)) {
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={this.onSettingsClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
leaveSection = <IconizedContextMenuOptionList red first>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconLeave"
|
||||
label={_t("Leave space")}
|
||||
onClick={this.onLeaveClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
let newRoomOption;
|
||||
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
newRoomOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconPlus"
|
||||
label={_t("New room")}
|
||||
onClick={this.onNewRoomClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...toRightOf(this.state.contextMenuPosition, 0)}
|
||||
onFinished={this.onMenuClose}
|
||||
className="mx_SpacePanel_contextMenu"
|
||||
compact
|
||||
>
|
||||
<div className="mx_SpacePanel_contextMenu_header">
|
||||
{ this.props.space.name }
|
||||
</div>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconHome"
|
||||
label={_t("Space Home")}
|
||||
onClick={this.onHomeClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={this.onMembersClick}
|
||||
/>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconExplore"
|
||||
label={_t("Explore rooms")}
|
||||
onClick={this.onExploreRoomsClick}
|
||||
/>
|
||||
{ newRoomOption }
|
||||
</IconizedContextMenuOptionList>
|
||||
{ leaveSection }
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_SpaceButton_menuButton"
|
||||
onClick={this.onMenuOpenClick}
|
||||
title={_t("Space options")}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {space, activeSpaces, isNested} = this.props;
|
||||
|
||||
|
@ -133,6 +347,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
|
@ -149,6 +364,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
<span className="mx_SpaceButton_name">{ space.name }</span>
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
|
|
|
@ -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 <user />.": "This bridge was provisioned by <user />.",
|
||||
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
|
||||
|
@ -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?",
|
||||
|
|
Loading…
Reference in a new issue