Merge branch 'develop' into travis/room-list/sublist-badges

This commit is contained in:
Travis Ralston 2020-06-08 18:12:43 -06:00
commit 9859891b4d
19 changed files with 546 additions and 26 deletions

View file

@ -29,6 +29,7 @@
@import "./structures/_ToastContainer.scss"; @import "./structures/_ToastContainer.scss";
@import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_TopLeftMenuButton.scss";
@import "./structures/_UploadBar.scss"; @import "./structures/_UploadBar.scss";
@import "./structures/_UserMenuButton.scss";
@import "./structures/_ViewSource.scss"; @import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss"; @import "./structures/auth/_Login.scss";

View file

@ -73,6 +73,11 @@ $roomListMinimizedWidth: 50px;
font-weight: 600; font-weight: 600;
font-size: $font-15px; font-size: $font-15px;
line-height: $font-20px; line-height: $font-20px;
flex: 1;
}
.mx_LeftPanel2_headerButtons {
// No special styles: the rest of the layout happens to make it work.
} }
.mx_LeftPanel2_breadcrumbsContainer { .mx_LeftPanel2_breadcrumbsContainer {

View file

@ -0,0 +1,162 @@
/*
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.
*/
.mx_UserMenuButton {
// No special styles on the button itself
}
.mx_UserMenuButton_contextMenu {
width: 231px;
// Put 20px of padding around the whole menu. We do this instead of a
// simple `padding: 20px` rule so the horizontal rules added by the
// optionLists is rendered correctly (full width).
> * {
padding-left: 20px;
padding-right: 20px;
&:first-child {
padding-top: 20px;
}
&:last-child {
padding-bottom: 20px;
}
}
.mx_UserMenuButton_contextMenu_header {
// Create a flexbox to organize the header a bit easier
display: flex;
align-items: center;
&:nth-child(n + 1) {
// The first header will have appropriate padding, subsequent ones need a margin.
margin-top: 10px;
}
.mx_UserMenuButton_contextMenu_name {
// Create another flexbox of columns to handle large user IDs
display: flex;
flex-direction: column;
// fit the container
flex: 1;
width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button
* {
// Automatically grow all subelements to fit the container
flex: 1;
width: 100%;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_UserMenuButton_contextMenu_displayName {
font-weight: bold;
font-size: $font-15px;
line-height: $font-20px;
}
.mx_UserMenuButton_contextMenu_userId {
font-size: $font-15px;
line-height: $font-24px;
}
}
.mx_UserMenuButton_contextMenu_themeButton {
min-width: 32px;
max-width: 32px;
width: 32px;
height: 32px;
margin-left: 8px;
border-radius: 32px;
background-color: $theme-button-bg-color;
cursor: pointer;
// to make alignment easier, create flexbox for the image
display: flex;
align-items: center;
justify-content: center;
}
}
.mx_UserMenuButton_contextMenu_optionList {
margin-top: 20px;
// This is a bit of a hack when we could just use a simple border-top property,
// however we have a (kinda) good reason for doing it this way: we need opacity.
// To get the right color, we need an opacity modifier which means we have to work
// around the problem. PostCSS doesn't support the opacity() function, and if we
// use something like postcss-functions we quickly run into an issue where the
// function we would define gets passed a CSS variable for custom themes, which
// can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379
//
// Therefore, we just hack in a line and border the thing ourselves
&::before {
border-top: 1px solid $primary-fg-color;
opacity: 0.1;
content: '';
// Counteract the padding problems (width: 100% ignores the 40px padding,
// unless we position it absolutely then it does the right thing).
width: 100%;
position: absolute;
left: 0;
}
ul {
list-style: none;
margin: 0;
padding: 0;
li {
margin: 0;
padding: 20px 0 0;
.mx_AccessibleButton {
text-decoration: none;
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
// Create a flexbox to more easily define the list items
display: flex;
align-items: center;
img { // icons
width: 16px;
min-width: 16px;
max-width: 16px;
}
span { // labels
padding-left: 14px;
width: 100%;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
}
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 361 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-horizontal"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>

After

Width:  |  Height:  |  Size: 343 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>

After

Width:  |  Height:  |  Size: 650 B

View file

@ -177,6 +177,7 @@ $header-divider-color: #91A1C0;
$roomtile2-preview-color: #9e9e9e; $roomtile2-preview-color: #9e9e9e;
$roomtile2-badge-color: #61708b; $roomtile2-badge-color: #61708b;
$roomtile2-selected-bg-color: #FFF; $roomtile2-selected-bg-color: #FFF;
$theme-button-bg-color: #e3e8f0;
$roomtile-name-color: #61708b; $roomtile-name-color: #61708b;
$roomtile-badge-fg-color: $accent-fg-color; $roomtile-badge-fg-color: $accent-fg-color;

View file

@ -27,6 +27,7 @@ import { Action } from "../../dispatcher/actions";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseAvatar from '../views/avatars/BaseAvatar'; import BaseAvatar from '../views/avatars/BaseAvatar';
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
import UserMenuButton from "./UserMenuButton";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -49,7 +50,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Properly support TagPanel // TODO: Properly support TagPanel
// TODO: Properly support searching/filtering // TODO: Properly support searching/filtering
// TODO: Properly support breadcrumbs // TODO: Properly support breadcrumbs
// TODO: Properly support TopLeftMenu (User Settings)
// TODO: a11y // TODO: a11y
// TODO: actually make this useful in general (match design proposals) // TODO: actually make this useful in general (match design proposals)
// TODO: Fadable support (is this still needed?) // TODO: Fadable support (is this still needed?)
@ -115,6 +115,9 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
/> />
</span> </span>
<span className="mx_LeftPanel2_userName">{displayName}</span> <span className="mx_LeftPanel2_userName">{displayName}</span>
<span className="mx_LeftPanel2_headerButtons">
<UserMenuButton />
</span>
</div> </div>
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer"> <div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
<RoomBreadcrumbs /> <RoomBreadcrumbs />

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,13 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
export default class RoomSettingsDialog extends React.Component { export default class RoomSettingsDialog extends React.Component {
static propTypes = { static propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
@ -56,21 +63,25 @@ export default class RoomSettingsDialog extends React.Component {
const tabs = []; const tabs = [];
tabs.push(new Tab( tabs.push(new Tab(
ROOM_GENERAL_TAB,
_td("General"), _td("General"),
"mx_RoomSettingsDialog_settingsIcon", "mx_RoomSettingsDialog_settingsIcon",
<GeneralRoomSettingsTab roomId={this.props.roomId} />, <GeneralRoomSettingsTab roomId={this.props.roomId} />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
ROOM_SECURITY_TAB,
_td("Security & Privacy"), _td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon", "mx_RoomSettingsDialog_securityIcon",
<SecurityRoomSettingsTab roomId={this.props.roomId} />, <SecurityRoomSettingsTab roomId={this.props.roomId} />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
ROOM_ROLES_TAB,
_td("Roles & Permissions"), _td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon", "mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={this.props.roomId} />, <RolesRoomSettingsTab roomId={this.props.roomId} />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
ROOM_NOTIFICATIONS_TAB,
_td("Notifications"), _td("Notifications"),
"mx_RoomSettingsDialog_notificationsIcon", "mx_RoomSettingsDialog_notificationsIcon",
<NotificationSettingsTab roomId={this.props.roomId} />, <NotificationSettingsTab roomId={this.props.roomId} />,
@ -78,6 +89,7 @@ export default class RoomSettingsDialog extends React.Component {
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) { if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
tabs.push(new Tab( tabs.push(new Tab(
ROOM_BRIDGES_TAB,
_td("Bridges"), _td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon", "mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab roomId={this.props.roomId} />, <BridgeSettingsTab roomId={this.props.roomId} />,
@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component {
} }
tabs.push(new Tab( tabs.push(new Tab(
ROOM_ADVANCED_TAB,
_td("Advanced"), _td("Advanced"),
"mx_RoomSettingsDialog_warningIcon", "mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />, <AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,

View file

@ -33,9 +33,21 @@ import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
export const USER_VOICE_TAB = "USER_VOICE_TAB";
export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
export const USER_LABS_TAB = "USER_LABS_TAB";
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
export const USER_HELP_TAB = "USER_HELP_TAB";
export default class UserSettingsDialog extends React.Component { export default class UserSettingsDialog extends React.Component {
static propTypes = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
initialTabId: PropTypes.string,
}; };
constructor() { constructor() {
@ -63,42 +75,50 @@ export default class UserSettingsDialog extends React.Component {
const tabs = []; const tabs = [];
tabs.push(new Tab( tabs.push(new Tab(
USER_GENERAL_TAB,
_td("General"), _td("General"),
"mx_UserSettingsDialog_settingsIcon", "mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />, <GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_APPEARANCE_TAB,
_td("Appearance"), _td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon", "mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />, <AppearanceUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_FLAIR_TAB,
_td("Flair"), _td("Flair"),
"mx_UserSettingsDialog_flairIcon", "mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />, <FlairUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_NOTIFICATIONS_TAB,
_td("Notifications"), _td("Notifications"),
"mx_UserSettingsDialog_bellIcon", "mx_UserSettingsDialog_bellIcon",
<NotificationUserSettingsTab />, <NotificationUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_PREFERENCES_TAB,
_td("Preferences"), _td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon", "mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />, <PreferencesUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_VOICE_TAB,
_td("Voice & Video"), _td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon", "mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />, <VoiceUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_SECURITY_TAB,
_td("Security & Privacy"), _td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon", "mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />, <SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab( tabs.push(new Tab(
USER_LABS_TAB,
_td("Labs"), _td("Labs"),
"mx_UserSettingsDialog_labsIcon", "mx_UserSettingsDialog_labsIcon",
<LabsUserSettingsTab />, <LabsUserSettingsTab />,
@ -106,12 +126,14 @@ export default class UserSettingsDialog extends React.Component {
} }
if (this.state.mjolnirEnabled) { if (this.state.mjolnirEnabled) {
tabs.push(new Tab( tabs.push(new Tab(
USER_MJOLNIR_TAB,
_td("Ignored users"), _td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon", "mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />, <MjolnirUserSettingsTab />,
)); ));
} }
tabs.push(new Tab( tabs.push(new Tab(
USER_HELP_TAB,
_td("Help & About"), _td("Help & About"),
"mx_UserSettingsDialog_helpIcon", "mx_UserSettingsDialog_helpIcon",
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />, <HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
@ -127,7 +149,7 @@ export default class UserSettingsDialog extends React.Component {
<BaseDialog className='mx_UserSettingsDialog' hasCancel={true} <BaseDialog className='mx_UserSettingsDialog' hasCancel={true}
onFinished={this.props.onFinished} title={_t("Settings")}> onFinished={this.props.onFinished} title={_t("Settings")}>
<div className='ms_SettingsDialog_content'> <div className='ms_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} /> <TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -36,6 +36,7 @@ export enum Action {
/** /**
* Open the user settings. No additional payload information required. * Open the user settings. No additional payload information required.
* Optionally can include an OpenToTabPayload.
*/ */
ViewUserSettings = "view_user_settings", ViewUserSettings = "view_user_settings",
@ -58,4 +59,9 @@ export enum Action {
* Focuses the user's cursor to the composer. No additional payload information required. * Focuses the user's cursor to the composer. No additional payload information required.
*/ */
FocusComposer = "focus_composer", FocusComposer = "focus_composer",
/**
* Opens the user menu (previously known as the top left menu). No additional payload information required.
*/
ToggleUserMenu = "toggle_user_menu",
} }

View file

@ -0,0 +1,27 @@
/*
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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface OpenToTabPayload extends ActionPayload {
action: Action.ViewUserSettings | string, // TODO: Add room settings action
/**
* The tab ID to open in the settings view to start, if possible.
*/
initialTabId?: string;
}

View file

@ -2042,6 +2042,14 @@
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
"Switch to light mode": "Switch to light mode",
"Switch to dark mode": "Switch to dark mode",
"Switch theme": "Switch theme",
"Security & privacy": "Security & privacy",
"All settings": "All settings",
"Archived rooms": "Archived rooms",
"Feedback": "Feedback",
"Account settings": "Account settings",
"Could not load user profile": "Could not load user profile", "Could not load user profile": "Could not load user profile",
"Verify this login": "Verify this login", "Verify this login": "Verify this login",
"Session verified": "Session verified", "Session verified": "Session verified",

View file

@ -62,7 +62,7 @@ function setCustomThemeVars(customTheme) {
} }
} }
function getCustomTheme(themeName) { export function getCustomTheme(themeName) {
// set css variables // set css variables
const customThemes = SettingsStore.getValue("custom_themes"); const customThemes = SettingsStore.getValue("custom_themes");
if (!customThemes) { if (!customThemes) {