diff --git a/.eslintrc.js b/.eslintrc.js index 971809f851..ec48f6b2ff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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. diff --git a/res/css/_common.scss b/res/css/_common.scss index bec4c02c18..306834bcde 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -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; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 9a5e390933..2617cd7c4c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss new file mode 100644 index 0000000000..0f4b67ad71 --- /dev/null +++ b/res/css/structures/_TabbedView.scss @@ -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; +} \ No newline at end of file diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss new file mode 100644 index 0000000000..c4bd8a5110 --- /dev/null +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -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'); +} \ No newline at end of file diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index d6702a232c..23445f5f6f 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -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; +} diff --git a/res/css/views/settings/tabs/_GeneralSettingsTab.scss b/res/css/views/settings/tabs/_GeneralSettingsTab.scss new file mode 100644 index 0000000000..8bc3de8689 --- /dev/null +++ b/res/css/views/settings/tabs/_GeneralSettingsTab.scss @@ -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; +} \ No newline at end of file diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss new file mode 100644 index 0000000000..0753df56af --- /dev/null +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -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; +} diff --git a/res/img/feather-icons/cancel.svg b/res/img/feather-icons/cancel.svg new file mode 100644 index 0000000000..6b734e4053 --- /dev/null +++ b/res/img/feather-icons/cancel.svg @@ -0,0 +1,10 @@ + + + + Slice 1 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/res/img/feather-icons/help-circle.svg b/res/img/feather-icons/help-circle.svg new file mode 100644 index 0000000000..7ecb0a8f35 --- /dev/null +++ b/res/img/feather-icons/help-circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/feather-icons/lock.svg b/res/img/feather-icons/lock.svg new file mode 100644 index 0000000000..1330903b30 --- /dev/null +++ b/res/img/feather-icons/lock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/feather-icons/sliders.svg b/res/img/feather-icons/sliders.svg new file mode 100644 index 0000000000..5b5ec8656c --- /dev/null +++ b/res/img/feather-icons/sliders.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 1678c9b586..235eb02e3c 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -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; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index c03db23930..1522ee82a0 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -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; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7acceb4bb7..4c783eedac 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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; diff --git a/src/components/structures/TabbedView.js b/src/components/structures/TabbedView.js new file mode 100644 index 0000000000..2b136128f3 --- /dev/null +++ b/src/components/structures/TabbedView.js @@ -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 = ; + } + + const onClickHandler = () => this._setActiveTab(tab); + + return ( + + {tabIcon} + + {_t(tab.label)} + + + ); + } + + _renderTabPanel(tab) { + return ( +
+ {tab.body} +
+ ); + } + + render() { + const labels = this.props.tabs.map(tab => this._renderTabLabel(tab)); + const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]); + + return ( +
+
+ {labels} +
+ {panel} +
+ ); + } +} diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js new file mode 100644 index 0000000000..dd404ce280 --- /dev/null +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -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
Hello World
; + } +} + +export default class UserSettingsDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _getTabs() { + return [ + new Tab( + _td("General"), + "mx_UserSettingsDialog_settingsIcon", + , + ), + new Tab( + _td("Notifications"), + "mx_UserSettingsDialog_bellIcon", +
Notifications Test
, + ), + new Tab( + _td("Preferences"), + "mx_UserSettingsDialog_preferencesIcon", +
Preferences Test
, + ), + new Tab( + _td("Voice & Video"), + "mx_UserSettingsDialog_voiceIcon", +
Voice Test
, + ), + new Tab( + _td("Security & Privacy"), + "mx_UserSettingsDialog_securityIcon", +
Security Test
, + ), + new Tab( + _td("Help & About"), + "mx_UserSettingsDialog_helpIcon", +
Help Test
, + ), + new Tab( + _td("Visit old settings"), + "mx_UserSettingsDialog_helpIcon", + , + ), + ]; + } + + render() { + return ( +
+
+ {_t("Settings")} + + + +
+ +
+ ); + } +} diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index e30ceb85fa..1c39ba4f49 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -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 = { diff --git a/src/components/views/settings/tabs/GeneralSettingsTab.js b/src/components/views/settings/tabs/GeneralSettingsTab.js new file mode 100644 index 0000000000..c8816325c0 --- /dev/null +++ b/src/components/views/settings/tabs/GeneralSettingsTab.js @@ -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 = ( +
+
+
+

{this.state.userId}

+ +
+
+
+
+
+ + {_t("Save")} + + + ); + + return ( +
+ {_t("Profile")} + {form} +
+ ); + } + + render() { + return ( +
+
{_t("General")}
+ {this._renderProfileSection()} +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index caa51a1349..6bf857be07 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 web browser": "Robot check is currently unavailable on desktop - please use a web browser", - "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 web browser": "Robot check is currently unavailable on desktop - please use a web browser", + "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 not able to run Riot.": "Sorry, your browser is not 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 Chrome or Firefox for the best experience.": "Please install Chrome or Firefox 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", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5a9bd9e455..4871ee92f9 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -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"),