mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Merge pull request #2491 from matrix-org/travis/usettings/tab/general
Implement the "General" tab of new user settings
This commit is contained in:
commit
2a9f6186d7
@ -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";
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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,
|
||||||
|
51
res/css/views/elements/_ToggleSwitch.scss
Normal file
51
res/css/views/elements/_ToggleSwitch.scss
Normal 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;
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
43
res/css/views/settings/_EmailAddresses.scss
Normal file
43
res/css/views/settings/_EmailAddresses.scss
Normal 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);
|
||||||
|
}
|
56
res/css/views/settings/_PhoneNumbers.scss
Normal file
56
res/css/views/settings/_PhoneNumbers.scss
Normal 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;
|
||||||
|
}
|
107
res/css/views/settings/_ProfileSettings.scss
Normal file
107
res/css/views/settings/_ProfileSettings.scss
Normal 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;
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
5
res/img/feather-icons/upload.svg
Normal file
5
res/img/feather-icons/upload.svg
Normal 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 |
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
|
||||||
|
@ -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} />
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
66
src/components/views/elements/ToggleSwitch.js
Normal file
66
src/components/views/elements/ToggleSwitch.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
<div className={rowInputClassName}>
|
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
|
||||||
<input id="password2" type="password" ref="confirm_input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AccessibleButton className={buttonClassName}
|
|
||||||
onClick={this.onClickChange}
|
|
||||||
element="button">
|
|
||||||
{ _t('Change Password') }
|
{ _t('Change Password') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
|
231
src/components/views/settings/EmailAddresses.js
Normal file
231
src/components/views/settings/EmailAddresses.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
247
src/components/views/settings/PhoneNumbers.js
Normal file
247
src/components/views/settings/PhoneNumbers.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
155
src/components/views/settings/ProfileSettings.js
Normal file
155
src/components/views/settings/ProfileSettings.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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:",
|
||||||
|
Loading…
Reference in New Issue
Block a user