Merge branch 'develop' into travis/room-list/breadcrumbs

This commit is contained in:
Travis Ralston 2020-06-08 18:11:42 -06:00
commit bcc455b248
19 changed files with 546 additions and 26 deletions

View file

@ -26,6 +26,7 @@ import TopLeftMenuButton from "./TopLeftMenuButton";
import { Action } from "../../dispatcher/actions";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseAvatar from '../views/avatars/BaseAvatar';
import UserMenuButton from "./UserMenuButton";
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
@ -52,7 +53,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Properly support TagPanel
// TODO: Properly support searching/filtering
// TODO: Properly support breadcrumbs
// TODO: Properly support TopLeftMenu (User Settings)
// TODO: a11y
// TODO: actually make this useful in general (match design proposals)
// TODO: Fadable support (is this still needed?)
@ -142,6 +142,9 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
/>
</span>
<span className="mx_LeftPanel2_userName">{displayName}</span>
<span className="mx_LeftPanel2_headerButtons">
<UserMenuButton />
</span>
</div>
{breadcrumbs}
</div>

View file

@ -452,9 +452,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
// composer, so CTRL+` it is
if (ctrlCmdOnly) {
dis.dispatch({
action: 'toggle_top_left_menu',
});
dis.fire(Action.ToggleUserMenu);
handled = true;
}
break;

View file

@ -72,6 +72,7 @@ import {
hideToast as hideAnalyticsToast
} from "../../toasts/AnalyticsToast";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
/** constants for MatrixChat.state.view */
export enum Views {
@ -604,9 +605,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewIndexedRoom(payload.roomIndex);
break;
case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
{initialTabId: tabPayload.initialTabId},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true
);
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();

View file

@ -27,25 +27,20 @@ import { ReactNode } from "react";
* Represents a tab for the TabbedView.
*/
export class Tab {
public label: string;
public icon: string;
public body: React.ReactNode;
/**
* Creates a new tab.
* @param {string} tabLabel The untranslated tab label.
* @param {string} tabIconClass The class for the tab icon. This should be a simple mask.
* @param {React.ReactNode} tabJsx The JSX for the tab container.
* @param {string} id The tab's ID.
* @param {string} label The untranslated tab label.
* @param {string} icon The class for the tab icon. This should be a simple mask.
* @param {React.ReactNode} body The JSX for the tab container.
*/
constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) {
this.label = tabLabel;
this.icon = tabIconClass;
this.body = tabJsx;
constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) {
}
}
interface IProps {
tabs: Tab[];
initialTabId?: string;
}
interface IState {
@ -53,16 +48,17 @@ interface IState {
}
export default class TabbedView extends React.Component<IProps, IState> {
static propTypes = {
// The tabs to show
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
};
constructor(props: IProps) {
super(props);
let activeTabIndex = 0;
if (props.initialTabId) {
const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId);
if (tabIndex >= 0) activeTabIndex = tabIndex;
}
this.state = {
activeTabIndex: 0,
activeTabIndex,
};
}

View file

@ -24,6 +24,7 @@ import * as Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher/dispatcher";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
import {Action} from "../../dispatcher/actions";
const AVATAR_SIZE = 28;
@ -75,7 +76,7 @@ export default class TopLeftMenuButton extends React.Component {
onAction = (payload) => {
// For accessibility
if (payload.action === "toggle_top_left_menu") {
if (payload.action === Action.ToggleUserMenu) {
if (this._buttonRef) this._buttonRef.click();
}
};

View file

@ -0,0 +1,270 @@
/*
Copyright 2020 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 * as React from "react";
import {User} from "matrix-js-sdk/src/models/user";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { createRef } from "react";
import { _t } from "../../languageHandler";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import {getCustomTheme} from "../../theme";
import {getHostingLink} from "../../utils/HostingLink";
import AccessibleButton from "../views/elements/AccessibleButton";
interface IProps {
}
interface IState {
user: User;
menuDisplayed: boolean;
isDarkTheme: boolean;
}
export default class UserMenuButton extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
constructor(props: IProps) {
super(props);
this.state = {
menuDisplayed: false,
user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()),
isDarkTheme: this.isUserOnDarkTheme(),
};
}
private get displayName(): string {
if (MatrixClientPeg.get().isGuest()) {
return _t("Guest");
} else if (this.state.user) {
return this.state.user.displayName;
} else {
return MatrixClientPeg.get().getUserId();
}
}
public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
}
public componentWillUnmount() {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
}
private isUserOnDarkTheme(): boolean {
const theme = SettingsStore.getValue("theme");
if (theme.startsWith("custom-")) {
return getCustomTheme(theme.substring(0, 7)).is_dark;
}
return theme === "dark";
}
private onThemeChanged = () => {
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
};
private onAction = (ev: ActionPayload) => {
if (ev.action !== Action.ToggleUserMenu) return; // not interested
// For accessibility
if (this.buttonRef.current) this.buttonRef.current.click();
};
private onOpenMenuClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({menuDisplayed: true});
};
private onCloseMenu = () => {
this.setState({menuDisplayed: false});
};
private onSwitchThemeClick = () => {
// Disable system theme matching if the user hits this button
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
const newTheme = this.state.isDarkTheme ? "light" : "dark";
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
};
private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => {
ev.preventDefault();
ev.stopPropagation();
const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId};
defaultDispatcher.dispatch(payload);
this.setState({menuDisplayed: false}); // also close the menu
};
private onShowArchived = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
// TODO: Archived room view (deferred)
console.log("TODO: Show archived rooms");
};
private onProvideFeedback = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
this.setState({menuDisplayed: false}); // also close the menu
};
private onSignOutClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
this.setState({menuDisplayed: false}); // also close the menu
};
public render() {
let contextMenu;
if (this.state.menuDisplayed) {
let hostingLink;
const signupLink = getHostingLink("user-context-menu");
if (signupLink) {
hostingLink = (
<div className="mx_UserMenuButton_contextMenu_header">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => (
<a
href={signupLink}
target="_blank"
rel="noreferrer noopener"
tabIndex={-1}
>{sub}</a>
),
},
)}
</div>
);
}
const elementRect = this.buttonRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
onFinished={this.onCloseMenu}
>
<div className="mx_UserMenuButton_contextMenu">
<div className="mx_UserMenuButton_contextMenu_header">
<div className="mx_UserMenuButton_contextMenu_name">
<span className="mx_UserMenuButton_contextMenu_displayName">
{this.displayName}
</span>
<span className="mx_UserMenuButton_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}
</span>
</div>
<div
className="mx_UserMenuButton_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
>
<img
src={require("../../../res/img/feather-customised/sun.svg")}
alt={_t("Switch theme")}
width={16}
/>
</div>
</div>
{hostingLink}
<div className="mx_UserMenuButton_contextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<img src={require("../../../res/img/feather-customised/notifications.svg")} width={16} />
<span>{_t("Notification settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<img src={require("../../../res/img/feather-customised/lock.svg")} width={16} />
<span>{_t("Security & privacy")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<img src={require("../../../res/img/feather-customised/settings.svg")} width={16} />
<span>{_t("All settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onShowArchived}>
<img src={require("../../../res/img/feather-customised/archive.svg")} width={16} />
<span>{_t("Archived rooms")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onProvideFeedback}>
<img src={require("../../../res/img/feather-customised/message-circle.svg")} width={16} />
<span>{_t("Feedback")}</span>
</AccessibleButton>
</li>
</ul>
</div>
<div className="mx_UserMenuButton_contextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={this.onSignOutClick}>
<img src={require("../../../res/img/feather-customised/sign-out.svg")} width={16} />
<span>{_t("Sign out")}</span>
</AccessibleButton>
</li>
</ul>
</div>
</div>
</ContextMenu>
);
}
return (
<React.Fragment>
<ContextMenuButton
className="mx_UserMenuButton"
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("Account settings")}
isExpanded={this.state.menuDisplayed}
>
<img src={require("../../../res/img/feather-customised/more-horizontal.svg")} alt="..." width={14} />
</ContextMenuButton>
{contextMenu}
</React.Fragment>
)
}
}