Tweaks to informational architecture 1.1 (#7052)

* Move user avatar to Space panel

* Add room list header for 'Home' or 'Space Name' to room list
Add existing Space context menus to room list header

* Re-add pending room join spinner

* Iterate RoomListHeader plus context menu

* Iterate space context menu

* Iterate room list + interactions

* Move DND to new iA model

* Replace composer custom status management with usermenu one

* Cull Quick Actions

* Iterate minimized room list state

* delint

* Merge the RoomListNumResults into the RoomListHeader

* Make the search shortcut prompt semi-bold

* Iterate RoomListHeader based on design review

* Iterate UserMenu based on feedback

* Add name to expanded spacepanel usermenu button

* i18n

* Make room sub list aux button components more generic

* Change left panel explore button to only refer to room directory

* Iterate RoomListHeader

* Fix custom user status input field width in Chrome

* Bring back Notification settings button

* delint

* i18n

* post-merge fix

* iterate pr

* Remove unused state

* update copy

* Apply suggestions from PR review

* delint

* Update invite iconography

* Iterate Space context menu to match Figma

* Fix chevron alignment

* Fix edge case for RoomListHeader on metaspaces

* Wire up general rageshake-driven feedback mechanism

* Add IA1.1 info toast

* add missing alt attribute

* delint

* delint

* tweak ia toast priority

* e2e test account for new toast

* autofocus feedback field and remove old subheading

* tweak copy

* Iterate space panel colours to match Figma

* Iterate PR

* delint

* Fix feedback submission with object setting values

* iterate based on review

* Tweak colours and update splash image

* Tweaks based on review

* Remove room list prompt, made redundant by the big fat `+`

* Fix edge cases around User Menu positioning and dnd

* Add missing import, bad merge?

* Update aria label in e2e test

* Fix room list space rooms context menu explore button behaviour

* Tweak copy

* Revert order of options in the UserMenu

* Tweak copy

* i18n
This commit is contained in:
Michael Telatynski
2021-11-30 18:08:46 +00:00
committed by GitHub
parent c09e0efdb9
commit 8fe582b094
49 changed files with 1433 additions and 1483 deletions

View File

@@ -24,7 +24,6 @@ import RoomList from "../views/rooms/RoomList";
import CallHandler from "../../CallHandler";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions";
import UserMenu from "./UserMenu";
import RoomSearch from "./RoomSearch";
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
@@ -33,15 +32,14 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomListHeader from "../views/rooms/RoomListHeader";
import { Key } from "../../Keyboard";
interface IProps {
@@ -63,9 +61,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private focusedElement = null;
private isDoingStickyHeaders = false;
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps) {
super(props);
@@ -327,14 +322,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
};
private renderHeader(): React.ReactNode {
return (
<div className="mx_LeftPanel_userHeader">
<UserMenu isMinimized={this.props.isMinimized} />
</div>
);
}
private renderBreadcrumbs(): React.ReactNode {
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
return (
@@ -362,7 +349,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
/>;
}
const space = this.state.activeSpace[0] === "!" ? this.context.getRoom(this.state.activeSpace) : null;
return (
<div
className="mx_LeftPanel_filterContainer"
@@ -378,13 +364,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{ dialPadButton }
<AccessibleTooltipButton
className={classNames("mx_LeftPanel_exploreButton", {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
})}
{ this.state.activeSpace === MetaSpace.Home && <AccessibleTooltipButton
className="mx_LeftPanel_exploreButton"
onClick={this.onExplore}
title={space ? _t("Explore %(spaceName)s", { spaceName: space.name }) : _t("Explore rooms")}
/>
title={_t("Explore rooms")}
/> }
</div>
);
}
@@ -415,10 +399,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return (
<div className={containerClasses} ref={this.ref}>
<aside className="mx_LeftPanel_roomListContainer">
{ this.renderHeader() }
{ this.renderSearchDialExplore() }
{ this.renderBreadcrumbs() }
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
{ !this.props.isMinimized && <RoomListHeader onVisibilityChange={this.refreshStickyHeaders} /> }
<div className="mx_LeftPanel_roomListWrapper">
<div
className={roomListClasses}

View File

@@ -82,7 +82,6 @@ import SpaceStore from "../../stores/spaces/SpaceStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
@@ -112,6 +111,10 @@ import { initSentry } from "../../sentry";
import { logger } from "matrix-js-sdk/src/logger";
import { showSpaceInvite } from "../../utils/space";
import GenericToast from "../views/toasts/GenericToast";
import InfoDialog from "../views/dialogs/InfoDialog";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import AccessibleButton from "../views/elements/AccessibleButton";
/** constants for MatrixChat.state.view */
export enum Views {
@@ -712,16 +715,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break;
}
case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace[0] === "!") {
defaultDispatcher.dispatch({
action: "view_room",
room_id: SpaceStore.instance.activeSpace,
});
} else {
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText,
}, 'mx_RoomDirectory_dialogWrapper', false, true);
}
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText,
}, 'mx_RoomDirectory_dialogWrapper', false, true);
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
@@ -1476,6 +1472,61 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
showNotificationsToast(false);
}
if (!localStorage.getItem("mx_seen_ia_1.1_changes_toast")) {
const key = "IA_1.1_TOAST";
ToastStore.sharedInstance().addOrReplaceToast({
key,
title: _t("Testing small changes"),
props: {
description: _t("Your feedback is wanted as we try out some design changes."),
acceptLabel: _t("More info"),
onAccept: () => {
Modal.createDialog(InfoDialog, {
title: _t("We're testing some design changes"),
description: <>
<img
src={require("../../../res/img/ia-design-changes.png")}
width="636"
height="303"
alt=""
/>
<p>{ _t(
"Your ongoing feedback would be very welcome, so if you see anything " +
"different you want to comment on, <a>please let us know about it</a>. " +
"Click your avatar to find a quick feedback link.",
{},
{
a: sub => <AccessibleButton
kind="link"
onClick={(ev) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
}}
>
{ sub }
</AccessibleButton>,
},
) }</p>
<p>{ _t("If you'd like to preview or test some potential upcoming changes, " +
"there's an option in feedback to let us contact you.") }</p>
</>,
}, "mx_DialogDesignChanges_wrapper");
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
ToastStore.sharedInstance().dismissToast(key);
},
rejectLabel: _t("Dismiss"),
onReject: () => {
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
ToastStore.sharedInstance().dismissToast(key);
},
},
icon: "labs",
component: GenericToast,
priority: 9,
});
}
dis.fire(Action.FocusSendMessageComposer);
this.setState({
ready: true,

View File

@@ -17,7 +17,6 @@ limitations under the License.
import * as React from "react";
import { createRef } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
@@ -29,7 +28,8 @@ import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCo
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/spaces";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { isMac } from "../../Keyboard";
interface IProps {
isMinimized: boolean;
@@ -42,7 +42,6 @@ interface IProps {
interface IState {
query: string;
focused: boolean;
inSpaces: boolean;
}
@replaceableComponent("structures.RoomSearch")
@@ -57,13 +56,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
this.state = {
query: "",
focused: false,
inSpaces: false,
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@@ -84,15 +81,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
}
private onSpaces = (spaces: Room[]) => {
this.setState({
inSpaces: spaces.length > 0,
});
};
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.ViewRoom && payload.clear_search) {
this.clearInput();
@@ -163,11 +153,6 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
});
let placeholder = _t("Filter");
if (this.state.inSpaces) {
placeholder = _t("Filter all spaces");
}
let icon = (
<div className='mx_RoomSearch_icon' />
);
@@ -181,7 +166,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onBlur={this.onBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
placeholder={placeholder}
placeholder={_t("Filter")}
autoComplete="off"
/>
);
@@ -193,6 +178,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onClick={this.clearInput}
/>
);
let shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt">
{ isMac ? "⌘ K" : "Ctrl K" }
</div>;
if (this.props.isMinimized) {
icon = (
@@ -204,12 +192,14 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
);
input = null;
clearButton = null;
shortcutPrompt = null;
}
return (
<div className={classes}>
{ icon }
{ input }
{ shortcutPrompt }
{ clearButton }
</div>
);

View File

@@ -48,6 +48,7 @@ import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPan
import { useStateArray } from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare";
import {
shouldShowSpaceInvite,
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
@@ -396,7 +397,7 @@ const SpaceLandingAddButton = ({ space }) => {
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
iconClassName="mx_RoomList_iconAddExistingRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -440,9 +441,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
const userId = cli.getUserId();
let inviteButton;
if (((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) &&
shouldShowComponent(UIComponent.InviteUsers)
) {
if (shouldShowSpaceInvite(space) && shouldShowComponent(UIComponent.InviteUsers)) {
inviteButton = (
<AccessibleButton
kind="primary"

View File

@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from "react";
import React, { createRef, useContext, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import * as fbEmitter from "fbemitter";
import classNames from "classnames";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
@@ -25,7 +25,7 @@ import dis from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
import { ContextMenuButton } from "./ContextMenu";
import { ChevronFace, ContextMenuButton } from "./ContextMenu";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
@@ -42,27 +42,62 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { SettingLevel } from "../../settings/SettingLevel";
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import { showCommunityInviteDialog } from "../../RoomInvite";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import RoomName from "../views/elements/RoomName";
import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton";
import { logger } from "matrix-js-sdk/src/logger";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
const CustomStatusSection = () => {
const cli = useContext(MatrixClientContext);
const setStatus = cli.getUser(cli.getUserId()).unstable_statusMessage || "";
const [value, setValue] = useState(setStatus);
let details: JSX.Element;
if (value !== setStatus) {
details = <>
<p>{ _t("Your status will be shown to people you have a DM with.") }</p>
<AccessibleButton
onClick={() => cli._unstable_setStatusMessage(value)}
kind="primary_outline"
>
{ value ? _t("Set status") : _t("Clear status") }
</AccessibleButton>
</>;
}
return <div className="mx_UserMenu_CustomStatusSection">
<div className="mx_UserMenu_CustomStatusSection_input">
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder={_t("Set a new status")}
autoComplete="off"
/>
<AccessibleButton
tabIndex={-1}
title={_t("Clear")}
className="mx_UserMenu_CustomStatusSection_clear"
onClick={() => setValue("")}
/>
</div>
{ details }
</div>;
};
interface IProps {
isMinimized: boolean;
isPanelCollapsed: boolean;
}
type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
@@ -72,14 +107,30 @@ interface IState {
isDarkTheme: boolean;
isHighContrast: boolean;
selectedSpace?: Room;
pendingRoomJoin: Set<string>;
dndEnabled: boolean;
}
const toRightOf = (rect: PartialDOMRect) => {
return {
left: rect.width + rect.left + 8,
top: rect.top,
chevronFace: ChevronFace.None,
};
};
const below = (rect: PartialDOMRect) => {
return {
left: rect.left,
top: rect.top + rect.height,
chevronFace: ChevronFace.None,
};
};
@replaceableComponent("structures.UserMenu")
export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;
private dndWatcherRef: string;
private readonly dndWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private tagStoreRef: fbEmitter.EventSubscription;
@@ -90,7 +141,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(),
pendingRoomJoin: new Set<string>(),
dndEnabled: this.doNotDisturb,
selectedSpace: SpaceStore.instance.activeSpaceRoom,
};
@@ -99,8 +150,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
// Force update is the easiest way to trigger the UI update (we don't store state for this)
this.dndWatcherRef = SettingsStore.watchSetting("doNotDisturb", null, () => this.forceUpdate());
SettingsStore.monitorSetting("feature_dnd", null);
SettingsStore.monitorSetting("doNotDisturb", null);
}
private get doNotDisturb(): boolean {
return SettingsStore.getValue("doNotDisturb");
}
private get hasHomePage(): boolean {
@@ -111,7 +166,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
MatrixClientPeg.get().on("Room", this.onRoom);
}
public componentWillUnmount() {
@@ -123,13 +177,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (SpaceStore.spacesEnabled) {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
MatrixClientPeg.get().removeListener("Room", this.onRoom);
}
private onRoom = (room: Room): void => {
this.removePendingJoinRoom(room.roomId);
};
private onTagStoreUpdate = () => {
this.forceUpdate(); // we don't have anything useful in state to update
};
@@ -178,8 +227,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
});
};
private onAction = (ev: ActionPayload) => {
switch (ev.action) {
private onAction = (payload: ActionPayload) => {
switch (payload.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) {
this.setState({ contextMenuPosition: null });
@@ -187,36 +236,27 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (this.buttonRef.current) this.buttonRef.current.click();
}
break;
case Action.JoinRoom:
this.addPendingJoinRoom(ev.roomId);
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
this.removePendingJoinRoom(ev.roomId);
break;
case Action.SettingUpdated: {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
switch (settingUpdatedPayload.settingName) {
case "feature_dnd":
case "doNotDisturb": {
const dndEnabled = this.doNotDisturb;
if (this.state.dndEnabled !== dndEnabled) {
this.setState({ dndEnabled });
}
break;
}
}
}
}
};
private addPendingJoinRoom(roomId: string): void {
this.setState({
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
.add(roomId),
});
}
private removePendingJoinRoom(roomId: string): void {
if (this.state.pendingRoomJoin.delete(roomId)) {
this.setState({
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
});
}
}
private onOpenMenuClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
this.setState({ contextMenuPosition: ev.currentTarget.getBoundingClientRect() });
};
private onContextMenu = (ev: React.MouseEvent) => {
@@ -262,15 +302,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onShowArchived = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038
// Note: You'll need to uncomment the button too.
logger.log("TODO: Show archived rooms");
};
private onProvideFeedback = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@@ -312,50 +343,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunitySettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// We'd ideally just pop open a right panel with the member list, but the current
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
// switch to the general room and open the member list there as it should be in sync
// anyways.
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat) {
dis.dispatch({
action: Action.ViewRoom,
room_id: chat.roomId,
}, true);
dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
title: _t('Failed to find the general chat for this community'),
description: _t("Failed to find the general chat for this community"),
});
}
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onDndToggle = (ev) => {
private onDndToggle = (ev: ButtonEvent) => {
ev.stopPropagation();
const current = SettingsStore.getValue("doNotDisturb");
SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
@@ -364,8 +352,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null;
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
let topSection;
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
if (MatrixClientPeg.get().isGuest()) {
@@ -411,6 +397,24 @@ export default class UserMenu extends React.Component<IProps, IState> {
);
}
let customStatusSection: JSX.Element;
if (SettingsStore.getValue("feature_custom_status")) {
customStatusSection = <CustomStatusSection />;
}
let dndButton: JSX.Element;
if (SettingsStore.getValue("feature_dnd")) {
dndButton = (
<IconizedContextMenuCheckbox
iconClassName={this.state.dndEnabled ? "mx_UserMenu_iconDnd" : "mx_UserMenu_iconDndOff"}
label={_t("Do not disturb")}
onClick={this.onDndToggle}
active={this.state.dndEnabled}
words
/>
);
}
let feedbackButton;
if (SettingsStore.getValue(UIFeature.Feedback)) {
feedbackButton = <IconizedContextMenuOption
@@ -420,156 +424,68 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>;
}
let primaryHeader = (
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ OwnProfileStore.instance.displayName }
</span>
<span className="mx_UserMenu_contextMenu_userId">
{ MatrixClientPeg.get().getUserId() }
</span>
</div>
);
let primaryOptionList = (
<React.Fragment>
<IconizedContextMenuOptionList>
{ homeButton }
{ dndButton }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell"
label={_t("Notifications")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ feedbackButton }
<IconizedContextMenuOption
className="mx_IconizedContextMenu_option_red"
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
);
if (MatrixClientPeg.get().isGuest()) {
primaryOptionList = (
<IconizedContextMenuOptionList>
{ homeButton }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell"
label={_t("Notification settings")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ /* <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconArchive"
label={_t("Archived rooms")}
onClick={this.onShowArchived}
/> */ }
{ feedbackButton }
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
</React.Fragment>
);
let secondarySection = null;
if (prototypeCommunityName) {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
primaryHeader = (
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ prototypeCommunityName }
</span>
</div>
);
let settingsOption;
let inviteOption;
if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
inviteOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconInvite"
label={_t("Invite")}
onClick={this.onCommunityInviteClick}
/>
);
}
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={this.onCommunitySettingsClick}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
);
}
primaryOptionList = (
<IconizedContextMenuOptionList>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={this.onCommunityMembersClick}
/>
{ inviteOption }
{ feedbackButton }
</IconizedContextMenuOptionList>
);
secondarySection = (
<React.Fragment>
<hr />
<div className="mx_UserMenu_contextMenu_header">
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ OwnProfileStore.instance.displayName }
</span>
<span className="mx_UserMenu_contextMenu_userId">
{ MatrixClientPeg.get().getUserId() }
</span>
</div>
</div>
<IconizedContextMenuOptionList>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("User settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ feedbackButton }
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSignOut"
label={_t("Sign out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
</React.Fragment>
);
} else if (MatrixClientPeg.get().isGuest()) {
primaryOptionList = (
<React.Fragment>
<IconizedContextMenuOptionList>
{ homeButton }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
onClick={(e) => this.onSettingsOpen(e, null)}
/>
{ feedbackButton }
</IconizedContextMenuOptionList>
</React.Fragment>
);
}
const classes = classNames({
"mx_UserMenu_contextMenu": true,
"mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName,
});
const position = this.props.isPanelCollapsed
? toRightOf(this.state.contextMenuPosition)
: below(this.state.contextMenuPosition);
return <IconizedContextMenu
// numerical adjustments to overlap the context menu by just over the width of the
// menu icon and make it look connected
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 10}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height + 8}
{...position}
onFinished={this.onCloseMenu}
className={classes}
className="mx_UserMenu_contextMenu"
>
<div className="mx_UserMenu_contextMenu_header">
{ primaryHeader }
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{ OwnProfileStore.instance.displayName }
</span>
<span className="mx_UserMenu_contextMenu_userId">
{ MatrixClientPeg.get().getUserId() }
</span>
</div>
<AccessibleTooltipButton
className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
@@ -582,9 +498,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
</AccessibleTooltipButton>
</div>
{ customStatusSection }
{ topSection }
{ primaryOptionList }
{ secondarySection }
</IconizedContextMenu>;
};
@@ -595,102 +511,45 @@ export default class UserMenu extends React.Component<IProps, IState> {
const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
let isPrototype = false;
let menuName = _t("User menu");
let name = <span className="mx_UserMenu_userName">{ displayName }</span>;
let buttons = (
<span className="mx_UserMenu_headerButtons">
{ /* masked image in CSS */ }
</span>
);
let dnd;
if (this.state.selectedSpace) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{ displayName }</span>
<RoomName room={this.state.selectedSpace}>
{ (roomName) => <span className="mx_UserMenu_subUserName">{ roomName }</span> }
</RoomName>
</div>
);
} else if (prototypeCommunityName) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{ prototypeCommunityName }</span>
<span className="mx_UserMenu_subUserName">{ displayName }</span>
</div>
);
menuName = _t("Community and user menu");
isPrototype = true;
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
name = (
<div className="mx_UserMenu_doubleName">
<span className="mx_UserMenu_userName">{ _t("Home") }</span>
<span className="mx_UserMenu_subUserName">{ displayName }</span>
</div>
);
isPrototype = true;
} else if (SettingsStore.getValue("feature_dnd")) {
const isDnd = SettingsStore.getValue("doNotDisturb");
dnd = <AccessibleButton
onClick={this.onDndToggle}
className={classNames({
"mx_UserMenu_dnd": true,
"mx_UserMenu_dnd_noisy": !isDnd,
"mx_UserMenu_dnd_muted": isDnd,
})}
/>;
}
if (this.props.isMinimized) {
name = null;
buttons = null;
let badge: JSX.Element;
if (this.state.dndEnabled) {
badge = <div className="mx_UserMenu_dndBadge" />;
}
const classes = classNames({
'mx_UserMenu': true,
'mx_UserMenu_minimized': this.props.isMinimized,
'mx_UserMenu_prototype': isPrototype,
});
let name: JSX.Element;
if (!this.props.isPanelCollapsed) {
name = <div className="mx_UserMenu_name">
{ displayName }
</div>;
}
return (
<React.Fragment>
<ContextMenuButton
className={classes}
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={menuName}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
>
<div className="mx_UserMenu_row">
<span className="mx_UserMenu_userAvatarContainer">
<BaseAvatar
idName={userId}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_UserMenu_userAvatar"
/>
</span>
{ name }
{ this.state.pendingRoomJoin.size > 0 && (
<InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: this.state.pendingRoomJoin.size },
)} />
</InlineSpinner>
) }
{ dnd }
{ buttons }
</div>
</ContextMenuButton>
<ContextMenuButton
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("User menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
className={classNames("mx_UserMenu", {
mx_UserMenu_cutout: badge,
})}
>
<div className="mx_UserMenu_userAvatar">
<BaseAvatar
idName={userId}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_UserMenu_userAvatar_BaseAvatar"
/>
{ badge }
</div>
{ name }
{ this.renderContextMenu() }
</React.Fragment>
</ContextMenuButton>
);
}
}