Merge pull request #2476 from matrix-org/travis/modal-tab-settings

Basic structure for tabbed user settings
This commit is contained in:
Travis Ralston 2019-01-23 07:52:44 -07:00 committed by GitHub
commit 94b1d739fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 725 additions and 79 deletions

View File

@ -42,9 +42,8 @@ module.exports = {
// bind or arrow function in props causes performance issues
// (but we currently use them in some places)
"react/jsx-no-bind": ["warn", {
"ignoreRefs": true,
}],
// It's disabled here, but we should using it sparingly.
"react/jsx-no-bind": "off",
"react/jsx-key": ["error"],
// Components in JSX should always be defined.

View File

@ -160,7 +160,7 @@ textarea {
padding: 0 58px 36px;
width: 60%;
max-width: 704px;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
box-shadow: 2px 15px 30px 0 $dialog-shadow-color;
max-height: 80%;
overflow-y: auto;
}
@ -171,7 +171,7 @@ textarea {
left: 0;
width: 100%;
height: 100%;
background-color: $dialog-background-bg-color;
background-color: $dialog-backdrop-color;
opacity: 0.8;
}

View File

@ -17,6 +17,7 @@
@import "./structures/_RoomSubList.scss";
@import "./structures/_RoomView.scss";
@import "./structures/_SearchBox.scss";
@import "./structures/_TabbedView.scss";
@import "./structures/_TagPanel.scss";
@import "./structures/_TopLeftMenuButton.scss";
@import "./structures/_UploadBar.scss";
@ -56,6 +57,7 @@
@import "./views/dialogs/_SetPasswordDialog.scss";
@import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
@ -128,6 +130,8 @@
@import "./views/settings/_IntegrationsManager.scss";
@import "./views/settings/_KeyBackupPanel.scss";
@import "./views/settings/_Notifications.scss";
@import "./views/settings/tabs/_GeneralSettingsTab.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_IncomingCallbox.scss";
@import "./views/voip/_VideoView.scss";

View File

@ -0,0 +1,92 @@
/*
Copyright 2017 Travis Ralston
Copyright 2019 New Vector Ltd
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_TabbedView {
margin: 0;
padding: 0;
display: flex;
width: 100%;
height: 100%;
}
.mx_TabbedView_tabLabels {
width: 136px;
height: 100%;
color: $tab-label-fg-color;
}
.mx_TabbedView_tabLabel {
vertical-align: text-top;
cursor: pointer;
display: block;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
height: 20px;
margin-bottom: 6px;
position: relative;
}
.mx_TabbedView_tabLabel_active {
background-color: $tab-label-active-bg-color;
color: $tab-label-active-fg-color;
}
// TODO: Remove temporary hack alongside "visit old settings" tab
.mx_TabbedView_tabLabel_TEMP_HACK {
background-color: orange;
}
.mx_TabbedView_maskedIcon {;
margin-left: 6px;
margin-right: 9px;
width: 14px;
height: 14px;
display: inline-block;
}
.mx_TabbedView_maskedIcon:before {
display: inline-block;
background-color: $tab-label-icon-bg-color;
mask-repeat: no-repeat;
mask-size: 14px;
width: 14px;
height: 14px;
mask-position: center;
content: '';
vertical-align: middle;
}
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon:before {
background-color: $tab-label-active-icon-bg-color;
}
.mx_TabbedView_tabLabel_text {
vertical-align: middle;
}
.mx_TabbedView_tabPanel {
width: calc(100% - 320px);
display: inline-block;
margin-left: 70px;
flex-grow: 1;
}
.mx_TabbedView_tabPanelContent {
flex-grow: 1;
min-width: 560px;
}

View File

@ -0,0 +1,63 @@
.mx_UserSettingsDialog_header {
font-size: 24px;
display: block;
text-align: center;
color: $dialog-title-fg-color;
margin-top: 16px;
margin-bottom: 24px;
padding: 0;
}
.mx_UserSettingsDialog_close {
position: absolute;
top: 16px;
right: 25px;
}
.mx_UserSettingsDialog_closeIcon {
width: 16px;
height: 16px;
display: inline-block;
}
.mx_UserSettingsDialog_closeIcon:before {
mask: url('$(res)/img/feather-icons/cancel.svg');
background-color: $dialog-close-fg-color;
mask-repeat: no-repeat;
mask-size: 16px;
mask-position: center;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
// ICONS
// ==========================================================
.mx_UserSettingsDialog_settingsIcon:before {
mask-image: url('$(res)/img/feather-icons/settings.svg');
}
.mx_UserSettingsDialog_voiceIcon:before {
mask-image: url('$(res)/img/feather-icons/phone.svg');
}
.mx_UserSettingsDialog_bellIcon:before {
mask-image: url('$(res)/img/feather-icons/notifications.svg');
}
.mx_UserSettingsDialog_preferencesIcon:before {
mask-image: url('$(res)/img/feather-icons/sliders.svg');
}
.mx_UserSettingsDialog_securityIcon:before {
mask-image: url('$(res)/img/feather-icons/lock.svg');
}
.mx_UserSettingsDialog_helpIcon:before {
mask-image: url('$(res)/img/feather-icons/help-circle.svg');
}

View File

@ -21,3 +21,24 @@ limitations under the License.
.mx_AccessibleButton {
cursor: pointer;
}
.mx_AccessibleButton_disabled {
cursor: default;
}
.mx_AccessibleButton_hasKind {
padding: 10px 25px;
text-align: center;
border-radius: 4px;
display: inline-block;
}
.mx_AccessibleButton_kind_primary {
color: $button-primary-fg-color;
background-color: $button-primary-bg-color;
}
.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled {
color: $button-primary-disabled-fg-color;
background-color: $button-primary-disabled-bg-color;
}

View File

@ -0,0 +1,25 @@
.mx_GeneralSettingsTab_profile {
display: flex;
}
.mx_GeneralSettingsTab_profileControls {
flex-grow: 1;
}
.mx_GeneralSettingsTab_profileControls .mx_Field #profileDisplayName {
width: calc(100% - 20px); // subtract 10px padding on left and right
}
.mx_GeneralSettingsTab_profileAvatar {
width: 88px;
height: 88px;
margin-left: 13px;
}
.mx_GeneralSettingsTab_profileAvatar div {
display: block;
width: 88px;
height: 88px;
border-radius: 4px;
background-color: #ccc;
}

View File

@ -0,0 +1,17 @@
.mx_SettingsTab_heading {
font-size: 20px;
font-weight: 600;
color: $primary-fg-color;
}
.mx_SettingsTab_subheading {
font-size: 14px;
display: block;
font-family: $font-family-semibold;
color: $primary-fg-color;
margin-bottom: 10px;
}
.mx_SettingsTab_section {
margin-top: 10px;
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="#" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<path d="M9.74464309,-3.02908503 L8.14106175,-3.02908503 L8.14106175,8.19448443 L-3.03028759,8.19448443 L-3.03028759,9.7978515 L8.14106175,9.7978515 L8.14106175,20.9685098 L9.74464309,20.9685098 L9.74464309,9.7978515 L20.9697124,9.7978515 L20.9697124,8.19448443 L9.74464309,8.19448443 L9.74464309,-3.02908503" id="Fill-108" opacity="0.9" fill="#9fa9ba" sketch:type="MSShapeGroup" transform="translate(8.969712, 8.969712) rotate(-315.000000) translate(-8.969712, -8.969712) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<g fill="none" fill-rule="evenodd" stroke="#454545" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
<circle cx="6" cy="6" r="6"/>
<path d="M4.254 4.2a1.8 1.8 0 0 1 3.498.6c0 1.2-1.8 1.8-1.8 1.8M6 8.991"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="14" viewBox="0 0 13 14">
<g fill="none" fill-rule="evenodd" stroke="#454545" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
<rect width="11" height="6.6" y="5.4" rx="1.2"/>
<path d="M2.444 5.4V3c0-1.657 1.368-3 3.056-3s3.056 1.343 3.056 3v2.4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12">
<g fill="none" fill-rule="evenodd" stroke="#454545" stroke-linecap="round" stroke-linejoin="round">
<path d="M2.636 11V7.111M2.636 4.889V1M7 11V6M7 3.778V1M11.364 11V8.222M11.364 6V1M1 7.111h3.273M5.364 3.778h3.272M9.727 8.222H13"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -5,6 +5,7 @@
horizontal mess. Arial empirically gets it right, hence prioritising
Arial here. */
$font-family: 'Nunito', Arial, Helvetica, Sans-Serif;
$font-family-semibold: 'Nunito SemiBold', Arial, Helvetica, Sans-Serif;
// typical text (dark-on-white in light skin)
$primary-fg-color: #454545;
@ -96,6 +97,11 @@ $avatar-bg-color: #ffffff;
$h3-color: #3d3b39;
$dialog-title-fg-color: #454545;
$dialog-backdrop-color: rgba(46, 48, 51, 0.38);
$dialog-shadow-color: rgba(0, 0, 0, 0.48);
$dialog-close-fg-color: #9fa9ba;
$dialog-background-bg-color: #e9e9e9;
$lightbox-background-bg-color: #000;
@ -186,6 +192,20 @@ $lightbox-bg-color: #454545;
$lightbox-fg-color: #ffffff;
$lightbox-border-color: #ffffff;
// Tabbed views
$tab-label-fg-color: #45474a;
$tab-label-active-fg-color: #ffffff;
$tab-label-bg-color: transparent;
$tab-label-active-bg-color: #7ac9a1;
$tab-label-icon-bg-color: #454545;
$tab-label-active-icon-bg-color: #ffffff;
// Buttons
$button-primary-fg-color: #ffffff;
$button-primary-bg-color: #7ac9a1;
$button-primary-disabled-fg-color: #ffffff;
$button-primary-disabled-bg-color: #bce4d0;
// unused?
$progressbar-color: #000;

View File

@ -4,6 +4,7 @@
horizontal mess. Arial empirically gets it right, hence prioritising
Arial here. */
$font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
$font-family-semibold: 'Open Sans', Arial, Helvetica, Sans-Serif;
// typical text (dark-on-white in light skin)
$primary-fg-color: #454545;
@ -66,6 +67,7 @@ $primary-hairline-color: #e5e5e5;
// used for the border of input text fields
$input-border-color: #f0f0f0;
$input-border-dark-color: #b8b8b8;
$input-darker-bg-color: #c1c9d6;
$input-darker-fg-color: #9fa9ba;
@ -92,6 +94,11 @@ $avatar-bg-color: #ffffff;
$h3-color: #3d3b39;
$dialog-title-fg-color: #454545;
$dialog-backdrop-color: rgba(46, 48, 51, 0.38);
$dialog-shadow-color: rgba(0, 0, 0, 0.48);
$dialog-close-fg-color: #9fa9ba;
$dialog-background-bg-color: #e9e9e9;
$lightbox-background-bg-color: #000;
@ -181,6 +188,20 @@ $imagebody-giflabel: rgba(0, 0, 0, 0.7);
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
$imagebody-giflabel-color: rgba(255, 255, 255, 1);
// Tabbed views
$tab-label-fg-color: #45474a;
$tab-label-active-fg-color: #ffffff;
$tab-label-bg-color: transparent;
$tab-label-active-bg-color: #7ac9a1;
$tab-label-icon-bg-color: #454545;
$tab-label-active-icon-bg-color: #ffffff;
// Buttons
$button-primary-fg-color: #ffffff;
$button-primary-bg-color: #7ac9a1;
$button-primary-disabled-fg-color: #ffffff;
$button-primary-disabled-bg-color: #bce4d0;
// unused?
$progressbar-color: #000;

View File

@ -610,7 +610,17 @@ export default React.createClass({
case 'view_indexed_room':
this._viewIndexedRoom(payload.roomIndex);
break;
case 'view_user_settings':
case 'view_user_settings': {
if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) {
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {});
} else {
this._setPage(PageTypes.UserSettings);
this.notifyNewScreen('settings');
}
break;
}
case 'view_old_user_settings':
this._setPage(PageTypes.UserSettings);
this.notifyNewScreen('settings');
break;

View File

@ -0,0 +1,118 @@
/*
Copyright 2017 Travis Ralston
Copyright 2019 New Vector Ltd
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 {_t} from '../../languageHandler';
import PropTypes from "prop-types";
/**
* Represents a tab for the TabbedView.
*/
export class Tab {
/**
* 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 {string} tabJsx The JSX for the tab container.
*/
constructor(tabLabel, tabIconClass, tabJsx) {
this.label = tabLabel;
this.icon = tabIconClass;
this.body = tabJsx;
}
}
export class TabbedView extends React.Component {
static propTypes = {
// The tabs to show
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
};
constructor() {
super();
this.state = {
activeTabIndex: 0,
};
}
_getActiveTabIndex() {
if (!this.state || !this.state.activeTabIndex) return 0;
return this.state.activeTabIndex;
}
/**
* Shows the given tab
* @param {Tab} tab the tab to show
* @private
*/
_setActiveTab(tab) {
const idx = this.props.tabs.indexOf(tab);
if (idx !== -1) {
this.setState({activeTabIndex: idx});
} else {
console.error("Could not find tab " + tab.label + " in tabs");
}
}
_renderTabLabel(tab) {
let classes = "mx_TabbedView_tabLabel ";
const idx = this.props.tabs.indexOf(tab);
if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
if (tab.label === "Visit old settings") classes += "mx_TabbedView_tabLabel_TEMP_HACK";
let tabIcon = null;
if (tab.icon) {
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
}
const onClickHandler = () => this._setActiveTab(tab);
return (
<span className={classes} key={"tab_label_" + tab.label}
onClick={onClickHandler}>
{tabIcon}
<span className="mx_TabbedView_tabLabel_text">
{_t(tab.label)}
</span>
</span>
);
}
_renderTabPanel(tab) {
return (
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
{tab.body}
</div>
);
}
render() {
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
return (
<div className="mx_TabbedView">
<div className="mx_TabbedView_tabLabels">
{labels}
</div>
{panel}
</div>
);
}
}

View File

@ -0,0 +1,99 @@
/*
Copyright 2019 New Vector Ltd
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 React from 'react';
import PropTypes from 'prop-types';
import {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import GeneralSettingsTab from "../settings/tabs/GeneralSettingsTab";
import dis from '../../../dispatcher';
// TODO: Ditch this whole component
export class TempTab extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
componentDidMount(): void {
dis.dispatch({action: "view_old_user_settings"});
this.props.onClose();
}
render() {
return <div>Hello World</div>;
}
}
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_getTabs() {
return [
new Tab(
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralSettingsTab />,
),
new Tab(
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
<div>Notifications Test</div>,
),
new Tab(
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
<div>Preferences Test</div>,
),
new Tab(
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<div>Voice Test</div>,
),
new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<div>Security Test</div>,
),
new Tab(
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<div>Help Test</div>,
),
new Tab(
_td("Visit old settings"),
"mx_UserSettingsDialog_helpIcon",
<TempTab onClose={this.props.onFinished} />,
),
];
}
render() {
return (
<div className="mx_UserSettingsDialog">
<div className="mx_UserSettingsDialog_header">
{_t("Settings")}
<span className="mx_UserSettingsDialog_close">
<AccessibleButton className="mx_UserSettingsDialog_closeIcon" onClick={this.props.onFinished} />
</span>
</div>
<TabbedView tabs={this._getTabs()} />
</div>
);
}
}

View File

@ -28,41 +28,56 @@ import { KeyCode } from '../../../Keyboard';
* @returns {Object} rendered react
*/
export default function AccessibleButton(props) {
const {element, onClick, children, ...restProps} = props;
restProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
// It's because we are using html buttons at a few places e.g. inside dialogs
// And divs which we report as role button to assistive technologies.
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
restProps.onKeyDown = function(e) {
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.onKeyUp = function(e) {
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
}
};
const {element, onClick, children, kind, disabled, ...restProps} = props;
if (!disabled) {
restProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
// It's because we are using html buttons at a few places e.g. inside dialogs
// And divs which we report as role button to assistive technologies.
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
restProps.onKeyDown = function(e) {
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
}
};
restProps.onKeyUp = function(e) {
if (e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
e.preventDefault();
return onClick(e);
}
if (e.keyCode === KeyCode.ENTER) {
e.stopPropagation();
e.preventDefault();
}
};
}
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";
restProps.className = (restProps.className ? restProps.className + " " : "") +
"mx_AccessibleButton";
if (kind) {
// We apply a hasKind class to maintain backwards compatibility with
// buttons which might not know about kind and break
restProps.className += " mx_AccessibleButton_hasKind mx_AccessibleButton_kind_" + kind;
}
if (disabled) {
restProps.className += " mx_AccessibleButton_disabled";
}
return React.createElement(element, restProps, children);
}
@ -76,6 +91,12 @@ AccessibleButton.propTypes = {
children: PropTypes.node,
element: PropTypes.string,
onClick: PropTypes.func.isRequired,
// The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options.
kind: PropTypes.string,
disabled: PropTypes.bool,
};
AccessibleButton.defaultProps = {

View File

@ -0,0 +1,95 @@
/*
Copyright 2019 New Vector Ltd
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 React from 'react';
import {_t} from "../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
export default class GeneralSettingsTab extends React.Component {
constructor() {
super();
const client = MatrixClientPeg.get();
this.state = {
userId: client.getUserId(),
displayName: client.getUser(client.getUserId()).displayName,
enableProfileSave: false,
};
}
_saveProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this.setState({enableProfileSave: false});
// TODO: What do we do about errors?
await MatrixClientPeg.get().setDisplayName(this.state.displayName);
// TODO: Support avatars
this.setState({enableProfileSave: true});
};
_onDisplayNameChanged = (e) => {
this.setState({
displayName: e.target.value,
enableProfileSave: true,
});
};
_renderProfileSection() {
// TODO: Ditch avatar placeholder and use the real thing
const form = (
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}>
<div className="mx_GeneralSettingsTab_profile">
<div className="mx_GeneralSettingsTab_profileControls">
<p className="mx_GeneralSettingsTab_profileUsername">{this.state.userId}</p>
<Field id="profileDisplayName" label={_t("Display Name")}
type="text" value={this.state.displayName} autocomplete="off"
onChange={this._onDisplayNameChanged} />
</div>
<div className="mx_GeneralSettingsTab_profileAvatar">
<div />
</div>
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
</form>
);
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
{form}
</div>
);
}
render() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
{this._renderProfileSection()}
</div>
);
}
}

View File

@ -262,6 +262,7 @@
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
"Failed to join room": "Failed to join room",
"Message Pinning": "Message Pinning",
"Tabbed settings": "Tabbed settings",
"Custom user status messages": "Custom user status messages",
"Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view",
"Backup of encryption keys to server": "Backup of encryption keys to server",
@ -401,6 +402,10 @@
"Off": "Off",
"On": "On",
"Noisy": "Noisy",
"Display Name": "Display Name",
"Save": "Save",
"Profile": "Profile",
"General": "General",
"Cannot add any more widgets": "Cannot add any more widgets",
"The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.",
"Add a widget": "Add a widget",
@ -533,7 +538,6 @@
"World readable": "World readable",
"Guests can join": "Guests can join",
"Failed to set avatar.": "Failed to set avatar.",
"Save": "Save",
"(~%(count)s results)|other": "(~%(count)s results)",
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
@ -710,46 +714,6 @@
"Removed or unknown message type": "Removed or unknown message type",
"Message removed by %(userId)s": "Message removed by %(userId)s",
"Message removed": "Message removed",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
"This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot",
"Custom Server Options": "Custom Server Options",
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.",
"This allows you to use this app with an existing Matrix account on a different home server.": "This allows you to use this app with an existing Matrix account on a different home server.",
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "You can also set a custom identity server but this will typically prevent interaction with users based on email address.",
"To continue, please enter your password.": "To continue, please enter your password.",
"Password:": "Password:",
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
"Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:",
"An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s",
"Please check your email to continue registration.": "Please check your email to continue registration.",
"Token incorrect": "Token incorrect",
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
"Please enter the code it contains:": "Please enter the code it contains:",
"Code": "Code",
"Start authentication": "Start authentication",
"powered by Matrix": "powered by Matrix",
"The email field must not be blank.": "The email field must not be blank.",
"The user name field must not be blank.": "The user name field must not be blank.",
"The phone number field must not be blank.": "The phone number field must not be blank.",
"The password field must not be blank.": "The password field must not be blank.",
"Username on %(hs)s": "Username on %(hs)s",
"User name": "User name",
"Mobile phone number": "Mobile phone number",
"Forgot your password?": "Forgot your password?",
"Matrix ID": "Matrix ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"Sign in with": "Sign in with",
"Email address": "Email address",
"Sign in": "Sign in",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Email address (optional)": "Email address (optional)",
"You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s",
"Mobile phone number (optional)": "Mobile phone number (optional)",
"Default server": "Default server",
"Custom server": "Custom server",
"Home server URL": "Home server URL",
"Identity server URL": "Identity server URL",
"What does this mean?": "What does this mean?",
"Remove from community": "Remove from community",
"Disinvite this user from community?": "Disinvite this user from community?",
"Remove this user from community?": "Remove this user from community?",
@ -886,6 +850,7 @@
"And %(count)s more...|other": "And %(count)s more...",
"ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User",
"Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID",
"email address": "email address",
"That doesn't look like a valid email address": "That doesn't look like a valid email address",
@ -1012,6 +977,7 @@
"Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
"Unable to add email address": "Unable to add email address",
"Unable to verify email address.": "Unable to verify email address.",
"Email address": "Email address",
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
"Skip": "Skip",
"Only use lower case letters, numbers and '=_-./'": "Only use lower case letters, numbers and '=_-./'",
@ -1043,6 +1009,11 @@
"Room contains unknown devices": "Room contains unknown devices",
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
"Unknown devices": "Unknown devices",
"Preferences": "Preferences",
"Voice & Video": "Voice & Video",
"Security & Privacy": "Security & Privacy",
"Help & About": "Help & About",
"Visit old settings": "Visit old settings",
"Unable to load backup status": "Unable to load backup status",
"Unable to restore backup": "Unable to restore backup",
"No backup found!": "No backup found!",
@ -1094,6 +1065,44 @@
"Set status": "Set status",
"Set a new status...": "Set a new status...",
"View Community": "View Community",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
"This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot",
"Custom Server Options": "Custom Server Options",
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.",
"This allows you to use this app with an existing Matrix account on a different home server.": "This allows you to use this app with an existing Matrix account on a different home server.",
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "You can also set a custom identity server but this will typically prevent interaction with users based on email address.",
"To continue, please enter your password.": "To continue, please enter your password.",
"Password:": "Password:",
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
"Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:",
"An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s",
"Please check your email to continue registration.": "Please check your email to continue registration.",
"Token incorrect": "Token incorrect",
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
"Please enter the code it contains:": "Please enter the code it contains:",
"Code": "Code",
"Start authentication": "Start authentication",
"powered by Matrix": "powered by Matrix",
"The email field must not be blank.": "The email field must not be blank.",
"The user name field must not be blank.": "The user name field must not be blank.",
"The phone number field must not be blank.": "The phone number field must not be blank.",
"The password field must not be blank.": "The password field must not be blank.",
"Username on %(hs)s": "Username on %(hs)s",
"User name": "User name",
"Mobile phone number": "Mobile phone number",
"Forgot your password?": "Forgot your password?",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"Sign in with": "Sign in with",
"Sign in": "Sign in",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Email address (optional)": "Email address (optional)",
"You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s",
"Mobile phone number (optional)": "Mobile phone number (optional)",
"Default server": "Default server",
"Custom server": "Custom server",
"Home server URL": "Home server URL",
"Identity server URL": "Identity server URL",
"What does this mean?": "What does this mean?",
"Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
"Please install <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> for the best experience.": "Please install <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> for the best experience.",
@ -1284,7 +1293,6 @@
"VoIP": "VoIP",
"Email": "Email",
"Add email address": "Add email address",
"Profile": "Profile",
"Display name": "Display name",
"Account": "Account",
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",

View File

@ -84,6 +84,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_tabbed_settings": {
isFeature: true,
displayName: _td("Tabbed settings"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_custom_status": {
isFeature: true,
displayName: _td("Custom user status messages"),