Merge pull request #2491 from matrix-org/travis/usettings/tab/general

Implement the "General" tab of new user settings
This commit is contained in:
Travis Ralston 2019-01-24 08:51:11 -07:00 committed by GitHub
commit 2a9f6186d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1355 additions and 168 deletions

View File

@ -80,6 +80,7 @@
@import "./views/elements/_RoleButton.scss"; @import "./views/elements/_RoleButton.scss";
@import "./views/elements/_Spinner.scss"; @import "./views/elements/_Spinner.scss";
@import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_ToggleSwitch.scss";
@import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_ToolTipButton.scss";
@import "./views/globals/_MatrixToolbar.scss"; @import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupPublicityToggle.scss";
@ -128,9 +129,12 @@
@import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/rooms/_WhoIsTypingTile.scss";
@import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_IntegrationsManager.scss"; @import "./views/settings/_IntegrationsManager.scss";
@import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_KeyBackupPanel.scss";
@import "./views/settings/_Notifications.scss"; @import "./views/settings/_Notifications.scss";
@import "./views/settings/_PhoneNumbers.scss";
@import "./views/settings/_ProfileSettings.scss";
@import "./views/settings/tabs/_GeneralSettingsTab.scss"; @import "./views/settings/tabs/_GeneralSettingsTab.scss";
@import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/voip/_CallView.scss"; @import "./views/voip/_CallView.scss";

View File

@ -255,3 +255,15 @@ input.mx_UserSettings_phoneNumberField {
.mx_UserSettings_analyticsModal table { .mx_UserSettings_analyticsModal table {
margin: 10px 0px; margin: 10px 0px;
} }
// Temp styles to keep the layout moderately usable. Not perfect, but better
// than 30 options being impossible to understand.
.mx_UserSettings .mx_SettingsFlag {
height: 30px;
}
.mx_UserSettings .mx_SettingsFlag .mx_ToggleSwitch {
float: left;
margin-right: 5px;
}

View File

@ -42,3 +42,35 @@ limitations under the License.
color: $button-primary-disabled-fg-color; color: $button-primary-disabled-fg-color;
background-color: $button-primary-disabled-bg-color; background-color: $button-primary-disabled-bg-color;
} }
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_primary_sm {
padding: 5px 12px;
color: $button-primary-fg-color;
background-color: $button-primary-bg-color;
}
.mx_AccessibleButton_kind_primary_sm.mx_AccessibleButton_disabled {
color: $button-primary-disabled-fg-color;
background-color: $button-primary-disabled-bg-color;
}
.mx_AccessibleButton_kind_danger {
color: $button-danger-fg-color;
background-color: $button-danger-bg-color;
}
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
padding: 5px 12px;
color: $button-danger-fg-color;
background-color: $button-danger-bg-color;
}
.mx_AccessibleButton_kind_danger_sm.mx_AccessibleButton_disabled {
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}

View File

@ -64,7 +64,7 @@ limitations under the License.
.mx_Field input:focus + label, .mx_Field input:focus + label,
.mx_Field input:not(:placeholder-shown) + label, .mx_Field input:not(:placeholder-shown) + label,
.mx_Field select:focus + label { .mx_Field select + label /* Always show a select's label on top to not collide with the value */ {
transition: transition:
font-size 0.25s ease-out 0s, font-size 0.25s ease-out 0s,
color 0.25s ease-out 0s, color 0.25s ease-out 0s,

View File

@ -0,0 +1,51 @@
/*
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.
*/
// TODO: Fancy transitions
.mx_ToggleSwitch {
width: 48px;
height: 24px;
border-radius: 14px;
background-color: $togglesw-off-color;
position: relative;
}
.mx_ToggleSwitch_enabled {
cursor: pointer;
}
.mx_ToggleSwitch.mx_ToggleSwitch_on {
background-color: $togglesw-on-color;
}
.mx_ToggleSwitch_ball {
margin: 2px;
width: 20px;
height: 20px;
border-radius: 20px;
background-color: $togglesw-ball-color;
position: absolute;
top: 0;
}
.mx_ToggleSwitch:not(.mx_ToggleSwitch_on) > .mx_ToggleSwitch_ball {
left: 2px;
}
.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball {
right: 2px;
}

View File

@ -18,6 +18,5 @@ limitations under the License.
height: 200px; height: 200px;
border: 1px solid $primary-hairline-color; border: 1px solid $primary-hairline-color;
border-radius: 3px; border-radius: 3px;
margin-right: 32px;
overflow: hidden; overflow: hidden;
} }

View File

@ -0,0 +1,43 @@
/*
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_ExistingEmailAddress {
margin-bottom: 5px;
}
.mx_ExistingEmailAddress_delete {
margin-right: 5px;
cursor: pointer;
vertical-align: middle;
}
.mx_ExistingEmailAddress_email {
vertical-align: middle;
}
.mx_ExistingEmailAddress_promptText {
margin-right: 10px;
}
.mx_ExistingEmailAddress_confirmBtn {
margin-right: 5px;
}
.mx_EmailAddresses_new .mx_Field input {
// Use 100% of the space available for the input, but don't let the 10px
// padding on either side of the input to push it out of alignment.
width: calc(100% - 20px);
}

View File

@ -0,0 +1,56 @@
/*
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_ExistingPhoneNumber {
margin-bottom: 5px;
}
.mx_ExistingPhoneNumber_delete {
margin-right: 5px;
cursor: pointer;
vertical-align: middle;
}
.mx_ExistingPhoneNumber_address {
vertical-align: middle;
}
.mx_ExistingPhoneNumber_promptText {
margin-right: 10px;
}
.mx_ExistingPhoneNumber_confirmBtn {
margin-right: 5px;
}
.mx_PhoneNumbers_new .mx_Field input {
// Use 100% of the space available for the input, but don't let the 10px
// padding on either side of the input to push it out of alignment.
width: calc(100% - 20px);
}
.mx_PhoneNumbers_input {
display: flex;
align-items: center;
}
.mx_PhoneNumbers_input > .mx_Field {
flex-grow: 1;
}
.mx_PhoneNumbers_country {
width: 80px;
}

View File

@ -0,0 +1,107 @@
/*
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_ProfileSettings_profile {
display: flex;
}
.mx_ProfileSettings_controls {
flex-grow: 1;
}
.mx_ProfileSettings_controls .mx_Field #profileDisplayName {
width: calc(100% - 20px); // subtract 10px padding on left and right
}
.mx_ProfileSettings_avatar {
width: 88px;
height: 88px;
margin-left: 13px;
position: relative;
cursor: pointer;
}
.mx_ProfileSettings_avatar > * {
display: block;
width: 88px;
height: 88px;
border-radius: 4px;
}
.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarPlaceholder {
background-color: $settings-profile-placeholder-bg-color;
}
.mx_ProfileSettings_avatarOverlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: none;
text-align: center;
vertical-align: middle;
font-size: 10px;
}
.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlay {
display: inline-block;
opacity: 0.5 !important;
color: $settings-profile-overlay-fg-color !important;
background-color: $settings-profile-overlay-bg-color !important;
}
.mx_ProfileSettings_avatarOverlay_show {
display: inline-block;
opacity: 1;
color: $settings-profile-overlay-placeholder-fg-color;
background-color: $settings-profile-overlay-placeholder-bg-color;
}
.mx_ProfileSettings_avatarOverlayText {
display: block;
margin-top: 17px;
margin-bottom: 8px;
}
.mx_ProfileSettings_avatarOverlayImgContainer {
position: relative;
width: 14px;
height: 14px;
margin: auto;
}
.mx_ProfileSettings_avatarOverlayImg:before {
background-color: $settings-profile-overlay-placeholder-fg-color;
mask: url("$(res)/img/feather-icons/upload.svg");
mask-repeat: no-repeat;
mask-size: 14px;
mask-position: center;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlayImg:before {
background-color: $settings-profile-overlay-fg-color !important;
}
.mx_ProfileSettings_avatarUpload {
display: none;
}

View File

@ -1,25 +1,46 @@
.mx_GeneralSettingsTab_profile { /*
display: flex; 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_GeneralSettingsTab_changePassword,
.mx_GeneralSettingsTab_themeSection {
display: block;
} }
.mx_GeneralSettingsTab_profileControls { .mx_GeneralSettingsTab_changePassword .mx_Field,
flex-grow: 1; .mx_GeneralSettingsTab_themeSection .mx_Field {
display: block;
margin-right: 100px; // Align with the other fields on the page
} }
.mx_GeneralSettingsTab_profileControls .mx_Field #profileDisplayName { .mx_GeneralSettingsTab_changePassword .mx_Field input {
display: block;
width: calc(100% - 20px); // subtract 10px padding on left and right width: calc(100% - 20px); // subtract 10px padding on left and right
} }
.mx_GeneralSettingsTab_profileAvatar { .mx_GeneralSettingsTab_changePassword .mx_Field:first-child {
width: 88px; margin-top: 0;
height: 88px;
margin-left: 13px;
} }
.mx_GeneralSettingsTab_profileAvatar div { .mx_GeneralSettingsTab_themeSection .mx_Field select {
display: block; display: block;
width: 88px; width: 100%;
height: 88px; }
border-radius: 4px;
background-color: #ccc; .mx_GeneralSettingsTab_accountSection > .mx_EmailAddresses,
.mx_GeneralSettingsTab_accountSection > .mx_PhoneNumbers,
.mx_GeneralSettingsTab_languageInput {
margin-right: 100px; // Align with the other fields on the page
} }

View File

@ -1,3 +1,19 @@
/*
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_SettingsTab_heading { .mx_SettingsTab_heading {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
@ -10,8 +26,30 @@
font-family: $font-family-semibold; font-family: $font-family-semibold;
color: $primary-fg-color; color: $primary-fg-color;
margin-bottom: 10px; margin-bottom: 10px;
}
.mx_SettingsTab_section {
margin-top: 10px; margin-top: 10px;
} }
.mx_SettingsTab_subsectionText {
color: $settings-subsection-fg-color;
font-size: 12px;
padding-bottom: 12px;
margin: 0;
display: block;
}
.mx_SettingsTab_section .mx_SettingsFlag {
margin-right: 100px;
height: 25px;
margin-bottom: 10px;
}
.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_label {
vertical-align: bottom;
display: inline-block;
font-size: 12px;
color: $primary-fg-color;
}
.mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch {
float: right;
}

View File

@ -0,0 +1,5 @@
<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" stroke-width="1.2">
<path d="M13 9v2.667c0 .736-.597 1.333-1.333 1.333H2.333A1.333 1.333 0 0 1 1 11.667V9M10.667 4.333L7.333 1 4 4.333M7.333 1v8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -120,6 +120,12 @@ $blockquote-bar-color: #ddd;
$blockquote-fg-color: #777; $blockquote-fg-color: #777;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7;
$settings-profile-overlay-bg-color: #000;
$settings-profile-overlay-placeholder-bg-color: transparent;
$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #454545;
$settings-subsection-fg-color: #61708b;
$voip-decline-color: #f48080; $voip-decline-color: #f48080;
$voip-accept-color: #80f480; $voip-accept-color: #80f480;
@ -205,6 +211,15 @@ $button-primary-fg-color: #ffffff;
$button-primary-bg-color: #7ac9a1; $button-primary-bg-color: #7ac9a1;
$button-primary-disabled-fg-color: #ffffff; $button-primary-disabled-fg-color: #ffffff;
$button-primary-disabled-bg-color: #bce4d0; $button-primary-disabled-bg-color: #bce4d0;
$button-danger-fg-color: #ffffff;
$button-danger-bg-color: #f56679;
$button-danger-disabled-fg-color: #ffffff;
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
// Toggle switch
$togglesw-off-color: #c1c9d6;
$togglesw-on-color: #7ac9a1;
$togglesw-ball-color: #fff;
// unused? // unused?
$progressbar-color: #000; $progressbar-color: #000;

View File

@ -113,6 +113,12 @@ $blockquote-bar-color: #ddd;
$blockquote-fg-color: #777; $blockquote-fg-color: #777;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7;
$settings-profile-overlay-bg-color: #000;
$settings-profile-overlay-placeholder-bg-color: transparent;
$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #454545;
$settings-subsection-fg-color: #61708b;
$voip-decline-color: #f48080; $voip-decline-color: #f48080;
$voip-accept-color: #80f480; $voip-accept-color: #80f480;
@ -201,6 +207,15 @@ $button-primary-fg-color: #ffffff;
$button-primary-bg-color: #7ac9a1; $button-primary-bg-color: #7ac9a1;
$button-primary-disabled-fg-color: #ffffff; $button-primary-disabled-fg-color: #ffffff;
$button-primary-disabled-bg-color: #bce4d0; $button-primary-disabled-bg-color: #bce4d0;
$button-danger-fg-color: #ffffff;
$button-danger-bg-color: #f56679;
$button-danger-disabled-fg-color: #ffffff;
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
// Toggle switch
$togglesw-off-color: #c1c9d6;
$togglesw-on-color: #7ac9a1;
$togglesw-ball-color: #fff;
// unused? // unused?
$progressbar-color: #000; $progressbar-color: #000;

View File

@ -26,7 +26,7 @@ import { _t } from './languageHandler';
* the client owns the given email address, which is then passed to the * the client owns the given email address, which is then passed to the
* add threepid API on the homeserver. * add threepid API on the homeserver.
*/ */
class AddThreepid { export default class AddThreepid {
constructor() { constructor() {
this.clientSecret = MatrixClientPeg.get().generateClientSecret(); this.clientSecret = MatrixClientPeg.get().generateClientSecret();
} }
@ -124,5 +124,3 @@ class AddThreepid {
}); });
} }
} }
module.exports = AddThreepid;

View File

@ -637,11 +637,14 @@ module.exports = React.createClass({
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = (e) => const onChange = (e) =>
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
// HACK: Lack of translations for themes header. We're removing this view in the very near future,
// and the header is really only there to maintain some semblance of the UX the section once was.
return ( return (
<div> <div>
<h3>{ _t("User Interface") }</h3> <h3>{ _t("User Interface") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ SIMPLE_SETTINGS.map( this._renderAccountSetting ) } { SIMPLE_SETTINGS.map( this._renderAccountSetting ) }
<div><b>Themes</b></div>
{ THEMES.map( this._renderThemeOption ) } { THEMES.map( this._renderThemeOption ) }
<table> <table>
<tbody> <tbody>
@ -676,18 +679,12 @@ module.exports = React.createClass({
}, },
_renderThemeOption: function(setting) { _renderThemeOption: function(setting) {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); // HACK: Temporary disablement of theme selection.
const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value}); // We don't support changing themes on experimental anyways, and radio groups aren't
return ( // a thing anymore for setting flags. We're also dropping this view in the very near
<div className="mx_UserSettings_toggle" key={setting.id + '_' + setting.value}> // future, so just replace the theme selection with placeholder text.
<SettingsFlag name="theme" const currentTheme = SettingsStore.getValue("theme");
label={setting.label} return <div>{_t(setting.label)} {currentTheme === setting.value ? '(current)' : null}</div>;
level={SettingLevel.ACCOUNT}
onChange={onChange}
group="theme"
value={setting.value} />
</div>
);
}, },
_renderCryptoInfo: function() { _renderCryptoInfo: function() {
@ -1268,9 +1265,7 @@ module.exports = React.createClass({
<ChangePassword <ChangePassword
className="mx_UserSettings_accountTable" className="mx_UserSettings_accountTable"
rowClassName="mx_UserSettings_profileTableRow" rowClassName="mx_UserSettings_profileTableRow"
rowLabelClassName="mx_UserSettings_profileLabelCell" buttonClassName="mx_UserSettings_button"
rowInputClassName="mx_UserSettings_profileInputCell"
buttonClassName="mx_UserSettings_button mx_UserSettings_changePasswordButton"
onError={this.onPasswordChangeError} onError={this.onPasswordChangeError}
onFinished={this.onPasswordChanged} /> onFinished={this.onPasswordChanged} />
); );

View File

@ -116,9 +116,8 @@ export default React.createClass({
<ChangePassword <ChangePassword
className="mx_SetPasswordDialog_change_password" className="mx_SetPasswordDialog_change_password"
rowClassName="" rowClassName=""
rowLabelClassName="" buttonClassNames="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
rowInputClassName="" buttonKind="primary"
buttonClassName="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
confirm={false} confirm={false}
autoFocusNewPasswordInput={true} autoFocusNewPasswordInput={true}
shouldAskForEmail={true} shouldAskForEmail={true}

View File

@ -31,6 +31,18 @@ export default class Field extends React.PureComponent {
// To define options for a select, use <Field><option ... /></Field> // To define options for a select, use <Field><option ... /></Field>
element: PropTypes.string, element: PropTypes.string,
// All other props pass through to the <input>. // All other props pass through to the <input>.
};
get value() {
if (!this.refs.fieldInput) return null;
return this.refs.fieldInput.value;
}
set value(newValue) {
if (!this.refs.fieldInput) {
throw new Error("No field input reference");
}
this.refs.fieldInput.value = newValue;
} }
render() { render() {
@ -42,6 +54,7 @@ export default class Field extends React.PureComponent {
// Set some defaults for the element // Set some defaults for the element
extraProps.type = extraProps.type || "text"; extraProps.type = extraProps.type || "text";
extraProps.ref = "fieldInput";
const element = this.props.element || "input"; const element = this.props.element || "input";
const fieldInput = React.createElement(element, extraProps, this.props.children); const fieldInput = React.createElement(element, extraProps, this.props.children);

View File

@ -18,6 +18,7 @@ import React from "react";
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import ToggleSwitch from "./ToggleSwitch";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'SettingsFlag', displayName: 'SettingsFlag',
@ -29,10 +30,6 @@ module.exports = React.createClass({
onChange: PropTypes.func, onChange: PropTypes.func,
isExplicit: PropTypes.bool, isExplicit: PropTypes.bool,
manualSave: PropTypes.bool, manualSave: PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: PropTypes.string,
value: PropTypes.any, // the value for the radio button
}, },
getInitialState: function() { getInitialState: function() {
@ -46,13 +43,12 @@ module.exports = React.createClass({
}; };
}, },
onChange: function(e) { onChange: function(checked) {
if (this.props.group && !e.target.checked) return; if (this.props.group && !checked) return;
const newState = this.props.group ? this.props.value : e.target.checked; if (!this.props.manualSave) this.save(checked);
if (!this.props.manualSave) this.save(newState); else this.setState({ value: checked });
else this.setState({ value: newState }); if (this.props.onChange) this.props.onChange(checked);
if (this.props.onChange) this.props.onChange(newState);
}, },
save: function(val = undefined) { save: function(val = undefined) {
@ -78,34 +74,11 @@ module.exports = React.createClass({
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level); if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
else label = _t(label); else label = _t(label);
// We generate a relatively complex ID to avoid conflicts
const id = this.props.name + "_" + this.props.group + "_" + this.props.value + "_" + this.props.level;
let checkbox = (
<input id={id}
type="checkbox"
defaultChecked={value}
onChange={this.onChange}
disabled={!canChange}
/>
);
if (this.props.group) {
checkbox = (
<input id={id}
type="radio"
name={this.props.group}
value={this.props.value}
checked={value === this.props.value}
onChange={this.onChange}
disabled={!canChange}
/>
);
}
return ( return (
<label> <div className="mx_SettingsFlag">
{ checkbox } <span className="mx_SettingsFlag_label">{label}</span>
{ label } <ToggleSwitch checked={value} onChange={this.onChange} disabled={!canChange} />
</label> </div>
); );
}, },
}); });

View File

@ -0,0 +1,66 @@
/*
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 classNames from "classnames";
export default class ToggleSwitch extends React.Component {
static propTypes = {
// Whether or not this toggle is in the 'on' position. Default false (off).
checked: PropTypes.bool,
// Whether or not the user can interact with the switch
disabled: PropTypes.bool,
// Called when the checked state changes. First argument will be the new state.
onChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
checked: props.checked || false, // default false
};
}
_onClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.disabled) return;
const newState = !this.state.checked;
this.setState({checked: newState});
if (this.props.onChange) {
this.props.onChange(newState);
}
};
render() {
const classes = classNames({
"mx_ToggleSwitch": true,
"mx_ToggleSwitch_on": this.state.checked,
"mx_ToggleSwitch_enabled": !this.props.disabled,
});
return (
<div className={classes} onClick={this._onClick}>
<div className="mx_ToggleSwitch_ball" />
</div>
);
}
}

View File

@ -68,15 +68,12 @@ export default React.createClass({
text = _t("You're not currently a member of any communities."); text = _t("You're not currently a member of any communities.");
} }
return <div> return (
<h3>{ _t('Flair') }</h3> <div>
<div className="mx_UserSettings_section"> <p className="mx_SettingsTab_subsectionText">{ text }</p>
<p>
{ text }
</p>
{ scrollbox } { scrollbox }
</div> </div>
</div>; );
}, },
render() { render() {

View File

@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018-2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import Field from "../elements/Field";
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const MatrixClientPeg = require("../../../MatrixClientPeg"); const MatrixClientPeg = require("../../../MatrixClientPeg");
@ -35,9 +37,8 @@ module.exports = React.createClass({
onError: PropTypes.func, onError: PropTypes.func,
onCheckPassword: PropTypes.func, onCheckPassword: PropTypes.func,
rowClassName: PropTypes.string, rowClassName: PropTypes.string,
rowLabelClassName: PropTypes.string,
rowInputClassName: PropTypes.string,
buttonClassName: PropTypes.string, buttonClassName: PropTypes.string,
buttonKind: PropTypes.string,
confirm: PropTypes.bool, confirm: PropTypes.bool,
// Whether to autoFocus the new password input // Whether to autoFocus the new password input
autoFocusNewPasswordInput: PropTypes.bool, autoFocusNewPasswordInput: PropTypes.bool,
@ -203,21 +204,18 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
// TODO: Live validation on `new pw == confirm pw`
const rowClassName = this.props.rowClassName; const rowClassName = this.props.rowClassName;
const rowLabelClassName = this.props.rowLabelClassName;
const rowInputClassName = this.props.rowInputClassName;
const buttonClassName = this.props.buttonClassName; const buttonClassName = this.props.buttonClassName;
let currentPassword = null; let currentPassword = null;
if (!this.state.cachedPassword) { if (!this.state.cachedPassword) {
currentPassword = <div className={rowClassName}> currentPassword = (
<div className={rowLabelClassName}> <div className={rowClassName}>
<label htmlFor="passwordold">{ _t('Current password') }</label> <Field id="passwordold" type="password" ref="old_input" label={_t('Current password')} />
</div> </div>
<div className={rowInputClassName}> );
<input id="passwordold" type="password" ref="old_input" />
</div>
</div>;
} }
switch (this.state.phase) { switch (this.state.phase) {
@ -228,24 +226,13 @@ module.exports = React.createClass({
<form className={this.props.className} onSubmit={this.onClickChange}> <form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword } { currentPassword }
<div className={rowClassName}> <div className={rowClassName}>
<div className={rowLabelClassName}> <Field id="password1" type="password" ref="new_input" label={passwordLabel}
<label htmlFor="password1">{ passwordLabel }</label> autoFocus={this.props.autoFocusNewPasswordInput} />
</div>
<div className={rowInputClassName}>
<input id="password1" type="password" ref="new_input" autoFocus={this.props.autoFocusNewPasswordInput} />
</div>
</div> </div>
<div className={rowClassName}> <div className={rowClassName}>
<div className={rowLabelClassName}> <Field id="password2" type="password" ref="confirm_input" label={_t("Confirm password")} />
<label htmlFor="password2">{ _t('Confirm password') }</label>
</div>
<div className={rowInputClassName}>
<input id="password2" type="password" ref="confirm_input" />
</div>
</div> </div>
<AccessibleButton className={buttonClassName} <AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
onClick={this.onClickChange}
element="button">
{ _t('Change Password') } { _t('Change Password') }
</AccessibleButton> </AccessibleButton>
</form> </form>

View File

@ -0,0 +1,231 @@
/*
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 {_t} from "../../../languageHandler";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import * as Email from "../../../email";
import AddThreepid from "../../../AddThreepid";
const sdk = require('../../../index');
const Modal = require("../../../Modal");
/*
TODO: Improve the UX for everything in here.
It's very much placeholder, but it gets the job done. The old way of handling
email addresses in user settings was to use dialogs to communicate state, however
due to our dialog system overriding dialogs (causing unmounts) this creates problems
for a sane UX. For instance, the user could easily end up entering an email address
and receive a dialog to verify the address, which then causes the component here
to forget what it was doing and ultimately fail. Dialogs are still used in some
places to communicate errors - these should be replaced with inline validation when
that is available.
*/
export class ExistingEmailAddress extends React.Component {
static propTypes = {
email: PropTypes.object.isRequired,
onRemoved: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
}
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
};
_onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
return this.props.onRemoved(this.props.email);
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
render() {
if (this.state.verifyRemove) {
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_promptText">
{_t("Are you sure?")}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("Yes")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("No")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_ExistingEmailAddress">
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_ExistingEmailAddress_delete" alt={_t("Remove")} />
<span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
</div>
);
}
}
export default class EmailAddresses extends React.Component {
constructor() {
super();
this.state = {
emails: [],
verifying: false,
addTask: null,
continueDisabled: false,
};
}
componentWillMount(): void {
const client = MatrixClientPeg.get();
client.getThreePids().then((addresses) => {
this.setState({emails: addresses.threepids.filter((a) => a.medium === 'email')});
});
}
_onRemoved = (address) => {
this.setState({emails: this.state.emails.filter((e) => e !== address)});
};
_onAddClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.refs.newEmailAddress) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const email = this.refs.newEmailAddress.value;
// TODO: Inline field validation
if (!Email.looksValid(email)) {
Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
});
return;
}
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
task.addEmailAddress(email, true).then(() => {
this.setState({continueDisabled: false});
}).catch((err) => {
console.error("Unable to add email address " + email + " " + err);
this.setState({verifying: false, continueDisabled: false, addTask: null});
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
_onContinueClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({continueDisabled: true});
this.state.addTask.checkEmailLinkClicked().then(() => {
const email = this.refs.newEmailAddress.value;
this.setState({
emails: [...this.state.emails, {address: email, medium: "email"}],
addTask: null,
continueDisabled: false,
verifying: false,
});
this.refs.newEmailAddress.value = "";
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
};
render() {
const existingEmailElements = this.state.emails.map((e) => {
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
});
let addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
</AccessibleButton>
);
if (this.state.verifying) {
addButton = (
<div>
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
{_t("Continue")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_EmailAddresses">
{existingEmailElements}
<form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_EmailAddresses_new">
<Field id="newEmailAddress" ref="newEmailAddress" label={_t("Email Address")}
type="text" autoComplete="off" disabled={this.state.verifying} />
{addButton}
</form>
</div>
);
}
}

View File

@ -0,0 +1,247 @@
/*
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 {_t} from "../../../languageHandler";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import AddThreepid from "../../../AddThreepid";
import CountryDropdown from "../auth/CountryDropdown";
const sdk = require('../../../index');
const Modal = require("../../../Modal");
/*
TODO: Improve the UX for everything in here.
This is a copy/paste of EmailAddresses, mostly.
*/
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
export class ExistingPhoneNumber extends React.Component {
static propTypes = {
msisdn: PropTypes.object.isRequired,
onRemoved: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
}
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
};
_onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
return this.props.onRemoved(this.props.msisdn);
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
render() {
if (this.state.verifyRemove) {
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_promptText">
{_t("Are you sure?")}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
{_t("Yes")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
{_t("No")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_ExistingPhoneNumber">
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_ExistingPhoneNumber_delete" alt={_t("Remove")} />
<span className="mx_ExistingPhoneNumber_address">+{this.props.msisdn.address}</span>
</div>
);
}
}
export default class PhoneNumbers extends React.Component {
constructor() {
super();
this.state = {
msisdns: [],
verifying: false,
verifyError: false,
verifyMsisdn: "",
addTask: null,
continueDisabled: false,
phoneCountry: "",
};
}
componentWillMount(): void {
const client = MatrixClientPeg.get();
client.getThreePids().then((addresses) => {
this.setState({msisdns: addresses.threepids.filter((a) => a.medium === 'msisdn')});
});
}
_onRemoved = (address) => {
this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)});
};
_onAddClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.refs.newPhoneNumber) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const phoneNumber = this.refs.newPhoneNumber.value;
const phoneCountry = this.state.phoneCountry;
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
task.addMsisdn(phoneCountry, phoneNumber, true).then((response) => {
this.setState({continueDisabled: false, verifyMsisdn: response.msisdn});
}).catch((err) => {
console.error("Unable to add phone number " + phoneNumber + " " + err);
this.setState({verifying: false, continueDisabled: false, addTask: null});
Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, {
title: _t("Error"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
_onContinueClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({continueDisabled: true});
const token = this.refs.newPhoneNumberCode.value;
this.state.addTask.haveMsisdnToken(token).then(() => {
this.setState({
msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}],
addTask: null,
continueDisabled: false,
verifying: false,
verifyMsisdn: "",
verifyError: null,
});
this.refs.newPhoneNumber.value = "";
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify phone number: " + err);
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
title: _t("Unable to verify phone number."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
} else {
this.setState({verifyError: _t("Incorrect verification code")});
}
});
};
_onCountryChanged = (e) => {
this.setState({phoneCountry: e.iso2});
};
render() {
const existingPhoneElements = this.state.msisdns.map((p) => {
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />;
});
let addVerifySection = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
</AccessibleButton>
);
if (this.state.verifying) {
const msisdn = this.state.verifyMsisdn;
addVerifySection = (
<div>
<div>
{_t("A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains", {msisdn: msisdn})}
<br />
{this.state.verifyError}
</div>
<form onSubmit={this._onContinueClick} autoComplete={false} noValidate={true}>
<Field id="newPhoneNumberCode" ref="newPhoneNumberCode" label={_t("Verification code")}
type="text" autoComplete="off" disabled={this.state.continueDisabled} />
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
{_t("Continue")}
</AccessibleButton>
</form>
</div>
);
}
return (
<div className="mx_PhoneNumbers">
{existingPhoneElements}
<form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_PhoneNumbers_new">
<div className="mx_PhoneNumbers_input">
<CountryDropdown onOptionChange={this._onCountryChanged}
className="mx_PhoneNumbers_country"
value={this.state.phoneCountry}
disabled={this.state.verifying}
isSmall={true}
/>
<Field id="newPhoneNumber" ref="newPhoneNumber" label={_t("Phone Number")}
type="text" autoComplete="off" disabled={this.state.verifying} />
</div>
{addVerifySection}
</form>
</div>
);
}
}

View File

@ -0,0 +1,155 @@
/*
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";
import classNames from 'classnames';
export default class ProfileSettings extends React.Component {
constructor() {
super();
const client = MatrixClientPeg.get();
const user = client.getUser(client.getUserId());
let avatarUrl = user.avatarUrl;
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
this.state = {
userId: user.userId,
originalDisplayName: user.displayName,
displayName: user.displayName,
originalAvatarUrl: avatarUrl,
avatarUrl: avatarUrl,
avatarFile: null,
enableProfileSave: false,
};
}
_uploadAvatar = (e) => {
e.stopPropagation();
e.preventDefault();
this.refs.avatarUpload.click();
};
_saveProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this.setState({enableProfileSave: false});
const client = MatrixClientPeg.get();
const newState = {};
// TODO: What do we do about errors?
if (this.state.originalDisplayName !== this.state.displayName) {
await client.setDisplayName(this.state.displayName);
newState.originalDisplayName = this.state.displayName;
}
if (this.state.avatarFile) {
const uri = await client.uploadContent(this.state.avatarFile);
await client.setAvatarUrl(uri);
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
newState.originalAvatarUrl = newState.avatarUrl;
newState.avatarFile = null;
}
newState.enableProfileSave = true;
this.setState(newState);
};
_onDisplayNameChanged = (e) => {
this.setState({
displayName: e.target.value,
enableProfileSave: true,
});
};
_onAvatarChanged = (e) => {
if (!e.target.files || !e.target.files.length) {
this.setState({
avatarUrl: this.state.originalAvatarUrl,
avatarFile: null,
enableProfileSave: false,
});
return;
}
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev) => {
this.setState({
avatarUrl: ev.target.result,
avatarFile: file,
enableProfileSave: true,
});
};
reader.readAsDataURL(file);
};
render() {
// TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced?
let showOverlayAnyways = true;
let avatarElement = <div className="mx_ProfileSettings_avatarPlaceholder" />;
if (this.state.avatarUrl) {
showOverlayAnyways = false;
avatarElement = <img src={this.state.avatarUrl}
alt={_t("Profile picture")} />;
}
const avatarOverlayClasses = classNames({
"mx_ProfileSettings_avatarOverlay": true,
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
});
const avatarHoverElement = (
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload profile picture")}</span>
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
<div className="mx_ProfileSettings_avatarOverlayImg" />
</div>
</div>
);
return (
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}>
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<p>{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_ProfileSettings_avatar">
{avatarElement}
{avatarHoverElement}
</div>
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
</form>
);
}
}

View File

@ -17,69 +17,176 @@ limitations under the License.
import React from 'react'; import React from 'react';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../MatrixClientPeg";
import GroupUserSettings from "../../groups/GroupUserSettings";
import PropTypes from "prop-types";
import {MatrixClient} from "matrix-js-sdk";
import { DragDropContext } from 'react-beautiful-dnd';
import ProfileSettings from "../ProfileSettings";
import EmailAddresses from "../EmailAddresses";
import PhoneNumbers from "../PhoneNumbers";
import Field from "../../elements/Field"; import Field from "../../elements/Field";
import * as languageHandler from "../../../../languageHandler";
import {SettingLevel} from "../../../../settings/SettingsStore";
import SettingsStore from "../../../../settings/SettingsStore";
import LanguageDropdown from "../../elements/LanguageDropdown";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../dialogs/DeactivateAccountDialog";
const PlatformPeg = require("../../../../PlatformPeg");
const sdk = require('../../../../index');
const Modal = require("../../../../Modal");
const dis = require("../../../../dispatcher");
export default class GeneralSettingsTab extends React.Component { export default class GeneralSettingsTab extends React.Component {
static childContextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor() { constructor() {
super(); super();
const client = MatrixClientPeg.get();
this.state = { this.state = {
userId: client.getUserId(), language: languageHandler.getCurrentLanguage(),
displayName: client.getUser(client.getUserId()).displayName, theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
enableProfileSave: false,
}; };
} }
_saveProfile = async (e) => { getChildContext() {
e.stopPropagation(); return {
e.preventDefault(); matrixClient: MatrixClientPeg.get(),
};
}
if (!this.state.enableProfileSave) return; _onLanguageChange = (newLanguage) => {
this.setState({enableProfileSave: false}); if (this.state.language === newLanguage) return;
// TODO: What do we do about errors? SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
await MatrixClientPeg.get().setDisplayName(this.state.displayName); this.setState({language: newLanguage});
PlatformPeg.get().reload();
// TODO: Support avatars
this.setState({enableProfileSave: true});
}; };
_onDisplayNameChanged = (e) => { _onThemeChange = (e) => {
this.setState({ const newTheme = e.target.value;
displayName: e.target.value, if (this.state.theme === newTheme) return;
enableProfileSave: true,
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
dis.dispatch({action: 'set_theme', value: newTheme});
};
_onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = _t("Failed to change password. Is your password correct?");
} else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg);
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
title: _t("Error"),
description: errMsg,
}); });
}; };
_renderProfileSection() { _onPasswordChanged = () => {
// TODO: Ditch avatar placeholder and use the real thing // TODO: Figure out a design that doesn't involve replacing the current dialog
const form = ( const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}> Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
<div className="mx_GeneralSettingsTab_profile"> title: _t("Success"),
<div className="mx_GeneralSettingsTab_profileControls"> description: _t(
<p className="mx_GeneralSettingsTab_profileUsername">{this.state.userId}</p> "Your password was successfully changed. You will not receive " +
<Field id="profileDisplayName" label={_t("Display Name")} "push notifications on other devices until you log back in to them",
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>
);
_onDeactivateClicked = () => {
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {});
};
_renderProfileSection() {
// HACK/TODO: Using DragDropContext feels wrong, but we need it.
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span> <span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
{form} <ProfileSettings />
<span className="mx_SettingsTab_subheading">{_t("Flair")}</span>
<DragDropContext>
<GroupUserSettings />
</DragDropContext>
</div>
);
}
_renderAccountSection() {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const passwordChangeForm = (
<ChangePassword
className="mx_GeneralSettingsTab_changePassword"
rowClassName=""
buttonKind="primary"
onError={this._onPasswordChangeError}
onFinished={this._onPasswordChanged} />
);
return (
<div className="mx_SettingsTab_section mx_GeneralSettingsTab_accountSection">
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
<p className="mx_SettingsTab_subsectionText">
{_t("Set a new account password...")}
</p>
{passwordChangeForm}
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers />
</div>
);
}
_renderLanguageSection() {
// TODO: Convert to new-styled Field
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
<LanguageDropdown className="mx_GeneralSettingsTab_languageInput"
onOptionChange={this._onLanguageChange} value={this.state.language} />
</div>
);
}
_renderThemeSection() {
// TODO: Re-enable theme selection once the themes actually work
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return (
<div className="mx_SettingsTab_section mx_GeneralSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select" disabled={true}
value={this.state.theme} onChange={this._onThemeChange}>
<option value="light">{_t("Light theme")}</option>
<option value="dark">{_t("Dark theme")}</option>
<option value="dharma">{_t("2018 theme")}</option>
<option value="status">{_t("Status.im theme")}</option>
</Field>
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
</div>
);
}
_renderManagementSection() {
// TODO: Improve warning text for account deactivation
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Account management")}</span>
<span className="mx_SettingsTab_subsectionText">
{_t("Deactivating your account is a permanent action - be careful!")}
</span>
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
{_t("Close Account")}
</AccessibleButton>
</div> </div>
); );
} }
@ -89,6 +196,10 @@ export default class GeneralSettingsTab extends React.Component {
<div className="mx_SettingsTab"> <div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div> <div className="mx_SettingsTab_heading">{_t("General")}</div>
{this._renderProfileSection()} {this._renderProfileSection()}
{this._renderAccountSection()}
{this._renderLanguageSection()}
{this._renderThemeSection()}
{this._renderManagementSection()}
</div> </div>
); );
} }

View File

@ -352,6 +352,17 @@
"Last seen": "Last seen", "Last seen": "Last seen",
"Select devices": "Select devices", "Select devices": "Select devices",
"Failed to set display name": "Failed to set display name", "Failed to set display name": "Failed to set display name",
"Unable to remove contact information": "Unable to remove contact information",
"Are you sure?": "Are you sure?",
"Yes": "Yes",
"No": "No",
"Remove": "Remove",
"Invalid Email Address": "Invalid Email Address",
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
"Unable to add email address": "Unable to add email address",
"Unable to verify email address.": "Unable to verify email address.",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
"Email Address": "Email Address",
"Disable Notifications": "Disable Notifications", "Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications", "Enable Notifications": "Enable Notifications",
"Delete Backup": "Delete Backup", "Delete Backup": "Delete Backup",
@ -402,9 +413,31 @@
"Off": "Off", "Off": "Off",
"On": "On", "On": "On",
"Noisy": "Noisy", "Noisy": "Noisy",
"Unable to verify phone number.": "Unable to verify phone number.",
"Verification code": "Verification code",
"Phone Number": "Phone Number",
"Profile picture": "Profile picture",
"Upload profile picture": "Upload profile picture",
"Display Name": "Display Name", "Display Name": "Display Name",
"Save": "Save", "Save": "Save",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them",
"Profile": "Profile", "Profile": "Profile",
"Flair": "Flair",
"Account": "Account",
"Set a new account password...": "Set a new account password...",
"Email addresses": "Email addresses",
"Phone numbers": "Phone numbers",
"Language and region": "Language and region",
"Theme": "Theme",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
"2018 theme": "2018 theme",
"Status.im theme": "Status.im theme",
"Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Close Account": "Close Account",
"General": "General", "General": "General",
"Cannot add any more widgets": "Cannot add any more widgets", "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.", "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.",
@ -453,7 +486,6 @@
"Failed to toggle moderator status": "Failed to toggle moderator status", "Failed to toggle moderator status": "Failed to toggle moderator status",
"Failed to change power level": "Failed to change power level", "Failed to change power level": "Failed to change power level",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
"Are you sure?": "Are you sure?",
"No devices with registered encryption keys": "No devices with registered encryption keys", "No devices with registered encryption keys": "No devices with registered encryption keys",
"Devices": "Devices", "Devices": "Devices",
"Unignore": "Unignore", "Unignore": "Unignore",
@ -673,7 +705,6 @@
"New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)", "New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)",
"Invalid community ID": "Invalid community ID", "Invalid community ID": "Invalid community ID",
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID", "'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID",
"Flair": "Flair",
"Showing flair for these communities:": "Showing flair for these communities:", "Showing flair for these communities:": "Showing flair for these communities:",
"This room is not showing flair for any communities": "This room is not showing flair for any communities", "This room is not showing flair for any communities": "This room is not showing flair for any communities",
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)", "New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
@ -727,7 +758,6 @@
"Flair will not appear": "Flair will not appear", "Flair will not appear": "Flair will not appear",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
"Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.",
"Remove": "Remove",
"Failed to remove room from community": "Failed to remove room from community", "Failed to remove room from community": "Failed to remove room from community",
"Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s",
"Something went wrong!": "Something went wrong!", "Something went wrong!": "Something went wrong!",
@ -972,12 +1002,8 @@
"We encountered an error trying to restore your previous session.": "We encountered an error trying to restore your previous session.", "We encountered an error trying to restore your previous session.": "We encountered an error trying to restore your previous session.",
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.", "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.",
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.",
"Invalid Email Address": "Invalid Email Address",
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
"Verification Pending": "Verification Pending", "Verification Pending": "Verification Pending",
"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.", "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", "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.", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
"Skip": "Skip", "Skip": "Skip",
@ -994,7 +1020,6 @@
"You have successfully set a password and an email address!": "You have successfully set a password and an email address!", "You have successfully set a password and an email address!": "You have successfully set a password and an email address!",
"You can now return to your account after signing out, and sign in on other devices.": "You can now return to your account after signing out, and sign in on other devices.", "You can now return to your account after signing out, and sign in on other devices.": "You can now return to your account after signing out, and sign in on other devices.",
"Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.", "Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"(HTTP status %(httpStatus)s)": "(HTTP status %(httpStatus)s)", "(HTTP status %(httpStatus)s)": "(HTTP status %(httpStatus)s)",
"Please set a password!": "Please set a password!", "Please set a password!": "Please set a password!",
"This will allow you to return to your account after signing out, and sign in on other devices.": "This will allow you to return to your account after signing out, and sign in on other devices.", "This will allow you to return to your account after signing out, and sign in on other devices.": "This will allow you to return to your account after signing out, and sign in on other devices.",
@ -1236,17 +1261,10 @@
"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",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
"2018 theme": "2018 theme",
"Status.im theme": "Status.im theme",
"Can't load user settings": "Can't load user settings", "Can't load user settings": "Can't load user settings",
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded", "Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
"Success": "Success",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them",
"Remove Contact Information?": "Remove Contact Information?", "Remove Contact Information?": "Remove Contact Information?",
"Remove %(threePid)s?": "Remove %(threePid)s?", "Remove %(threePid)s?": "Remove %(threePid)s?",
"Unable to remove contact information": "Unable to remove contact information",
"Refer a friend to Riot:": "Refer a friend to Riot:", "Refer a friend to Riot:": "Refer a friend to Riot:",
"Interface Language": "Interface Language", "Interface Language": "Interface Language",
"User Interface": "User Interface", "User Interface": "User Interface",
@ -1292,7 +1310,6 @@
"Email": "Email", "Email": "Email",
"Add email address": "Add email address", "Add email address": "Add email address",
"Display name": "Display name", "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", "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",
"Logged in as:": "Logged in as:", "Logged in as:": "Logged in as:",
"Access Token:": "Access Token:", "Access Token:": "Access Token:",