Add context menu to spaces in the space panel

This commit is contained in:
Michael Telatynski 2021-03-02 14:34:47 +00:00
parent ca1bd78921
commit 716268b2f9
5 changed files with 314 additions and 10 deletions

View file

@ -212,6 +212,30 @@ $activeBorderColor: $secondary-fg-color;
border-radius: 8px; 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 { .mx_SpacePanel_badgeContainer {
@ -254,6 +278,10 @@ $activeBorderColor: $secondary-fg-color;
height: 0; height: 0;
display: none; 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;
}

View file

@ -19,14 +19,23 @@ limitations under the License.
import React from "react"; import React from "react";
import AccessibleButton from "../../components/views/elements/AccessibleButton"; import AccessibleButton from "../../components/views/elements/AccessibleButton";
import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
interface IProps extends React.ComponentProps<typeof AccessibleButton> { interface IProps extends React.ComponentProps<typeof AccessibleButton> {
label?: string; label?: string;
tooltip?: string;
} }
// Semantic component for representing a role=menuitem // 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; const ariaLabel = props["aria-label"] || label;
if (tooltip) {
return <AccessibleTooltipButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel} title={tooltip}>
{ children }
</AccessibleTooltipButton>;
}
return ( return (
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}> <AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
{ children } { children }

View file

@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
className: PropTypes.string, className: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
description: PropTypes.node, description: PropTypes.node,
button: PropTypes.string, button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
onFinished: PropTypes.func, onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool, hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func, 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"> <div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
{ this.props.description } { this.props.description }
</div> </div>
<DialogButtons primaryButton={this.props.button || _t('OK')} { this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
onPrimaryButtonClick={this.onFinished} onPrimaryButtonClick={this.onFinished}
hasCancel={false} hasCancel={false}
> >
</DialogButtons> </DialogButtons> }
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -23,7 +23,27 @@ import SpaceStore from "../../../stores/SpaceStore";
import NotificationBadge from "../rooms/NotificationBadge"; import NotificationBadge from "../rooms/NotificationBadge";
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton"; import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton"; 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 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 { interface IItemProps {
space?: Room; space?: Room;
@ -78,6 +98,200 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
SpaceStore.instance.setActiveSpace(this.props.space); 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() { render() {
const {space, activeSpaces, isNested} = this.props; const {space, activeSpaces, isNested} = this.props;
@ -133,6 +347,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
<div className="mx_SpaceButton_selectionWrapper"> <div className="mx_SpaceButton_selectionWrapper">
<RoomAvatar width={avatarSize} height={avatarSize} room={space} /> <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
{ notifBadge } { notifBadge }
{ this.renderContextMenu() }
</div> </div>
</RovingAccessibleTooltipButton> </RovingAccessibleTooltipButton>
); );
@ -149,6 +364,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
<RoomAvatar width={avatarSize} height={avatarSize} room={space} /> <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
<span className="mx_SpaceButton_name">{ space.name }</span> <span className="mx_SpaceButton_name">{ space.name }</span>
{ notifBadge } { notifBadge }
{ this.renderContextMenu() }
</div> </div>
</RovingAccessibleButton> </RovingAccessibleButton>
); );

View file

@ -1003,6 +1003,16 @@
"Failed to copy": "Failed to copy", "Failed to copy": "Failed to copy",
"Share invite link": "Share invite link", "Share invite link": "Share invite link",
"Invite by email or username": "Invite by email or username", "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", "Remove": "Remove",
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.", "This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
"This bridge is managed by <user />.": "This bridge is managed by <user />.", "This bridge is managed by <user />.": "This bridge is managed by <user />.",
@ -1583,7 +1593,6 @@
"Favourited": "Favourited", "Favourited": "Favourited",
"Favourite": "Favourite", "Favourite": "Favourite",
"Low Priority": "Low Priority", "Low Priority": "Low Priority",
"Settings": "Settings",
"Leave Room": "Leave Room", "Leave Room": "Leave Room",
"Room options": "Room options", "Room options": "Room options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
@ -1672,7 +1681,6 @@
"The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to", "The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to",
"Yours, or the other users internet connection": "Yours, or the other users internet connection", "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", "Yours, or the other users session": "Yours, or the other users session",
"Members": "Members",
"Room Info": "Room Info", "Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
"Unpin": "Unpin", "Unpin": "Unpin",
@ -2510,13 +2518,11 @@
"Explore Public Rooms": "Explore Public Rooms", "Explore Public Rooms": "Explore Public Rooms",
"Create a Group Chat": "Create a Group Chat", "Create a Group Chat": "Create a Group Chat",
"Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s",
"Explore rooms": "Explore rooms",
"Failed to reject invitation": "Failed to reject invitation", "Failed to reject invitation": "Failed to reject invitation",
"Cannot create rooms in this community": "Cannot create rooms in this community", "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.", "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 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.", "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 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'?", "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", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
@ -2592,7 +2598,6 @@
"Manage rooms": "Manage rooms", "Manage rooms": "Manage rooms",
"Find a room...": "Find a room...", "Find a room...": "Find a room...",
"Accept Invite": "Accept Invite", "Accept Invite": "Accept Invite",
"Invite people": "Invite people",
"Add existing rooms & spaces": "Add existing rooms & spaces", "Add existing rooms & spaces": "Add existing rooms & spaces",
"%(count)s members|other": "%(count)s members", "%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member", "%(count)s members|one": "%(count)s member",
@ -2607,7 +2612,6 @@
"Failed to create initial space rooms": "Failed to create initial space rooms", "Failed to create initial space rooms": "Failed to create initial space rooms",
"Skip for now": "Skip for now", "Skip for now": "Skip for now",
"Creating rooms...": "Creating rooms...", "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.", "At the moment only you can see it.": "At the moment only you can see it.",
"Finish": "Finish", "Finish": "Finish",
"Who are you working with?": "Who are you working with?", "Who are you working with?": "Who are you working with?",