diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 4376439012..2ccf83489c 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -1,7 +1,6 @@ # autogenerated file: run scripts/generate-eslint-error-ignore-file to update. src/component-index.js -src/components/structures/auth/ForgotPassword.js src/components/structures/BottomLeftMenu.js src/components/structures/CreateRoom.js src/components/structures/MessagePanel.js @@ -13,11 +12,6 @@ src/components/structures/ScrollPanel.js src/components/structures/SearchBox.js src/components/structures/TimelinePanel.js src/components/structures/UploadBar.js -src/components/views/auth/CountryDropdown.js -src/components/views/auth/InteractiveAuthEntryComponents.js -src/components/views/auth/PasswordLogin.js -src/components/views/auth/RegistrationForm.js -src/components/views/auth/ServerConfig.js src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js @@ -68,7 +62,6 @@ src/HtmlUtils.js src/ImageUtils.js src/languageHandler.js src/linkify-matrix.js -src/Login.js src/Markdown.js src/MatrixClientPeg.js src/Modal.js @@ -96,11 +89,9 @@ src/VectorConferenceHandler.js src/Velociraptor.js src/WhoIsTyping.js src/wrappers/withMatrixClient.js -test/components/structures/auth/Registration-test.js test/components/structures/MessagePanel-test.js test/components/structures/ScrollPanel-test.js test/components/structures/TimelinePanel-test.js -test/components/views/auth/RegistrationForm-test.js test/components/views/dialogs/InteractiveAuthDialog-test.js test/components/views/rooms/MessageComposerInput-test.js test/components/views/rooms/RoomSettings-test.js diff --git a/package.json b/package.json index aca3d433ad..290adfef09 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "start:init": "babel src -d lib --source-maps --copy-files", "lint": "eslint src/", "lintall": "eslint src/ test/", - "lintwithexclusions": "eslint --max-warnings 13 --ignore-path .eslintignore.errorfiles src test", + "lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", "clean": "rimraf lib", "prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt", "test": "karma start --single-run=true --browsers ChromeHeadless", diff --git a/res/css/_components.scss b/res/css/_components.scss index 7aa10448b2..0603296ef5 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -31,6 +31,7 @@ @import "./views/auth/_AuthHeaderLogo.scss"; @import "./views/auth/_AuthPage.scss"; @import "./views/auth/_InteractiveAuthEntryComponents.scss"; +@import "./views/auth/_LanguageSelector.scss"; @import "./views/auth/_ServerConfig.scss"; @import "./views/avatars/_BaseAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @@ -51,10 +52,12 @@ @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_RestoreKeyBackupDialog.scss"; +@import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss"; +@import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; @@ -80,6 +83,7 @@ @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToolTipButton.scss"; @import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @@ -128,11 +132,18 @@ @import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_DevicesPanel.scss"; +@import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_IntegrationsManager.scss"; @import "./views/settings/_KeyBackupPanel.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/_HelpSettingsTab.scss"; +@import "./views/settings/tabs/_PreferencesSettingsTab.scss"; +@import "./views/settings/tabs/_SecuritySettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; +@import "./views/settings/tabs/_VoiceSettingsTab.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_IncomingCallbox.scss"; @import "./views/voip/_VideoView.scss"; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 0f4b67ad71..d1a448711a 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -24,7 +24,8 @@ limitations under the License. } .mx_TabbedView_tabLabels { - width: 136px; + width: 150px; + max-width: 150px; height: 100%; color: $tab-label-fg-color; } @@ -36,7 +37,7 @@ limitations under the License. border-radius: 3px; font-size: 12px; font-weight: 600; - height: 20px; + min-height: 20px; // use min-height instead of height to allow the label to overflow a bit margin-bottom: 6px; position: relative; } @@ -65,7 +66,7 @@ limitations under the License. mask-repeat: no-repeat; mask-size: 14px; width: 14px; - height: 14px; + height: 15px; mask-position: center; content: ''; vertical-align: middle; diff --git a/res/css/structures/_UserSettings.scss b/res/css/structures/_UserSettings.scss index 74d8c2c718..b078a4e242 100644 --- a/res/css/structures/_UserSettings.scss +++ b/res/css/structures/_UserSettings.scss @@ -255,3 +255,15 @@ input.mx_UserSettings_phoneNumberField { .mx_UserSettings_analyticsModal table { 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; +} \ No newline at end of file diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index c4eebc48a1..c32f70a696 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -69,43 +69,12 @@ limitations under the License. margin-right: 10px; } -.mx_Login_create { - display: block; - text-align: center; - width: 100%; - font-size: 13px; - opacity: 0.8; -} - -.mx_Login_create:link { - color: $primary-fg-color; -} - -.mx_Login_prompt { - padding-top: 15px; - padding-bottom: 15px; - font-size: 13px; -} - -.mx_Login_forgot { - font-size: 15px; -} - -.mx_Login_forgot:link { - color: $primary-fg-color; -} - .mx_Login_sso_link { display: block; text-align: center; - font-size: 15px; margin-bottom: 20px; } -.mx_Login_sso_link:link { - color: $primary-fg-color; -} - .mx_Login_loader { display: inline; position: relative; @@ -233,16 +202,3 @@ limitations under the License. margin: 3px; vertical-align: top; } - -.mx_Login_language { - margin-left: auto; - margin-right: auto; - min-width: 60%; -} - -.mx_Login_language_div { - display: flex; - margin-top: 12px; - margin-bottom: 12px; -} - diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 97632c2e3f..cb28d57801 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -20,4 +20,19 @@ limitations under the License. border-radius: 0 4px 4px 0; padding: 25px 60px; box-sizing: border-box; + font-size: 12px; + color: $authpage-body-color; +} + +.mx_AuthBody a:link, +.mx_AuthBody a:hover, +.mx_AuthBody a:visited { + color: $accent-color; + text-decoration: none; +} + +.mx_Auth_changeFlow { + display: block; + text-align: center; + width: 100%; } diff --git a/res/css/views/auth/_AuthHeader.scss b/res/css/views/auth/_AuthHeader.scss index 376864d268..e1e8802437 100644 --- a/res/css/views/auth/_AuthHeader.scss +++ b/res/css/views/auth/_AuthHeader.scss @@ -15,6 +15,8 @@ limitations under the License. */ .mx_AuthHeader { + display: flex; + flex-direction: column; width: 206px; padding: 25px 50px; box-sizing: border-box; diff --git a/res/css/views/auth/_AuthHeaderLogo.scss b/res/css/views/auth/_AuthHeaderLogo.scss index 3d8ab29325..b6f107d9c4 100644 --- a/res/css/views/auth/_AuthHeaderLogo.scss +++ b/res/css/views/auth/_AuthHeaderLogo.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_AuthHeaderLogo { margin-top: 15px; + flex: 1; } .mx_AuthHeaderLogo img { diff --git a/res/css/views/auth/_AuthPage.scss b/res/css/views/auth/_AuthPage.scss index 9e32750771..04dab59ed6 100644 --- a/res/css/views/auth/_AuthPage.scss +++ b/res/css/views/auth/_AuthPage.scss @@ -24,9 +24,9 @@ limitations under the License. } .mx_AuthPage h2 { - font-weight: 300; - margin-top: 32px; - margin-bottom: 20px; + font-size: 24px; + font-weight: 600; + margin-top: 8px; } .mx_AuthPage_modal { diff --git a/res/css/views/auth/_LanguageSelector.scss b/res/css/views/auth/_LanguageSelector.scss new file mode 100644 index 0000000000..e73dd4ac64 --- /dev/null +++ b/res/css/views/auth/_LanguageSelector.scss @@ -0,0 +1,46 @@ +/* +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_Auth_language { + width: 100%; +} + +.mx_Auth_language .mx_Dropdown_input { + border: none; + font-size: 14px; + font-weight: 600; + color: $authpage-lang-color; +} + +/* TODO: Consider using this new arrow for all dropdowns */ +.mx_Auth_language .mx_Dropdown_arrow { + width: 10px; + height: 6px; + border: none; + right: 6px; +} + +.mx_Auth_language .mx_Dropdown_arrow::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask: url('$(res)/img/feather-icons/dropdown-arrow.svg'); + mask-repeat: no-repeat; + background: $authpage-lang-color; +} diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss new file mode 100644 index 0000000000..f255bb38f0 --- /dev/null +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -0,0 +1,34 @@ +/* +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. +*/ + +// ICONS +// ========================================================== + +.mx_RoomSettingsDialog_settingsIcon:before { + mask-image: url('$(res)/img/feather-icons/settings.svg'); +} + +.mx_RoomSettingsDialog_securityIcon:before { + mask-image: url('$(res)/img/feather-icons/lock.svg'); +} + +.mx_RoomSettingsDialog_rolesIcon:before { + mask-image: url('$(res)/img/feather-icons/users-sm.svg'); +} + +.mx_RoomSettingsDialog_warningIcon:before { + mask-image: url('$(res)/img/feather-icons/warning-triangle.svg'); +} diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss new file mode 100644 index 0000000000..2cc7c08039 --- /dev/null +++ b/res/css/views/dialogs/_SettingsDialog.scss @@ -0,0 +1,70 @@ +/* +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_SettingsDialog { + .mx_Dialog { + max-width: 784px; // 900px - 58px (left padding) - 58px (right padding) + width: 80%; + height: 80%; + border-radius: 4px; + + .mx_TabbedView_tabLabels { + // Force the sidebar to be always visible, letting the rest of the content scroll + position: fixed; + } + + .mx_TabbedView_tabPanel { + max-width: 485px; + margin-left: 206px; // 70px margin + 136px for the sidebar + } + + .mx_SettingsDialog_header { + font-size: 24px; + display: block; + text-align: center; + color: $dialog-title-fg-color; + margin-top: 16px; + margin-bottom: 24px; + padding: 0; + } + + .mx_SettingsDialog_close { + position: absolute; + top: 16px; + right: 25px; + } + + .mx_SettingsDialog_closeIcon { + width: 16px; + height: 16px; + display: inline-block; + } + + .mx_SettingsDialog_closeIcon:before { + mask: url('$(res)/img/feather-icons/cancel.svg'); + background-color: $dialog-close-fg-color; + mask-repeat: no-repeat; + mask-size: 16px; + mask-position: center; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + } +} diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index c4bd8a5110..2849573790 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -1,39 +1,18 @@ -.mx_UserSettingsDialog_header { - font-size: 24px; - display: block; - text-align: center; - color: $dialog-title-fg-color; - margin-top: 16px; - margin-bottom: 24px; - padding: 0; -} +/* +Copyright 2019 New Vector Ltd. -.mx_UserSettingsDialog_close { - position: absolute; - top: 16px; - right: 25px; -} +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 -.mx_UserSettingsDialog_closeIcon { - width: 16px; - height: 16px; - display: inline-block; -} - -.mx_UserSettingsDialog_closeIcon:before { - mask: url('$(res)/img/feather-icons/cancel.svg'); - background-color: $dialog-close-fg-color; - mask-repeat: no-repeat; - mask-size: 16px; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} + 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. +*/ // ICONS // ========================================================== @@ -60,4 +39,8 @@ .mx_UserSettingsDialog_helpIcon:before { mask-image: url('$(res)/img/feather-icons/help-circle.svg'); -} \ No newline at end of file +} + +.mx_UserSettingsDialog_labsIcon:before { + mask-image: url('$(res)/img/feather-icons/flag.svg'); +} diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 23445f5f6f..8de0b82554 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -27,10 +27,11 @@ limitations under the License. } .mx_AccessibleButton_hasKind { - padding: 10px 25px; + padding: 7px 18px; text-align: center; border-radius: 4px; display: inline-block; + font-size: 14px; } .mx_AccessibleButton_kind_primary { @@ -42,3 +43,35 @@ limitations under the License. color: $button-primary-disabled-fg-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; +} \ No newline at end of file diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 4f6e868249..b2c6822bf2 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -24,6 +24,7 @@ limitations under the License. .mx_Field input, .mx_Field select { font-weight: normal; + font-family: $font-family; border-radius: 4px; transition: border-color 0.25s; border: 1px solid $input-border-color; @@ -64,7 +65,7 @@ limitations under the License. .mx_Field input:focus + 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: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s, diff --git a/res/css/views/elements/_ToggleSwitch.scss b/res/css/views/elements/_ToggleSwitch.scss new file mode 100644 index 0000000000..4076414086 --- /dev/null +++ b/res/css/views/elements/_ToggleSwitch.scss @@ -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. +*/ + +.mx_ToggleSwitch { + transition: background-color 0.20s ease-out 0.1s; + 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 { + transition: left 0.15s ease-out 0.1s; + 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 { + left: 23px; // 48px switch - 20px ball - 5px padding = 23px +} diff --git a/res/css/views/groups/_GroupUserSettings.scss b/res/css/views/groups/_GroupUserSettings.scss index 0c909b7cf7..b207aa2958 100644 --- a/res/css/views/groups/_GroupUserSettings.scss +++ b/res/css/views/groups/_GroupUserSettings.scss @@ -18,6 +18,5 @@ limitations under the License. height: 200px; border: 1px solid $primary-hairline-color; border-radius: 3px; - margin-right: 32px; overflow: hidden; } diff --git a/res/css/views/settings/_EmailAddresses.scss b/res/css/views/settings/_EmailAddresses.scss new file mode 100644 index 0000000000..635ef4768f --- /dev/null +++ b/res/css/views/settings/_EmailAddresses.scss @@ -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); +} diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss new file mode 100644 index 0000000000..50aeb5cf8b --- /dev/null +++ b/res/css/views/settings/_PhoneNumbers.scss @@ -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; +} diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss new file mode 100644 index 0000000000..4bec429d55 --- /dev/null +++ b/res/css/views/settings/_ProfileSettings.scss @@ -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; +} diff --git a/res/css/views/settings/tabs/_GeneralSettingsTab.scss b/res/css/views/settings/tabs/_GeneralSettingsTab.scss index 8bc3de8689..cbf56ab559 100644 --- a/res/css/views/settings/tabs/_GeneralSettingsTab.scss +++ b/res/css/views/settings/tabs/_GeneralSettingsTab.scss @@ -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 { - flex-grow: 1; +.mx_GeneralSettingsTab_changePassword .mx_Field, +.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 } -.mx_GeneralSettingsTab_profileAvatar { - width: 88px; - height: 88px; - margin-left: 13px; +.mx_GeneralSettingsTab_changePassword .mx_Field:first-child { + margin-top: 0; } -.mx_GeneralSettingsTab_profileAvatar div { +.mx_GeneralSettingsTab_themeSection .mx_Field select { display: block; - width: 88px; - height: 88px; - border-radius: 4px; - background-color: #ccc; + width: 100%; +} + +.mx_GeneralSettingsTab_accountSection > .mx_EmailAddresses, +.mx_GeneralSettingsTab_accountSection > .mx_PhoneNumbers, +.mx_GeneralSettingsTab_languageInput { + margin-right: 100px; // Align with the other fields on the page } \ No newline at end of file diff --git a/res/css/views/settings/tabs/_HelpSettingsTab.scss b/res/css/views/settings/tabs/_HelpSettingsTab.scss new file mode 100644 index 0000000000..a1199b59b6 --- /dev/null +++ b/res/css/views/settings/tabs/_HelpSettingsTab.scss @@ -0,0 +1,24 @@ +/* +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_HelpSettingsTab_debugButton { + margin-bottom: 5px; + margin-top: 5px; +} + +.mx_HelpSettingsTab span.mx_AccessibleButton { + word-break: break-word; +} \ No newline at end of file diff --git a/res/css/views/settings/tabs/_PreferencesSettingsTab.scss b/res/css/views/settings/tabs/_PreferencesSettingsTab.scss new file mode 100644 index 0000000000..d6d7a8018e --- /dev/null +++ b/res/css/views/settings/tabs/_PreferencesSettingsTab.scss @@ -0,0 +1,27 @@ +/* +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_PreferencesSettingsTab .mx_Field { + margin-right: 100px; // Align with the rest of the controls +} + +.mx_PreferencesSettingsTab .mx_Field input { + display: block; + + // Subtract 10px padding on left and right + // This is to keep the input aligned with the rest of the tab's controls. + width: calc(100% - 20px); +} diff --git a/res/css/views/settings/tabs/_SecuritySettingsTab.scss b/res/css/views/settings/tabs/_SecuritySettingsTab.scss new file mode 100644 index 0000000000..2640df1383 --- /dev/null +++ b/res/css/views/settings/tabs/_SecuritySettingsTab.scss @@ -0,0 +1,53 @@ +/* +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_SecuritySettingsTab .mx_DevicesPanel { + // Normally the panel is 880px, however this can easily overflow the container. + // TODO: Fix the table to not be squishy + width: auto; + max-width: 880px; +} + +.mx_SecuritySettingsTab_deviceInfo { + display: table; + padding-left: 0; +} + +.mx_SecuritySettingsTab_deviceInfo > li { + display: table-row; +} + +.mx_SecuritySettingsTab_deviceInfo > li > label, +.mx_SecuritySettingsTab_deviceInfo > li > span { + display: table-cell; + padding-right: 1em; +} + +.mx_SecuritySettingsTab_importExportButtons .mx_AccessibleButton { + margin-right: 10px; +} + +.mx_SecuritySettingsTab_importExportButtons { + margin-bottom: 15px; +} + +.mx_SecuritySettingsTab_ignoredUser { + margin-bottom: 5px; +} + +.mx_SecuritySettingsTab_ignoredUser .mx_AccessibleButton { + margin-right: 10px; +} \ No newline at end of file diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 0753df56af..17f869b5b0 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -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 { font-size: 20px; font-weight: 600; @@ -10,8 +26,37 @@ font-family: $font-family-semibold; color: $primary-fg-color; margin-bottom: 10px; + margin-top: 12px; } -.mx_SettingsTab_section { - margin-top: 10px; +.mx_SettingsTab_subsectionText { + color: $settings-subsection-fg-color; + font-size: 12px; + padding-bottom: 12px; + display: block; + margin: 0 100px 0 0; // Align with the rest of the view } + +.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; + max-width: calc(100% - 48px); // Force word wrap instead of colliding with the switch +} + +.mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch { + float: right; +} + +.mx_SettingsTab_linkBtn { + cursor: pointer; + color: $accent-color; + word-break: break-all; +} \ No newline at end of file diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/_VoiceSettingsTab.scss new file mode 100644 index 0000000000..345fefe3cd --- /dev/null +++ b/res/css/views/settings/tabs/_VoiceSettingsTab.scss @@ -0,0 +1,28 @@ +/* +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_VoiceSettingsTab .mx_Field select { + width: 100%; + max-width: 100%; +} + +.mx_VoiceSettingsTab .mx_Field { + margin-right: 100px; // align with the rest of the fields +} + +.mx_VoiceSettingsTab_missingMediaPermissions { + margin-bottom: 15px; +} diff --git a/res/img/feather-icons/dropdown-arrow.svg b/res/img/feather-icons/dropdown-arrow.svg new file mode 100644 index 0000000000..a1d46fa61a --- /dev/null +++ b/res/img/feather-icons/dropdown-arrow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/feather-icons/flag.svg b/res/img/feather-icons/flag.svg new file mode 100644 index 0000000000..983c02762b --- /dev/null +++ b/res/img/feather-icons/flag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/feather-icons/upload.svg b/res/img/feather-icons/upload.svg new file mode 100644 index 0000000000..8ec5d95c2c --- /dev/null +++ b/res/img/feather-icons/upload.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/feather-icons/users-sm.svg b/res/img/feather-icons/users-sm.svg new file mode 100644 index 0000000000..6098be38c3 --- /dev/null +++ b/res/img/feather-icons/users-sm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/feather-icons/warning-triangle.svg b/res/img/feather-icons/warning-triangle.svg new file mode 100644 index 0000000000..02196cbf43 --- /dev/null +++ b/res/img/feather-icons/warning-triangle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 235eb02e3c..940e26a1a2 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -120,6 +120,12 @@ $blockquote-bar-color: #ddd; $blockquote-fg-color: #777; $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-accept-color: #80f480; @@ -205,6 +211,15 @@ $button-primary-fg-color: #ffffff; $button-primary-bg-color: #7ac9a1; $button-primary-disabled-fg-color: #ffffff; $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? $progressbar-color: #000; @@ -216,6 +231,8 @@ $memberstatus-placeholder-color: $roomtile-name-color; $authpage-bg-color: #2e3649; $authpage-modal-bg-color: rgba(255, 255, 255, 0.59); $authpage-body-bg-color: #ffffff; +$authpage-lang-color: #4e5054; +$authpage-body-color: #61708b; /*** form elements ***/ diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 1522ee82a0..6871ec8d6c 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -113,6 +113,12 @@ $blockquote-bar-color: #ddd; $blockquote-fg-color: #777; $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-accept-color: #80f480; @@ -201,6 +207,15 @@ $button-primary-fg-color: #ffffff; $button-primary-bg-color: #7ac9a1; $button-primary-disabled-fg-color: #ffffff; $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? $progressbar-color: #000; @@ -212,6 +227,8 @@ $memberstatus-placeholder-color: $roomtile-name-color; $authpage-bg-color: #2e3649; $authpage-modal-bg-color: rgba(255, 255, 255, 0.59); $authpage-body-bg-color: #ffffff; +$authpage-lang-color: #4e5054; +$authpage-body-color: #61708b; // ***** Mixins! ***** diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 337e38d867..8de7e9e21c 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -26,7 +26,7 @@ import { _t } from './languageHandler'; * the client owns the given email address, which is then passed to the * add threepid API on the homeserver. */ -class AddThreepid { +export default class AddThreepid { constructor() { this.clientSecret = MatrixClientPeg.get().generateClientSecret(); } @@ -124,5 +124,3 @@ class AddThreepid { }); } } - -module.exports = AddThreepid; diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index 2330f86b99..9a1c9d70b8 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -69,4 +69,16 @@ export default { SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); Matrix.setMatrixCallVideoInput(deviceId); }, + + getAudioOutput: function() { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); + }, + + getAudioInput: function() { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput"); + }, + + getVideoInput: function() { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput"); + }, }; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index ed057eb020..54ac605c65 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -27,7 +27,6 @@ import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; -import RtsClient from './RtsClient'; import Modal from './Modal'; import sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; @@ -224,7 +223,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { // // The plan is to gradually move the localStorage access done here into // SessionStore to avoid bugs where the view becomes out-of-sync with -// localStorage (e.g. teamToken, isGuest etc.) +// localStorage (e.g. isGuest etc.) async function _restoreFromLocalStorage() { if (!localStorage) { return false; @@ -286,15 +285,6 @@ function _handleLoadSessionFailure(e) { }); } -let rtsClient = null; -export function initRtsClient(url) { - if (url) { - rtsClient = new RtsClient(url); - } else { - rtsClient = null; - } -} - /** * Transitions to a logged-in state using the given credentials. * @@ -333,7 +323,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { ); // This is dispatched to indicate that the user is still in the process of logging in - // because `teamPromise` may take some time to resolve, breaking the assumption that + // because async code may take some time to resolve, breaking the assumption that // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms // later than MatrixChat might assume. // @@ -347,10 +337,6 @@ async function _doSetLoggedIn(credentials, clearStorage) { Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl); - // Resolves by default - let teamPromise = Promise.resolve(null); - - if (localStorage) { try { _persistCredentialsToLocalStorage(credentials); @@ -367,27 +353,13 @@ async function _doSetLoggedIn(credentials, clearStorage) { } catch (e) { console.warn("Error using local storage: can't persist session!", e); } - - if (rtsClient && !credentials.guest) { - teamPromise = rtsClient.login(credentials.userId).then((body) => { - if (body.team_token) { - localStorage.setItem("mx_team_token", body.team_token); - } - return body.team_token; - }, (err) => { - console.warn(`Failed to get team token on login: ${err}` ); - return null; - }); - } } else { console.warn("No local storage available: can't persist session!"); } MatrixClientPeg.replaceUsingCreds(credentials); - teamPromise.then((teamToken) => { - dis.dispatch({action: 'on_logged_in', teamToken: teamToken}); - }); + dis.dispatch({ action: 'on_logged_in' }); await startMatrixClient(); return MatrixClientPeg.get(); diff --git a/src/Login.js b/src/Login.js index ca045e36cd..893ec42097 100644 --- a/src/Login.js +++ b/src/Login.js @@ -49,6 +49,7 @@ export default class Login { /** * Get a temporary MatrixClient, which can be used for login or register * requests. + * @returns {MatrixClient} */ _createTemporaryClient() { return Matrix.createClient({ @@ -144,8 +145,8 @@ export default class Login { const tryFallbackHs = (originalError) => { return sendLoginRequest( self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams, - ).catch((fallback_error) => { - console.log("fallback HS login failed", fallback_error); + ).catch((fallbackError) => { + console.log("fallback HS login failed", fallbackError); // throw the original error throw originalError; }); diff --git a/src/RtsClient.js b/src/RtsClient.js deleted file mode 100644 index 493b19599c..0000000000 --- a/src/RtsClient.js +++ /dev/null @@ -1,104 +0,0 @@ -import 'whatwg-fetch'; - -let fetchFunction = fetch; - -function checkStatus(response) { - if (!response.ok) { - return response.text().then((text) => { - throw new Error(text); - }); - } - return response; -} - -function parseJson(response) { - return response.json(); -} - -function encodeQueryParams(params) { - return '?' + Object.keys(params).map((k) => { - return k + '=' + encodeURIComponent(params[k]); - }).join('&'); -} - -const request = (url, opts) => { - if (opts && opts.qs) { - url += encodeQueryParams(opts.qs); - delete opts.qs; - } - if (opts && opts.body) { - if (!opts.headers) { - opts.headers = {}; - } - opts.body = JSON.stringify(opts.body); - opts.headers['Content-Type'] = 'application/json'; - } - return fetchFunction(url, opts) - .then(checkStatus) - .then(parseJson); -}; - - -export default class RtsClient { - constructor(url) { - this._url = url; - } - - getTeamsConfig() { - return request(this._url + '/teams'); - } - - /** - * Track a referral with the Riot Team Server. This should be called once a referred - * user has been successfully registered. - * @param {string} referrer the user ID of one who referred the user to Riot. - * @param {string} sid the sign-up identity server session ID . - * @param {string} clientSecret the sign-up client secret. - * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon - * success. - */ - trackReferral(referrer, sid, clientSecret) { - return request(this._url + '/register', - { - body: { - referrer: referrer, - session_id: sid, - client_secret: clientSecret, - }, - method: 'POST', - }, - ); - } - - getTeam(teamToken) { - return request(this._url + '/teamConfiguration', - { - qs: { - team_token: teamToken, - }, - }, - ); - } - - /** - * Signal to the RTS that a login has occurred and that a user requires their team's - * token. - * @param {string} userId the user ID of the user who is a member of a team. - * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon - * success. - */ - login(userId) { - return request(this._url + '/login', - { - qs: { - user_id: userId, - }, - }, - ); - } - - // allow fetch to be replaced, for testing. - static setFetch(fn) { - fetchFunction = fn; - } -} diff --git a/src/UiEffects.js b/src/UiEffects.js index 76db0b7f12..06b0a0e3b7 100644 --- a/src/UiEffects.js +++ b/src/UiEffects.js @@ -22,6 +22,6 @@ limitations under the License. import Velocity from 'velocity-vector'; import 'velocity-vector/velocity.ui'; -export function field_input_incorrect(element) { +export function fieldInputIncorrect(element) { Velocity(element, "callout.shake", 300); } diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index aa17e63d73..948a07ee59 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -30,11 +30,6 @@ class HomePage extends React.Component { static displayName = 'HomePage'; static propTypes = { - // URL base of the team server. Optional. - teamServerUrl: PropTypes.string, - // Team token. Optional. If set, used to get the static homepage of the team - // associated. If unset, homePageUrl will be used. - teamToken: PropTypes.string, // URL to use as the iFrame src. Defaults to /home.html. homePageUrl: PropTypes.string, }; @@ -56,35 +51,29 @@ class HomePage extends React.Component { componentWillMount() { this._unmounted = false; - if (this.props.teamToken && this.props.teamServerUrl) { - this.setState({ - iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`, - }); - } else { - // we use request() to inline the homepage into the react component - // so that it can inherit CSS and theming easily rather than mess around - // with iframes and trying to synchronise document.stylesheets. + // we use request() to inline the homepage into the react component + // so that it can inherit CSS and theming easily rather than mess around + // with iframes and trying to synchronise document.stylesheets. - const src = this.props.homePageUrl || 'home.html'; + const src = this.props.homePageUrl || 'home.html'; - request( - { method: "GET", url: src }, - (err, response, body) => { - if (this._unmounted) { - return; - } + request( + { method: "GET", url: src }, + (err, response, body) => { + if (this._unmounted) { + return; + } - if (err || response.status < 200 || response.status >= 300) { - console.warn(`Error loading home page: ${err}`); - this.setState({ page: _t("Couldn't load home page") }); - return; - } + if (err || response.status < 200 || response.status >= 300) { + console.warn(`Error loading home page: ${err}`); + this.setState({ page: _t("Couldn't load home page") }); + return; + } - body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); - this.setState({ page: body }); - }, - ); - } + body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); + this.setState({ page: body }); + }, + ); } componentWillUnmount() { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 6f2e1f3989..645782a854 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -63,7 +63,6 @@ const LoggedInView = React.createClass({ // transitioned to PWLU) onRegistered: PropTypes.func, collapsedRhs: PropTypes.bool, - teamToken: PropTypes.string, // Used by the RoomView to handle joining rooms viaServers: PropTypes.arrayOf(PropTypes.string), @@ -457,8 +456,6 @@ const LoggedInView = React.createClass({ pageElement = ; break; @@ -475,15 +472,7 @@ const LoggedInView = React.createClass({ case PageTypes.HomePage: { - // If team server config is present, pass the teamServerURL. props.teamToken - // must also be set for the team page to be displayed, otherwise the - // welcomePageUrl is used (which might be undefined). - const teamServerUrl = this.props.config.teamServerConfig ? - this.props.config.teamServerConfig.teamServerURL : null; - pageElement = ; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2df97b4653..b8be3e017a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -75,8 +75,8 @@ const VIEWS = { // we have valid matrix credentials (either via an explicit login, via the // initial re-animation/guest registration, or via a registration), and are // now setting up a matrixclient to talk to it. This isn't an instant - // process because (a) we need to clear out indexeddb, and (b) we need to - // talk to the team server; while it is going on we show a big spinner. + // process because we need to clear out indexeddb. While it is going on we + // show a big spinner. LOGGING_IN: 5, // we are logged in with an active matrix client. @@ -256,42 +256,6 @@ export default React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } - // To enable things like riot.im/geektime in a nicer way than rewriting the URL - // and appending a team token query parameter, use the first path segment to - // indicate a team, with "public" team tokens stored in the config teamTokenMap. - let routedTeamToken = null; - if (this.props.config.teamTokenMap) { - const teamName = window.location.pathname.split('/')[1]; - if (teamName && this.props.config.teamTokenMap.hasOwnProperty(teamName)) { - routedTeamToken = this.props.config.teamTokenMap[teamName]; - } - } - - // Persist the team token across refreshes using sessionStorage. A new window or - // tab will not persist sessionStorage, but refreshes will. - if (this.props.startingFragmentQueryParams.team_token) { - window.sessionStorage.setItem( - 'mx_team_token', - this.props.startingFragmentQueryParams.team_token, - ); - } - - // Use the locally-stored team token first, then as a fall-back, check to see if - // a referral link was used, which will contain a query parameter `team_token`. - this._teamToken = routedTeamToken || - window.localStorage.getItem('mx_team_token') || - window.sessionStorage.getItem('mx_team_token'); - - // Some users have ended up with "undefined" as their local storage team token, - // treat that as undefined. - if (this._teamToken === "undefined") { - this._teamToken = undefined; - } - - if (this._teamToken) { - console.info(`Team token set to ${this._teamToken}`); - } - // Set up the default URLs (async) if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { this.setState({loadingDefaultHomeserver: true}); @@ -360,9 +324,6 @@ export default React.createClass({ linkifyMatrix.onGroupClick = this.onGroupClick; } - const teamServerConfig = this.props.config.teamServerConfig || {}; - Lifecycle.initRtsClient(teamServerConfig.teamServerURL); - // the first thing to do is to try the token params in the query-string Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { if (loggedIn) { @@ -613,7 +574,7 @@ export default React.createClass({ case 'view_user_settings': { if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) { const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); - Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}); + Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog'); } else { this._setPage(PageTypes.UserSettings); this.notifyNewScreen('settings'); @@ -726,7 +687,7 @@ export default React.createClass({ }); break; case 'on_logged_in': - this._onLoggedIn(payload.teamToken); + this._onLoggedIn(); break; case 'on_logged_out': this._onLoggedOut(); @@ -1196,16 +1157,10 @@ export default React.createClass({ /** * Called when a new logged in session has started - * - * @param {string} teamToken */ - _onLoggedIn: async function(teamToken) { + _onLoggedIn: async function() { this.setStateForNewView({view: VIEWS.LOGGED_IN}); - if (teamToken) { - // A team member has logged in, not a guest - this._teamToken = teamToken; - dis.dispatch({action: 'view_home_page'}); - } else if (this._is_registered) { + if (this._is_registered) { this._is_registered = false; if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { @@ -1261,7 +1216,6 @@ export default React.createClass({ currentRoomId: null, page_type: PageTypes.RoomDirectory, }); - this._teamToken = null; this._setPageSubtitle(); }, @@ -1714,15 +1668,13 @@ export default React.createClass({ onReturnToAppClick: function() { // treat it the same as if the user had completed the login - this._onLoggedIn(null); + this._onLoggedIn(); }, // returns a promise which resolves to the new MatrixClient - onRegistered: function(credentials, teamToken) { - // XXX: These both should be in state or ideally store(s) because we risk not + onRegistered: function(credentials) { + // XXX: This should be in state or ideally store(s) because we risk not // rendering the most up-to-date view of state otherwise. - // teamToken may not be truthy - this._teamToken = teamToken; this._is_registered = true; return Lifecycle.setLoggedIn(credentials); }, @@ -1895,7 +1847,6 @@ export default React.createClass({ onCloseAllSettings={this.onCloseAllSettings} onRegistered={this.onRegistered} currentRoomId={this.state.currentRoomId} - teamToken={this._teamToken} showCookieBar={this.state.showCookieBar} {...this.props} {...this.state} @@ -1936,7 +1887,6 @@ export default React.createClass({ defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} - teamServerConfig={this.props.config.teamServerConfig} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} makeRegistrationUrl={this._makeRegistrationUrl} @@ -1962,7 +1912,6 @@ export default React.createClass({ customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} onComplete={this.onLoginClick} - onRegisterClick={this.onRegisterClick} onLoginClick={this.onLoginClick} /> ); } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 65e1897137..f4a2c57aaf 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -167,13 +167,6 @@ module.exports = React.createClass({ onClose: PropTypes.func, // The brand string given when creating email pushers brand: PropTypes.string, - - // The base URL to use in the referral link. Defaults to window.location.origin. - referralBaseUrl: PropTypes.string, - - // Team token for the referral link. If falsy, the referral section will - // not appear - teamToken: PropTypes.string, }, getDefaultProps: function() { @@ -590,27 +583,6 @@ module.exports = React.createClass({ return ; }, - _renderReferral: function() { - const teamToken = this.props.teamToken; - if (!teamToken) { - return null; - } - if (typeof teamToken !== 'string') { - console.warn('Team token not a string'); - return null; - } - const href = (this.props.referralBaseUrl || window.location.origin) + - `/#/register?referrer=${this._me}&team_token=${teamToken}`; - return ( -
-

Referral

-
- { _t("Refer a friend to Riot:") } { href } -
-
- ); - }, - onLanguageChange: function(newLang) { if (this.state.language !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); @@ -637,11 +609,14 @@ module.exports = React.createClass({ // to rebind the onChange each time we render const onChange = (e) => 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 (

{ _t("User Interface") }

{ SIMPLE_SETTINGS.map( this._renderAccountSetting ) } +
Themes
{ THEMES.map( this._renderThemeOption ) } @@ -676,18 +651,12 @@ module.exports = React.createClass({ }, _renderThemeOption: function(setting) { - const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); - const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value}); - return ( -
- -
- ); + // HACK: Temporary disablement of theme selection. + // We don't support changing themes on experimental anyways, and radio groups aren't + // a thing anymore for setting flags. We're also dropping this view in the very near + // future, so just replace the theme selection with placeholder text. + const currentTheme = SettingsStore.getValue("theme"); + return
{_t(setting.label)} {currentTheme === setting.value ? '(current)' : null}
; }, _renderCryptoInfo: function() { @@ -1268,9 +1237,7 @@ module.exports = React.createClass({ ); @@ -1360,8 +1327,6 @@ module.exports = React.createClass({ { this._renderGroupSettings() } - { this._renderReferral() } - { notificationArea } { this._renderUserInterfaceSettings() } diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 87e8c7d131..73d9f95e20 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -34,7 +34,6 @@ module.exports = React.createClass({ customHsUrl: PropTypes.string, customIsUrl: PropTypes.string, onLoginClick: PropTypes.func, - onRegisterClick: PropTypes.func, onComplete: PropTypes.func.isRequired, // The default server name to use when the user hasn't specified @@ -168,12 +167,6 @@ module.exports = React.createClass({ this.props.onLoginClick(); }, - onRegisterClick: function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - this.props.onRegisterClick(); - }, - showErrorDialog: function(body, title) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { @@ -195,7 +188,7 @@ module.exports = React.createClass({ resetPasswordJsx = ; } else if (this.state.progress === "sent_email") { resetPasswordJsx = ( -
+
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, " + "click below.", { emailAddress: this.state.email }) }
@@ -205,7 +198,7 @@ module.exports = React.createClass({ ); } else if (this.state.progress === "complete") { resetPasswordJsx = ( -
+

{ _t('Your password has been reset') }.

{ _t('You have been logged out of all devices and will no longer receive push notifications. ' + 'To re-enable notifications, sign in again on each device') }.

@@ -234,13 +227,11 @@ module.exports = React.createClass({ errorText =
{ err }
; } - const LanguageSelector = sdk.getComponent('structures.auth.LanguageSelector'); - resetPasswordJsx = (
-
+

{ _t('To reset your password, enter the email address linked to your account') }: -

+

); @@ -282,6 +269,7 @@ module.exports = React.createClass({ +

{ _t('Set a new password') }

{resetPasswordJsx}
diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 28bed9af05..e28987e601 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -446,7 +446,9 @@ module.exports = React.createClass({ "Either use HTTPS or enable unsafe scripts.", {}, { 'a': (sub) => { - return + return { sub } ; }, @@ -460,7 +462,9 @@ module.exports = React.createClass({ "is not blocking requests.", {}, { 'a': (sub) => { - return { sub }; + return { sub }; }, }, ) } @@ -508,6 +512,14 @@ module.exports = React.createClass({ }, _renderSsoStep: function(url) { + // XXX: This link does *not* have a target="_blank" because single sign-on relies on + // redirecting the user back to a URI once they're logged in. On the web, this means + // we use the same window and redirect back to riot. On electron, this actually + // opens the SSO page in the electron app itself due to + // https://github.com/electron/electron/issues/8841 and so happens to work. + // If this bug gets fixed, it will break SSO since it will open the SSO page in the + // user's browser, let them log into their SSO provider, then redirect their browser + // to vector://vector which, of course, will not work. return ( { _t('Sign in with single sign-on') } ); @@ -526,7 +538,7 @@ module.exports = React.createClass({ let loginAsGuestJsx; if (this.props.enableGuest) { loginAsGuestJsx = - + { _t('Try the app first') } ; } @@ -544,8 +556,6 @@ module.exports = React.createClass({ delayTimeMs={1000} />; } - const header =

{ _t('Sign in') } { loader }

; - let errorTextSection; if (errorText) { errorTextSection = ( @@ -555,21 +565,21 @@ module.exports = React.createClass({ ); } - const LanguageSelector = sdk.getComponent('structures.auth.LanguageSelector'); - return ( - { header } +

+ {_t('Sign in to your account')} + {loader} +

{ errorTextSection } { this.componentForStep(this.state.currentFlow) } { serverConfig } - - { _t('Create an account') } + + { _t('Create account') } { loginAsGuestJsx } -
); diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index fe6fb078e3..acc45df86f 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -23,9 +23,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import RegistrationForm from '../../views/auth/RegistrationForm'; -import RtsClient from '../../../RtsClient'; import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; @@ -48,13 +46,6 @@ module.exports = React.createClass({ brand: PropTypes.string, email: PropTypes.string, referrer: PropTypes.string, - teamServerConfig: PropTypes.shape({ - // Email address to request new teams - supportEmail: PropTypes.string.isRequired, - // URL of the riot-team-server to get team configurations and track referrals - teamServerURL: PropTypes.string.isRequired, - }), - teamSelected: PropTypes.object, // The default server name to use when the user hasn't specified // one. This is used when displaying the defaultHsUrl in the UI. @@ -70,18 +61,11 @@ module.exports = React.createClass({ onLoginClick: PropTypes.func.isRequired, onCancelClick: PropTypes.func, onServerConfigChange: PropTypes.func.isRequired, - - rtsClient: PropTypes.shape({ - getTeamsConfig: PropTypes.func.isRequired, - trackReferral: PropTypes.func.isRequired, - getTeam: PropTypes.func.isRequired, - }), }, getInitialState: function() { return { busy: false, - teamServerBusy: false, errorText: null, // We remember the values entered by the user because // the registration form will be unmounted during the @@ -106,37 +90,7 @@ module.exports = React.createClass({ componentWillMount: function() { this._unmounted = false; - this._replaceClient(); - - if ( - this.props.teamServerConfig && - this.props.teamServerConfig.teamServerURL && - !this._rtsClient - ) { - this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL); - - this.setState({ - teamServerBusy: true, - }); - // GET team configurations including domains, names and icons - this._rtsClient.getTeamsConfig().then((data) => { - const teamsConfig = { - teams: data, - supportEmail: this.props.teamServerConfig.supportEmail, - }; - console.log('Setting teams config to ', teamsConfig); - this.setState({ - teamsConfig: teamsConfig, - teamServerBusy: false, - }); - }, (err) => { - console.error('Error retrieving config for teams', err); - this.setState({ - teamServerBusy: false, - }); - }); - } }, onServerConfigChange: function(config) { @@ -191,7 +145,7 @@ module.exports = React.createClass({ }); }, - _onUIAuthFinished: function(success, response, extra) { + _onUIAuthFinished: async function(success, response, extra) { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? @@ -240,58 +194,15 @@ module.exports = React.createClass({ doingUIAuth: false, }); - // Done regardless of `teamSelected`. People registering with non-team emails - // will just nop. The point of this being we might not have the email address - // that the user registered with at this stage (depending on whether this - // is the client they initiated registration). - let trackPromise = Promise.resolve(null); - if (this._rtsClient && extra.emailSid) { - // Track referral if this.props.referrer set, get team_token in order to - // retrieve team config and see welcome page etc. - trackPromise = this._rtsClient.trackReferral( - this.props.referrer || '', // Default to empty string = not referred - extra.emailSid, - extra.clientSecret, - ).then((data) => { - const teamToken = data.team_token; - // Store for use /w welcome pages - window.localStorage.setItem('mx_team_token', teamToken); - - this._rtsClient.getTeam(teamToken).then((team) => { - console.log( - `User successfully registered with team ${team.name}`, - ); - if (!team.rooms) { - return; - } - // Auto-join rooms - team.rooms.forEach((room) => { - if (room.auto_join && room.room_id) { - console.log(`Auto-joining ${room.room_id}`); - MatrixClientPeg.get().joinRoom(room.room_id); - } - }); - }, (err) => { - console.error('Error getting team config', err); - }); - - return teamToken; - }, (err) => { - console.error('Error tracking referral', err); - }); - } - - trackPromise.then((teamToken) => { - return this.props.onLoggedIn({ - userId: response.user_id, - deviceId: response.device_id, - homeserverUrl: this._matrixClient.getHomeserverUrl(), - identityServerUrl: this._matrixClient.getIdentityServerUrl(), - accessToken: response.access_token, - }, teamToken); - }).then((cli) => { - return this._setupPushers(cli); + const cli = await this.props.onLoggedIn({ + userId: response.user_id, + deviceId: response.device_id, + homeserverUrl: this._matrixClient.getHomeserverUrl(), + identityServerUrl: this._matrixClient.getIdentityServerUrl(), + accessToken: response.access_token, }); + + this._setupPushers(cli); }, _setupPushers: function(matrixClient) { @@ -356,12 +267,6 @@ module.exports = React.createClass({ }); }, - onTeamSelected: function(teamSelected) { - if (!this._unmounted) { - this.setState({ teamSelected }); - } - }, - onLoginClick: function(ev) { ev.preventDefault(); ev.stopPropagation(); @@ -418,7 +323,7 @@ module.exports = React.createClass({ poll={true} /> ); - } else if (this.state.busy || this.state.teamServerBusy || !this.state.flows) { + } else if (this.state.busy || !this.state.flows) { registerBody = ; } else { let serverConfigSection; @@ -443,11 +348,9 @@ module.exports = React.createClass({ defaultPhoneCountry={this.state.formVals.phoneCountry} defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPassword={this.state.formVals.password} - teamsConfig={this.state.teamsConfig} minPasswordLength={MIN_PASSWORD_LENGTH} onError={this.onFormValidationFailed} onRegisterClick={this.onFormSubmit} - onTeamSelected={this.onTeamSelected} flows={this.state.flows} /> { serverConfigSection } @@ -457,7 +360,6 @@ module.exports = React.createClass({ let errorText; const err = this.state.errorText || this.props.defaultServerDiscoveryError; - const header =

{ _t('Create an account') }

; if (err) { errorText =
{ err }
; } @@ -465,28 +367,20 @@ module.exports = React.createClass({ let signIn; if (!this.state.doingUIAuth) { signIn = ( - - { _t('I already have an account') } + + { _t('Sign in instead') } ); } - const LanguageSelector = sdk.getComponent('structures.auth.LanguageSelector'); - return ( - + - { header } +

{ _t('Create your account') }

{ registerBody } { signIn } { errorText } -
); diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.js index ea43bf322c..98359b9650 100644 --- a/src/components/views/auth/AuthFooter.js +++ b/src/components/views/auth/AuthFooter.js @@ -26,7 +26,7 @@ module.exports = React.createClass({ render: function() { return ( ); }, diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.js index c1d831a70a..f5e9e44167 100644 --- a/src/components/views/auth/AuthHeader.js +++ b/src/components/views/auth/AuthHeader.js @@ -25,10 +25,12 @@ module.exports = React.createClass({ render: function() { const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo'); + const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector'); return (
+
); }, diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js index ab1bfbd393..01715b397c 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.js @@ -62,7 +62,7 @@ module.exports = React.createClass({ console.log("Loading recaptcha script..."); window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; const protocol = global.location.protocol; - if (protocol === "file:") { + if (protocol === "vector:") { const warning = document.createElement('div'); // XXX: fix hardcoded app URL. Better solutions include: // * jumping straight to a hosted captcha page (but we don't support that yet) @@ -71,7 +71,11 @@ module.exports = React.createClass({ ReactDOM.render(_t( "Robot check is currently unavailable on desktop - please use a web browser", {}, - { 'a': (sub) => { return { sub }; }}), warning); + { + 'a': (sub) => { + return { sub }; + }, + }), warning); this.refs.recaptchaContainer.appendChild(warning); } else { const scriptTag = document.createElement('script'); diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js index 5f5dbf41a4..3a71d5d1c8 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.js @@ -21,7 +21,7 @@ import sdk from '../../../index'; import { COUNTRIES } from '../../../phonenumber'; -const COUNTRIES_BY_ISO2 = new Object(null); +const COUNTRIES_BY_ISO2 = {}; for (const c of COUNTRIES) { COUNTRIES_BY_ISO2[c.iso2] = c; } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 6a78898179..1df0a487cc 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -294,7 +294,7 @@ export const TermsAuthEntry = React.createClass({ _trySubmit: function() { let allChecked = true; for (const policy of this.state.policies) { - let checked = this.state.toggledPolicies[policy.id]; + const checked = this.state.toggledPolicies[policy.id]; allChecked = allChecked && checked; } @@ -440,7 +440,6 @@ export const MsisdnAuthEntry = React.createClass({ clientSecret: PropTypes.func, submitAuthDict: PropTypes.func.isRequired, matrixClient: PropTypes.object, - submitAuthDict: PropTypes.func, }, getInitialState: function() { diff --git a/src/components/structures/auth/LanguageSelector.js b/src/components/views/auth/LanguageSelector.js similarity index 87% rename from src/components/structures/auth/LanguageSelector.js rename to src/components/views/auth/LanguageSelector.js index 965d8334d9..d964af184c 100644 --- a/src/components/structures/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.js @@ -32,7 +32,8 @@ export default function LanguageSelector() { if (SdkConfig.get()['disable_login_language_selector']) return
; const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); - return
- -
; + return ; } diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 1d36c52a24..bf1c27e6e3 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {field_input_incorrect} from '../../../UiEffects'; +import {fieldInputIncorrect} from '../../../UiEffects'; import SdkConfig from '../../../SdkConfig'; /** @@ -71,7 +71,7 @@ class PasswordLogin extends React.Component { componentWillReceiveProps(nextProps) { if (!this.props.loginIncorrect && nextProps.loginIncorrect) { - field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField); + fieldInputIncorrect(this.isLoginEmpty() ? this._loginField : this._passwordField); } } @@ -245,11 +245,16 @@ class PasswordLogin extends React.Component { let forgotPasswordJsx; if (this.props.onForgotPasswordClick) { - forgotPasswordJsx = ( - - { _t('Forgot your password?') } - - ); + forgotPasswordJsx = + {_t('Not sure of your password? Set a new one', {}, { + a: sub => + {sub} + , + })} + ; } let matrixIdText = _t('Matrix ID'); diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index a0fc79b947..b38d8ca361 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { field_input_incorrect } from '../../../UiEffects'; +import { fieldInputIncorrect } from '../../../UiEffects'; import sdk from '../../../index'; import Email from '../../../email'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; @@ -28,7 +28,6 @@ import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; const FIELD_EMAIL = 'field_email'; -const FIELD_PHONE_COUNTRY = 'field_phone_country'; const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_USERNAME = 'field_username'; const FIELD_PASSWORD = 'field_password'; @@ -47,17 +46,6 @@ module.exports = React.createClass({ defaultPhoneNumber: PropTypes.string, defaultUsername: PropTypes.string, defaultPassword: PropTypes.string, - teamsConfig: PropTypes.shape({ - // Email address to request new teams - supportEmail: PropTypes.string, - teams: PropTypes.arrayOf(PropTypes.shape({ - // The displayed name of the team - "name": PropTypes.string, - // The domain of team email addresses - "domain": PropTypes.string, - })).required, - }), - minPasswordLength: PropTypes.number, onError: PropTypes.func, onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise @@ -76,7 +64,6 @@ module.exports = React.createClass({ getInitialState: function() { return { fieldValid: {}, - selectedTeam: null, // The ISO2 country code selected in the phone number entry phoneCountry: this.props.defaultPhoneCountry, }; @@ -139,8 +126,7 @@ module.exports = React.createClass({ }, /** - * Returns true if all fields were valid last time - * they were validated. + * @returns {boolean} true if all fields were valid last time they were validated. */ allFieldsValid: function() { const keys = Object.keys(this.state.fieldValid); @@ -152,103 +138,84 @@ module.exports = React.createClass({ return true; }, - _isUniEmail: function(email) { - return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org'); - }, - - validateField: function(field_id) { + validateField: function(fieldID) { const pwd1 = this.refs.password.value.trim(); const pwd2 = this.refs.passwordConfirm.value.trim(); - switch (field_id) { - case FIELD_EMAIL: + switch (fieldID) { + case FIELD_EMAIL: { const email = this.refs.email.value; - if (this.props.teamsConfig && this._isUniEmail(email)) { - const matchingTeam = this.props.teamsConfig.teams.find( - (team) => { - return email.split('@').pop() === team.domain; - }, - ) || null; - this.setState({ - selectedTeam: matchingTeam, - showSupportEmail: !matchingTeam, - }); - this.props.onTeamSelected(matchingTeam); - } else { - this.props.onTeamSelected(null); - this.setState({ - selectedTeam: null, - showSupportEmail: false, - }); - } const emailValid = email === '' || Email.looksValid(email); if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) { - this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_EMAIL"); - } else this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); + this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL"); + } else this.markFieldValid(fieldID, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); break; - case FIELD_PHONE_NUMBER: + } + case FIELD_PHONE_NUMBER: { const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : ''; const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) { - this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER"); - } else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); + this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER"); + } else this.markFieldValid(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); break; - case FIELD_USERNAME: + } + case FIELD_USERNAME: { const username = this.refs.username.value.trim(); if (!SAFE_LOCALPART_REGEX.test(username)) { this.markFieldValid( - field_id, + fieldID, false, "RegistrationForm.ERR_USERNAME_INVALID", ); } else if (username == '') { this.markFieldValid( - field_id, + fieldID, false, "RegistrationForm.ERR_USERNAME_BLANK", ); } else { - this.markFieldValid(field_id, true); + this.markFieldValid(fieldID, true); } break; + } case FIELD_PASSWORD: if (pwd1 == '') { this.markFieldValid( - field_id, + fieldID, false, "RegistrationForm.ERR_PASSWORD_MISSING", ); } else if (pwd1.length < this.props.minPasswordLength) { this.markFieldValid( - field_id, + fieldID, false, "RegistrationForm.ERR_PASSWORD_LENGTH", ); } else { - this.markFieldValid(field_id, true); + this.markFieldValid(fieldID, true); } break; case FIELD_PASSWORD_CONFIRM: this.markFieldValid( - field_id, pwd1 == pwd2, + fieldID, pwd1 == pwd2, "RegistrationForm.ERR_PASSWORD_MISMATCH", ); break; } }, - markFieldValid: function(field_id, val, error_code) { + markFieldValid: function(fieldID, val, errorCode) { const fieldValid = this.state.fieldValid; - fieldValid[field_id] = val; + fieldValid[fieldID] = val; this.setState({fieldValid: fieldValid}); if (!val) { - field_input_incorrect(this.fieldElementById(field_id)); - this.props.onError(error_code); + fieldInputIncorrect(this.fieldElementById(fieldID)); + this.props.onError(errorCode); } }, - fieldElementById(field_id) { - switch (field_id) { + fieldElementById(fieldID) { + switch (fieldID) { case FIELD_EMAIL: return this.refs.email; case FIELD_PHONE_NUMBER: @@ -262,9 +229,9 @@ module.exports = React.createClass({ } }, - _classForField: function(field_id, ...baseClasses) { + _classForField: function(fieldID, ...baseClasses) { let cls = baseClasses.join(' '); - if (this.state.fieldValid[field_id] === false) { + if (this.state.fieldValid[fieldID] === false) { if (cls) cls += ' '; cls += 'error'; } @@ -289,7 +256,9 @@ module.exports = React.createClass({ render: function() { const self = this; - const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ? _t("Email address") : _t("Email address (optional)"); + const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ? + _t("Email address") : + _t("Email address (optional)"); const emailSection = (
@@ -301,32 +270,13 @@ module.exports = React.createClass({ value={self.state.email} />
); - let belowEmailSection; - if (this.props.teamsConfig) { - if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { - belowEmailSection = ( -

- Sorry, but your university is not registered with us just yet.  - Email us on  - - { this.props.teamsConfig.supportEmail } -   - to get your university signed up. Or continue to register with Riot to enjoy our open source platform. -

- ); - } else if (this.state.selectedTeam) { - belowEmailSection = ( -

- { _t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name}) } -

- ); - } - } const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); let phoneSection; if (!SdkConfig.get().disable_3pid_login) { - const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ? _t("Mobile phone number") : _t("Mobile phone number (optional)"); + const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ? + _t("Mobile phone number") : + _t("Mobile phone number (optional)"); phoneSection = (
{ emailSection } - { belowEmailSection } { phoneSection } { + this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { let hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); if (hsUrl === "") hsUrl = this.props.defaultHsUrl; this.props.onServerConfigChange({ @@ -101,8 +101,8 @@ module.exports = React.createClass({ }, onIdentityServerChanged: function(ev) { - this.setState({is_url: ev.target.value}, function() { - this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { + this.setState({is_url: ev.target.value}, () => { + this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { let isUrl = this.state.is_url.trim().replace(/\/$/, ""); if (isUrl === "") isUrl = this.props.defaultIsUrl; this.props.onServerConfigChange({ diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 8c74b3aff5..1f81472cc0 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -138,7 +138,9 @@ export default class LogoutDialog extends React.Component { // once you can restorew a backup by verifying a device description={_t( "When signing in again, you can access encrypted chat history by " + - "restoring your key backup. You'll need your recovery key.", + "restoring your key backup. You'll need your recovery passphrase " + + "or, if you didn't set a recovery passphrase, your recovery key " + + "(that you downloaded).", )} button={_t("Sign out")} onFinished={this._onFinished} diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js new file mode 100644 index 0000000000..ce834d564e --- /dev/null +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -0,0 +1,90 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {Tab, TabbedView} from "../../structures/TabbedView"; +import {_t, _td} from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import dis from '../../../dispatcher'; + +// TODO: Ditch this whole component +export class TempTab extends React.Component { + static propTypes = { + onClose: PropTypes.func.isRequired, + }; + + componentDidMount(): void { + dis.dispatch({action: "open_old_room_settings"}); + this.props.onClose(); + } + + render() { + return
Hello World
; + } +} + +export default class UserSettingsDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _getTabs() { + const tabs = []; + + tabs.push(new Tab( + _td("General"), + "mx_RoomSettingsDialog_settingsIcon", +
General Test
, + )); + tabs.push(new Tab( + _td("Security & Privacy"), + "mx_RoomSettingsDialog_securityIcon", +
Security Test
, + )); + tabs.push(new Tab( + _td("Roles & Permissions"), + "mx_RoomSettingsDialog_rolesIcon", +
Roles Test
, + )); + tabs.push(new Tab( + _td("Advanced"), + "mx_RoomSettingsDialog_warningIcon", +
Advanced Test
, + )); + tabs.push(new Tab( + _td("Visit old settings"), + "mx_RoomSettingsDialog_warningIcon", + , + )); + + return tabs; + } + + render() { + return ( +
+
+ {_t("Settings")} + + + +
+ +
+ ); + } +} diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 222a2c35fe..6f11a28eae 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -193,9 +193,6 @@ export default React.createClass({ return; } - // XXX Implement RTS /register here - const teamToken = null; - this.props.onFinished(true, { userId: response.user_id, deviceId: response.device_id, @@ -203,7 +200,6 @@ export default React.createClass({ identityServerUrl: this._matrixClient.getIdentityServerUrl(), accessToken: response.access_token, password: this._generatedPassword, - teamToken: teamToken, }); }, diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js index 42c35ad187..fe3a2216f4 100644 --- a/src/components/views/dialogs/SetPasswordDialog.js +++ b/src/components/views/dialogs/SetPasswordDialog.js @@ -116,9 +116,8 @@ export default React.createClass({ , - ), - new Tab( - _td("Notifications"), - "mx_UserSettingsDialog_bellIcon", -
Notifications Test
, - ), - new Tab( - _td("Preferences"), - "mx_UserSettingsDialog_preferencesIcon", -
Preferences Test
, - ), - new Tab( - _td("Voice & Video"), - "mx_UserSettingsDialog_voiceIcon", -
Voice Test
, - ), - new Tab( - _td("Security & Privacy"), - "mx_UserSettingsDialog_securityIcon", -
Security Test
, - ), - new Tab( - _td("Help & About"), - "mx_UserSettingsDialog_helpIcon", -
Help Test
, - ), - new Tab( - _td("Visit old settings"), - "mx_UserSettingsDialog_helpIcon", - , - ), - ]; + const tabs = []; + + tabs.push(new Tab( + _td("General"), + "mx_UserSettingsDialog_settingsIcon", + , + )); + tabs.push(new Tab( + _td("Notifications"), + "mx_UserSettingsDialog_bellIcon", + , + )); + tabs.push(new Tab( + _td("Preferences"), + "mx_UserSettingsDialog_preferencesIcon", + , + )); + tabs.push(new Tab( + _td("Voice & Video"), + "mx_UserSettingsDialog_voiceIcon", + , + )); + tabs.push(new Tab( + _td("Security & Privacy"), + "mx_UserSettingsDialog_securityIcon", + , + )); + if (SettingsStore.getLabsFeatures().length > 0) { + tabs.push(new Tab( + _td("Labs"), + "mx_UserSettingsDialog_labsIcon", + , + )); + } + tabs.push(new Tab( + _td("Help & About"), + "mx_UserSettingsDialog_helpIcon", + , + )); + tabs.push(new Tab( + _td("Visit old settings"), + "mx_UserSettingsDialog_helpIcon", + , + )); + + return tabs; } render() { return (
-
+
{_t("Settings")} - - + +
diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index b88fbdfc06..cfd79529f1 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -42,7 +42,9 @@ export default React.createClass({ componentWillUnmount: function() { const cli = MatrixClientPeg.get(); - cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + if (cli) { + cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + } }, onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) { diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 69b890b911..774ec1db88 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -25,12 +25,24 @@ export default class Field extends React.PureComponent { type: PropTypes.string, // The field's label string. label: PropTypes.string, - // The field's placeholder string. + // The field's placeholder string. Defaults to the label. placeholder: PropTypes.string, // The type of field to create. Defaults to "input". Should be "input" or "select". // To define options for a select, use element: PropTypes.string, // All other props pass through to the . + }; + + 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() { @@ -42,6 +54,8 @@ export default class Field extends React.PureComponent { // Set some defaults for the element extraProps.type = extraProps.type || "text"; + extraProps.ref = "fieldInput"; + extraProps.placeholder = extraProps.placeholder || extraProps.label; const element = this.props.element || "input"; const fieldInput = React.createElement(element, extraProps, this.props.children); diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.js new file mode 100644 index 0000000000..3d3025ad06 --- /dev/null +++ b/src/components/views/elements/LabelledToggleSwitch.js @@ -0,0 +1,42 @@ +/* +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 ToggleSwitch from "./ToggleSwitch"; + +export default class LabelledToggleSwitch extends React.Component { + static propTypes = { + // The value for the toggle switch + value: PropTypes.bool.isRequired, + + // The function to call when the value changes + onChange: PropTypes.func.isRequired, + + // The translated label for the switch + label: PropTypes.string.isRequired, + }; + + render() { + // This is a minimal version of a SettingsFlag + return ( +
+ {this.props.label} + +
+ ); + } +} diff --git a/src/components/views/elements/SettingsFlag.js b/src/components/views/elements/SettingsFlag.js index 7f6c74538a..f1bd72f53d 100644 --- a/src/components/views/elements/SettingsFlag.js +++ b/src/components/views/elements/SettingsFlag.js @@ -18,6 +18,7 @@ import React from "react"; import PropTypes from 'prop-types'; import SettingsStore from "../../../settings/SettingsStore"; import { _t } from '../../../languageHandler'; +import ToggleSwitch from "./ToggleSwitch"; module.exports = React.createClass({ displayName: 'SettingsFlag', @@ -29,10 +30,6 @@ module.exports = React.createClass({ onChange: PropTypes.func, isExplicit: 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() { @@ -46,13 +43,12 @@ module.exports = React.createClass({ }; }, - onChange: function(e) { - if (this.props.group && !e.target.checked) return; + onChange: function(checked) { + if (this.props.group && !checked) return; - const newState = this.props.group ? this.props.value : e.target.checked; - if (!this.props.manualSave) this.save(newState); - else this.setState({ value: newState }); - if (this.props.onChange) this.props.onChange(newState); + if (!this.props.manualSave) this.save(checked); + else this.setState({ value: checked }); + if (this.props.onChange) this.props.onChange(checked); }, save: function(val = undefined) { @@ -78,34 +74,11 @@ module.exports = React.createClass({ if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level); 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 = ( - - ); - if (this.props.group) { - checkbox = ( - - ); - } - return ( - +
+ {label} + +
); }, }); diff --git a/src/components/views/elements/ToggleSwitch.js b/src/components/views/elements/ToggleSwitch.js new file mode 100644 index 0000000000..19c87aab56 --- /dev/null +++ b/src/components/views/elements/ToggleSwitch.js @@ -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 ( +
+
+
+ ); + } +} diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 0405d411d2..a349a34caf 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -68,15 +68,12 @@ export default React.createClass({ text = _t("You're not currently a member of any communities."); } - return
-

{ _t('Flair') }

-
-

- { text } -

+ return ( +
+

{ text }

{ scrollbox }
-
; + ); }, render() { diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index f0479eb8be..3f5f58121d 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -78,7 +78,6 @@ export default class HeaderButtons extends React.Component { // till show_right_panel, just without the fromHeader flag // as that would hide the right panel again dis.dispatch(Object.assign({}, payload, {fromHeader: false})); - } this.setState({ phase: payload.phase, diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 01447012e6..e8c315ecf5 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -62,7 +62,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { let unverifiedDevice; for (const sig of backupSigStatus.sigs) { - if (!sig.device.isVerified()) { + if (sig.device && !sig.device.isVerified()) { unverifiedDevice = sig.device; break; } @@ -133,6 +133,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); let body; + let primaryCaption = _t("Set up"); if (this.state.error) { body =
{_t("Unable to load key backup status")} @@ -140,10 +141,20 @@ export default class RoomRecoveryReminder extends React.PureComponent { } else if (this.state.unverifiedDevice) { // A key backup exists for this account, but the creating device is not // verified. - body = _t( - "To view your secure message history and ensure you can view new " + - "messages on future devices, set up Secure Message Recovery.", - ); + body =
+

{_t( + "Secure Message Recovery has been set up on another device: ", + {}, + { + deviceName: () => {this.state.unverifiedDevice.unsigned.device_display_name}, + }, + )}

+

{_t( + "To view your secure message history and ensure you can view new " + + "messages on future devices, verify that device now.", + )}

+
; + primaryCaption = _t("Verify device"); } else { // The default case assumes that a key backup doesn't exist for this account. // (This component doesn't currently check that itself.) @@ -167,7 +178,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { - { _t("Set up") } + {primaryCaption}
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 77c7810436..69b80b03b3 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -1,6 +1,6 @@ /* 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"); 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. */ +import Field from "../elements/Field"; + const React = require('react'); import PropTypes from 'prop-types'; const MatrixClientPeg = require("../../../MatrixClientPeg"); @@ -35,9 +37,8 @@ module.exports = React.createClass({ onError: PropTypes.func, onCheckPassword: PropTypes.func, rowClassName: PropTypes.string, - rowLabelClassName: PropTypes.string, - rowInputClassName: PropTypes.string, buttonClassName: PropTypes.string, + buttonKind: PropTypes.string, confirm: PropTypes.bool, // Whether to autoFocus the new password input autoFocusNewPasswordInput: PropTypes.bool, @@ -114,7 +115,11 @@ module.exports = React.createClass({ 'making encrypted chat history unreadable, unless you first export your room keys ' + 'and re-import them afterwards. ' + 'In future this will be improved.', - ) } (https://github.com/vector-im/riot-web/issues/2671) + ) } + {' '} + + https://github.com/vector-im/riot-web/issues/2671 +
, button: _t("Continue"), extraButtons: [ @@ -203,21 +208,18 @@ module.exports = React.createClass({ }, render: function() { + // TODO: Live validation on `new pw == confirm pw` + const rowClassName = this.props.rowClassName; - const rowLabelClassName = this.props.rowLabelClassName; - const rowInputClassName = this.props.rowInputClassName; const buttonClassName = this.props.buttonClassName; let currentPassword = null; if (!this.state.cachedPassword) { - currentPassword =
-
- + currentPassword = ( +
+
-
- -
-
; + ); } switch (this.state.phase) { @@ -228,24 +230,13 @@ module.exports = React.createClass({ { currentPassword }
-
- -
-
- -
+
-
- -
-
- -
+
- + { _t('Change Password') } diff --git a/src/components/views/settings/EmailAddresses.js b/src/components/views/settings/EmailAddresses.js new file mode 100644 index 0000000000..d8577f6a99 --- /dev/null +++ b/src/components/views/settings/EmailAddresses.js @@ -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 ( +
+ + {_t("Are you sure?")} + + + {_t("Yes")} + + + {_t("No")} + +
+ ); + } + + return ( +
+ {_t("Remove")} + {this.props.email.address} +
+ ); + } +} + +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 ; + }); + + let addButton = ( + + {_t("Add")} + + ); + if (this.state.verifying) { + addButton = ( +
+
{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
+ + {_t("Continue")} + +
+ ); + } + + return ( +
+ {existingEmailElements} +
+ + {addButton} + +
+ ); + } +} diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 8dc817faad..4c32e3eaf8 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -257,20 +257,17 @@ export default class KeyBackupPanel extends React.PureComponent { {uploadStatus}
{backupSigStatuses}


- + { _t("Restore backup") }     - + { _t("Delete backup") }
; } else { return
{_t("No backup is present")}

- + { _t("Start a new backup") }
; diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 40c43e6b2e..c502ef6bc6 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -28,6 +28,8 @@ import { PushRuleVectorState, ContentRules, } from '../../../notifications'; +import * as SdkConfig from "../../../SdkConfig"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; // TODO: this "view" component still has far too much application logic in it, // which should be factored out to other files. @@ -70,19 +72,6 @@ module.exports = React.createClass({ ERROR: "ERROR", // There was an error }, - propTypes: { - // The array of threepids from the JS SDK (required for email notifications) - threepids: React.PropTypes.array.isRequired, - // The brand string set when creating an email pusher - brand: React.PropTypes.string, - }, - - getDefaultProps: function() { - return { - threepids: [], - }; - }, - getInitialState: function() { return { phase: this.phases.LOADING, @@ -94,6 +83,7 @@ module.exports = React.createClass({ }, externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI externalContentRules: [], // Keyword push rules that have been defined outside Vector UI + threepids: [], // used for email notifications }; }, @@ -101,42 +91,42 @@ module.exports = React.createClass({ this._refreshFromServer(); }, - onEnableNotificationsChange: function(event) { + onEnableNotificationsChange: function(checked) { const self = this; this.setState({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !event.target.checked).done(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { self._refreshFromServer(); }); }, - onEnableDesktopNotificationsChange: function(event) { + onEnableDesktopNotificationsChange: function(checked) { SettingsStore.setValue( "notificationsEnabled", null, SettingLevel.DEVICE, - event.target.checked, + checked, ).finally(() => { this.forceUpdate(); }); }, - onEnableDesktopNotificationBodyChange: function(event) { + onEnableDesktopNotificationBodyChange: function(checked) { SettingsStore.setValue( "notificationBodyEnabled", null, SettingLevel.DEVICE, - event.target.checked, + checked, ).finally(() => { this.forceUpdate(); }); }, - onEnableAudioNotificationsChange: function(event) { + onEnableAudioNotificationsChange: function(checked) { SettingsStore.setValue( "audioNotificationsEnabled", null, SettingLevel.DEVICE, - event.target.checked, + checked, ).finally(() => { this.forceUpdate(); }); @@ -146,7 +136,7 @@ module.exports = React.createClass({ let emailPusherPromise; if (event.target.checked) { const data = {}; - data['brand'] = this.props.brand || 'Riot'; + data['brand'] = SdkConfig.get().brand || 'Riot'; emailPusherPromise = UserSettingsStore.addEmailPusher(address, data); } else { const emailPusher = UserSettingsStore.getEmailPusher(this.state.pushers, address); @@ -434,7 +424,6 @@ module.exports = React.createClass({ // Check if any legacy im.vector rules need to be ported to the new API // for overriding the actions of default rules. _portRulesToNewAPI: function(rulesets) { - const self = this; const needsUpdate = []; const cli = MatrixClientPeg.get(); @@ -633,6 +622,8 @@ module.exports = React.createClass({ externalPushRules: self.state.externalPushRules, }); }).done(); + + MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, _updatePushRuleActions: function(rule, actions, enabled) { @@ -691,27 +682,25 @@ module.exports = React.createClass({ return rows; }, + hasEmailPusher: function(pushers, address) { + if (pushers === undefined) { + return false; + } + for (let i = 0; i < pushers.length; ++i) { + if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { + return true; + } + } + return false; + }, + emailNotificationsRow: function(address, label) { - return (
-
- -
-
- -
-
); + return ; }, render: function() { - const self = this; - let spinner; if (this.state.phase === this.phases.LOADING) { const Loader = sdk.getComponent("elements.Spinner"); @@ -720,23 +709,9 @@ module.exports = React.createClass({ let masterPushRuleDiv; if (this.state.masterPushRule) { - masterPushRuleDiv = ( -
-
- -
-
- -
-
- ); + masterPushRuleDiv = ; } // When enabled, the master rule inhibits all existing rules @@ -747,17 +722,17 @@ module.exports = React.createClass({ {masterPushRuleDiv}
- { _t('All notifications are currently disabled for all targets.') }. + { _t('All notifications are currently disabled for all targets.') }
); } - const emailThreepids = this.props.threepids.filter((tp) => tp.medium === "email"); + const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email"); let emailNotificationsRow; if (emailThreepids.length === 0) { emailNotificationsRow =
- { _t('Add an email address above to configure email notifications') } + { _t('Add an email address to configure email notifications') }
; } else { // This only supports the first email address in your profile for now @@ -788,7 +763,7 @@ module.exports = React.createClass({ let devicesSection; if (this.state.pushers === undefined) { devicesSection =
{ _t('Unable to fetch notification target list') }
; - } else if (this.state.pushers.length == 0) { + } else if (this.state.pushers.length === 0) { devicesSection = null; } else { // TODO: It would be great to be able to delete pushers from here too, @@ -836,50 +811,17 @@ module.exports = React.createClass({ { spinner } -
-
- -
-
- -
-
+ -
-
- -
-
- -
-
+ -
-
- -
-
- -
-
+ { emailNotificationsRow } diff --git a/src/components/views/settings/PhoneNumbers.js b/src/components/views/settings/PhoneNumbers.js new file mode 100644 index 0000000000..18abb621e7 --- /dev/null +++ b/src/components/views/settings/PhoneNumbers.js @@ -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 ( +
+ + {_t("Are you sure?")} + + + {_t("Yes")} + + + {_t("No")} + +
+ ); + } + + return ( +
+ {_t("Remove")} + +{this.props.msisdn.address} +
+ ); + } +} + +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 ; + }); + + let addVerifySection = ( + + {_t("Add")} + + ); + if (this.state.verifying) { + const msisdn = this.state.verifyMsisdn; + addVerifySection = ( +
+
+ {_t("A text message has been sent to +%(msisdn)s. " + + "Please enter the verification code it contains", {msisdn: msisdn})} +
+ {this.state.verifyError} +
+
+ + + {_t("Continue")} + + +
+ ); + } + + return ( +
+ {existingPhoneElements} +
+
+ + +
+ {addVerifySection} + +
+ ); + } +} diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js new file mode 100644 index 0000000000..649128e4f3 --- /dev/null +++ b/src/components/views/settings/ProfileSettings.js @@ -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 =
; + if (this.state.avatarUrl) { + showOverlayAnyways = false; + avatarElement = {_t("Profile; + } + + const avatarOverlayClasses = classNames({ + "mx_ProfileSettings_avatarOverlay": true, + "mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways, + }); + const avatarHoverElement = ( +
+ {_t("Upload profile picture")} +
+
+
+
+ ); + + return ( +
+ +
+
+

{this.state.userId}

+ +
+
+ {avatarElement} + {avatarHoverElement} +
+
+ + {_t("Save")} + + + ); + } +} diff --git a/src/components/views/settings/tabs/GeneralSettingsTab.js b/src/components/views/settings/tabs/GeneralSettingsTab.js index c8816325c0..c1df7f4665 100644 --- a/src/components/views/settings/tabs/GeneralSettingsTab.js +++ b/src/components/views/settings/tabs/GeneralSettingsTab.js @@ -17,69 +17,176 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../languageHandler"; 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 * 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 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 { + static childContextTypes = { + matrixClient: PropTypes.instanceOf(MatrixClient), + }; + constructor() { super(); - const client = MatrixClientPeg.get(); this.state = { - userId: client.getUserId(), - displayName: client.getUser(client.getUserId()).displayName, - enableProfileSave: false, + language: languageHandler.getCurrentLanguage(), + theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), }; } - _saveProfile = async (e) => { - e.stopPropagation(); - e.preventDefault(); + getChildContext() { + return { + matrixClient: MatrixClientPeg.get(), + }; + } - if (!this.state.enableProfileSave) return; - this.setState({enableProfileSave: false}); + _onLanguageChange = (newLanguage) => { + if (this.state.language === newLanguage) return; - // TODO: What do we do about errors? - await MatrixClientPeg.get().setDisplayName(this.state.displayName); - - // TODO: Support avatars - - this.setState({enableProfileSave: true}); + SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage); + this.setState({language: newLanguage}); + PlatformPeg.get().reload(); }; - _onDisplayNameChanged = (e) => { - this.setState({ - displayName: e.target.value, - enableProfileSave: true, + _onThemeChange = (e) => { + const newTheme = e.target.value; + if (this.state.theme === newTheme) return; + + 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() { - // TODO: Ditch avatar placeholder and use the real thing - const form = ( -
-
-
-

{this.state.userId}

- -
-
-
-
-
- - {_t("Save")} - - - ); + _onPasswordChanged = () => { + // TODO: Figure out a design that doesn't involve replacing the current dialog + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Password changed', '', ErrorDialog, { + title: _t("Success"), + description: _t( + "Your password was successfully changed. You will not receive " + + "push notifications on other devices until you log back in to them", + ) + ".", + }); + }; + _onDeactivateClicked = () => { + Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {}); + }; + + _renderProfileSection() { + // HACK/TODO: Using DragDropContext feels wrong, but we need it. return (
{_t("Profile")} - {form} + + + {_t("Flair")} + + + +
+ ); + } + + _renderAccountSection() { + const ChangePassword = sdk.getComponent("views.settings.ChangePassword"); + const passwordChangeForm = ( + + ); + + return ( +
+ {_t("Account")} +

+ {_t("Set a new account password...")} +

+ {passwordChangeForm} + + {_t("Email addresses")} + + + {_t("Phone numbers")} + +
+ ); + } + + _renderLanguageSection() { + // TODO: Convert to new-styled Field + return ( +
+ {_t("Language and region")} + +
+ ); + } + + _renderThemeSection() { + // TODO: Re-enable theme selection once the themes actually work + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + return ( +
+ {_t("Theme")} + + + + + + + +
+ ); + } + + _renderManagementSection() { + // TODO: Improve warning text for account deactivation + return ( +
+ {_t("Account management")} + + {_t("Deactivating your account is a permanent action - be careful!")} + + + {_t("Close Account")} +
); } @@ -89,6 +196,10 @@ export default class GeneralSettingsTab extends React.Component {
{_t("General")}
{this._renderProfileSection()} + {this._renderAccountSection()} + {this._renderLanguageSection()} + {this._renderThemeSection()} + {this._renderManagementSection()}
); } diff --git a/src/components/views/settings/tabs/HelpSettingsTab.js b/src/components/views/settings/tabs/HelpSettingsTab.js new file mode 100644 index 0000000000..c0892cf448 --- /dev/null +++ b/src/components/views/settings/tabs/HelpSettingsTab.js @@ -0,0 +1,227 @@ +/* +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, getCurrentLanguage} from "../../../../languageHandler"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import AccessibleButton from "../../elements/AccessibleButton"; +import SdkConfig from "../../../../SdkConfig"; +import createRoom from "../../../../createRoom"; +const packageJson = require('../../../../../package.json'); +const Modal = require("../../../../Modal"); +const sdk = require("../../../../index"); +const PlatformPeg = require("../../../../PlatformPeg"); + +// if this looks like a release, use the 'version' from package.json; else use +// the git sha. Prepend version with v, to look like riot-web version +const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || ''; + +// Simple method to help prettify GH Release Tags and Commit Hashes. +const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; +const ghVersionLabel = function(repo, token='') { + const match = token.match(semVerRegex); + let url; + if (match && match[1]) { // basic semVer string possibly with commit hash + url = (match.length > 1 && match[2]) + ? `https://github.com/${repo}/commit/${match[2]}` + : `https://github.com/${repo}/releases/tag/v${match[1]}`; + } else { + url = `https://github.com/${repo}/commit/${token.split('-')[0]}`; + } + return { token }; +}; + +export default class HelpSettingsTab extends React.Component { + static propTypes = { + closeSettingsFn: PropTypes.func.isRequired, + }; + + constructor() { + super(); + + this.state = { + vectorVersion: null, + canUpdate: false, + }; + } + + componentWillMount(): void { + PlatformPeg.get().getAppVersion().then((ver) => this.setState({vectorVersion: ver})).catch((e) => { + console.error("Error getting vector version: ", e); + }); + PlatformPeg.get().canSelfUpdate().then((v) => this.setState({canUpdate: v})).catch((e) => { + console.error("Error getting self updatability: ", e); + }); + } + + _onClearCacheAndReload = (e) => { + if (!PlatformPeg.get()) return; + + MatrixClientPeg.get().stopClient(); + MatrixClientPeg.get().store.deleteAllData().done(() => { + PlatformPeg.get().reload(); + }); + }; + + _onBugReport = (e) => { + const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); + if (!BugReportDialog) { + return; + } + Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); + }; + + _onStartBotChat = (e) => { + this.props.closeSettingsFn(); + createRoom({ + dmUserId: SdkConfig.get().welcomeUserId, + andView: true, + }); + }; + + _showSpoiler = (event) => { + const target = event.target; + target.innerHTML = target.getAttribute('data-spoiler'); + + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }; + + _renderLegal() { + const tocLinks = SdkConfig.get().terms_and_conditions_links; + if (!tocLinks) return null; + + const legalLinks = []; + for (const tocEntry of SdkConfig.get().terms_and_conditions_links) { + legalLinks.push(); + } + + return ( +
+ {_t("Legal")} +
+ {legalLinks} +
+
+ ); + } + + render() { + let faqText = _t('For help with using Riot, click here.', {}, { + 'a': (sub) => {sub}, + }); + if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) { + faqText = ( +
+ { + _t('For help with using Riot, click here or start a chat with our ' + + 'bot using the button below.', {}, { + 'a': (sub) => {sub}, + }) + } + + {_t("Start a chat with Riot Bot")} + +
+ ); + } + + const reactSdkVersion = REACT_SDK_VERSION !== '' + ? ghVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) + : REACT_SDK_VERSION; + const vectorVersion = this.state.vectorVersion + ? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion) + : 'unknown'; + + let olmVersion = MatrixClientPeg.get().olmVersion; + olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; + + let updateButton = null; + if (this.state.canUpdate) { + const platform = PlatformPeg.get(); + updateButton = ( + + {_t('Check for update')} + + ); + } + + return ( +
+
{_t("Help & About")}
+
+ {_t('Bug reporting')} +
+ { + _t( "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contain messages.", + ) + } +
+ + {_t("Submit debug logs")} + +
+
+ + {_t("Clear Cache and Reload")} + +
+
+
+
+ {_t("FAQ")} +
+ {faqText} +
+
+
+ {_t("Versions")} +
+ {_t("matrix-react-sdk version:")} {reactSdkVersion}
+ {_t("riot-web version:")} {vectorVersion}
+ {_t("olm version:")} {olmVersion}
+ {updateButton} +
+
+ {this._renderLegal()} +
+ {_t("Advanced")} +
+ {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
+ {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
+ {_t("Access Token:") + ' '} + + <{ _t("click to reveal") }> + +
+
+
+ ); + } +} diff --git a/src/components/views/settings/tabs/LabsSettingsTab.js b/src/components/views/settings/tabs/LabsSettingsTab.js new file mode 100644 index 0000000000..fc64c1bd04 --- /dev/null +++ b/src/components/views/settings/tabs/LabsSettingsTab.js @@ -0,0 +1,92 @@ +/* +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 PropTypes from "prop-types"; +import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; +const Modal = require("../../../../Modal"); +const sdk = require("../../../../index"); + +export class LabsSettingToggle extends React.Component { + static propTypes = { + featureId: PropTypes.string.isRequired, + }; + + async _onLazyLoadChanging(enabling) { + // don't prevent turning LL off when not supported + if (enabling) { + const supported = await MatrixClientPeg.get().doesServerSupportLazyLoading(); + if (!supported) { + await new Promise((resolve) => { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createDialog(QuestionDialog, { + title: _t("Lazy loading members not supported"), + description: +
+ { _t("Lazy loading is not supported by your " + + "current homeserver.") } +
, + button: _t("OK"), + onFinished: resolve, + }); + }); + return false; + } + } + return true; + } + + _onChange = async (checked) => { + if (this.props.featureId === "feature_lazyloading") { + const confirmed = await this._onLazyLoadChanging(checked); + if (!confirmed) { + return; + } + } + + await SettingsStore.setFeatureEnabled(this.props.featureId, checked); + this.forceUpdate(); + }; + + render() { + const label = _t(SettingsStore.getDisplayName(this.props.featureId)); + const value = SettingsStore.isFeatureEnabled(this.props.featureId); + return ; + } +} + +export default class LabsSettingsTab extends React.Component { + constructor() { + super(); + } + + render() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + const flags = SettingsStore.getLabsFeatures().map(f => ); + return ( +
+
{_t("Labs")}
+
+ {flags} + +
+
+ ); + } +} diff --git a/src/components/views/settings/tabs/NotificationSettingsTab.js b/src/components/views/settings/tabs/NotificationSettingsTab.js new file mode 100644 index 0000000000..3a4e79f34c --- /dev/null +++ b/src/components/views/settings/tabs/NotificationSettingsTab.js @@ -0,0 +1,37 @@ +/* +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"; +const sdk = require("../../../../index"); + +export default class NotificationSettingsTab extends React.Component { + constructor() { + super(); + } + + render() { + const Notifications = sdk.getComponent("views.settings.Notifications"); + return ( +
+
{_t("Notifications")}
+
+ +
+
+ ); + } +} diff --git a/src/components/views/settings/tabs/PreferencesSettingsTab.js b/src/components/views/settings/tabs/PreferencesSettingsTab.js new file mode 100644 index 0000000000..d0f3f2b36f --- /dev/null +++ b/src/components/views/settings/tabs/PreferencesSettingsTab.js @@ -0,0 +1,125 @@ +/* +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 {SettingLevel} from "../../../../settings/SettingsStore"; +import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; +import SettingsStore from "../../../../settings/SettingsStore"; +import Field from "../../elements/Field"; +const sdk = require("../../../../index"); +const PlatformPeg = require("../../../../PlatformPeg"); + +export default class PreferencesSettingsTab extends React.Component { + static COMPOSER_SETTINGS = [ + 'MessageComposerInput.autoReplaceEmoji', + 'MessageComposerInput.dontSuggestEmoji', // TODO: Positive wording + 'dontSendTypingNotifications', // TODO: Positive wording + ]; + + static ROOM_LIST_SETTINGS = [ + 'pinUnreadRooms', + 'pinMentionedRooms', + ]; + + static TIMELINE_SETTINGS = [ + 'autoplayGifsAndVideos', + 'urlPreviewsEnabled', + 'TextualBody.disableBigEmoji', // TODO: Positive wording + 'hideReadReceipts', // TODO: Positive wording + 'showTwelveHourTimestamps', + 'alwaysShowTimestamps', + 'hideRedactions', // TODO: Positive wording ("Show a placeholder for removed messages") + 'enableSyntaxHighlightLanguageDetection', + 'hideJoinLeaves', // TODO: Positive wording + 'hideAvatarChanges', // TODO: Positive wording + 'hideDisplaynameChanges', // TODO: Positive wording + ]; + + static ADVANCED_SETTINGS = [ + 'alwaysShowEncryptionIcons', + 'Pill.shouldHidePillAvatar', // TODO: Positive wording + 'TagPanel.disableTagPanel', // TODO: Positive wording + 'promptBeforeInviteUnknownUsers', + // Start automatically after startup (electron-only) + // Autocomplete delay (niche text box) + ]; + + constructor() { + super(); + + this.state = { + autoLaunch: false, + autoLaunchSupported: false, + }; + } + + async componentWillMount(): void { + const autoLaunchSupported = await PlatformPeg.get().supportsAutoLaunch(); + let autoLaunch = false; + + if (autoLaunchSupported) { + autoLaunch = await PlatformPeg.get().getAutoLaunchEnabled(); + } + + this.setState({autoLaunch, autoLaunchSupported}); + } + + _onAutoLaunchChange = (checked) => { + PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); + }; + + _onAutocompleteDelayChange = (e) => { + SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); + }; + + _renderGroup(settingIds) { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + return settingIds.map(i => ); + } + + render() { + let autoLaunchOption = null; + if (this.state.autoLaunchSupported) { + autoLaunchOption = ; + } + + return ( +
+
{_t("Preferences")}
+
+ {_t("Composer")} + {this._renderGroup(PreferencesSettingsTab.COMPOSER_SETTINGS)} + + {_t("Room list")} + {this._renderGroup(PreferencesSettingsTab.ROOM_LIST_SETTINGS)} + + {_t("Timeline")} + {this._renderGroup(PreferencesSettingsTab.TIMELINE_SETTINGS)} + + {_t("Advanced")} + {this._renderGroup(PreferencesSettingsTab.ADVANCED_SETTINGS)} + {autoLaunchOption} + +
+
+ ); + } +} diff --git a/src/components/views/settings/tabs/SecuritySettingsTab.js b/src/components/views/settings/tabs/SecuritySettingsTab.js new file mode 100644 index 0000000000..a2a30a2190 --- /dev/null +++ b/src/components/views/settings/tabs/SecuritySettingsTab.js @@ -0,0 +1,242 @@ +/* +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 SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import * as FormattingUtils from "../../../../utils/FormattingUtils"; +import AccessibleButton from "../../elements/AccessibleButton"; +import Analytics from "../../../../Analytics"; +import Promise from "bluebird"; +import Modal from "../../../../Modal"; +import sdk from "../../../../index"; + +export class IgnoredUser extends React.Component { + static propTypes = { + userId: PropTypes.string.isRequired, + onUnignored: PropTypes.func.isRequired, + }; + + _onUnignoreClicked = (e) => { + this.props.onUnignored(this.props.userId); + }; + + render() { + return ( +
+ + {_t('Unignore')} + + {this.props.userId} +
+ ); + } +} + +export default class SecuritySettingsTab extends React.Component { + constructor() { + super(); + + this.state = { + ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(), + rejectingInvites: false, + }; + } + + _updateBlacklistDevicesFlag = (checked) => { + MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); + }; + + _updateAnalytics = (checked) => { + checked ? Analytics.enable() : Analytics.disable(); + }; + + _onExportE2eKeysClicked = () => { + Modal.createTrackedDialogAsync('Export E2E Keys', '', + import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'), + {matrixClient: MatrixClientPeg.get()}, + ); + }; + + _onImportE2eKeysClicked = () => { + Modal.createTrackedDialogAsync('Import E2E Keys', '', + import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'), + {matrixClient: MatrixClientPeg.get()}, + ); + }; + + _onUserUnignored = async (userId) => { + // Don't use this.state to get the ignored user list as it might be + // ever so slightly outdated. Instead, prefer to get a fresh list and + // update that. + const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); + const index = ignoredUsers.indexOf(userId); + if (index !== -1) { + ignoredUsers.splice(index, 1); + MatrixClientPeg.get().setIgnoredUsers(ignoredUsers); + } + this.setState({ignoredUsers}); + }; + + _onRejectAllInvitesClicked = (rooms, ev) => { + this.setState({ + rejectingInvites: true, + }); + // reject the invites + const promises = rooms.map((room) => { + return MatrixClientPeg.get().leave(room.roomId).catch((e) => { + // purposefully drop errors to the floor: we'll just have a non-zero number on the UI + // after trying to reject all the invites. + }); + }); + Promise.all(promises).then(() => { + this.setState({ + rejectingInvites: false, + }); + }); + }; + + _renderCurrentDeviceInfo() { + const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag'); + + const client = MatrixClientPeg.get(); + const deviceId = client.deviceId; + let identityKey = client.getDeviceEd25519Key(); + if (!identityKey) { + identityKey = _t(""); + } else { + identityKey = FormattingUtils.formatCryptoKey(identityKey); + } + + let importExportButtons = null; + if (client.isCryptoEnabled()) { + importExportButtons = ( +
+ + {_t("Export E2E room keys")} + + + {_t("Import E2E room keys")} + +
+ ); + } + + return ( +
+ {_t("Cryptography")} +
    +
  • + + {deviceId} +
  • +
  • + + {identityKey} +
  • +
+ {importExportButtons} + +
+ ); + } + + _renderIgnoredUsers() { + if (!this.state.ignoredUserIds || this.state.ignoredUserIds.length === 0) return null; + + const userIds = this.state.ignoredUserIds + .map((u) => ); + + return ( +
+ {_t('Ignored users')} +
+ {userIds} +
+
+ ); + } + + _renderRejectInvites() { + const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => { + return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite"); + }); + if (invitedRooms.length === 0) { + return null; + } + + const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms); + return ( +
+ {_t('Bulk options')} + + {_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})} + +
+ ); + } + + render() { + const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel'); + const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag'); + + let keyBackup = null; + if (SettingsStore.isFeatureEnabled("feature_keybackup")) { + const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel'); + keyBackup = ( +
+ {_t("Key backup")} +
+ +
+
+ ); + } + + return ( +
+
{_t("Security & Privacy")}
+
+ {_t("Devices")} +
+ +
+
+ {keyBackup} + {this._renderCurrentDeviceInfo()} +
+ {_t("Analytics")} +
+ {_t("Riot collects anonymous analytics to allow us to improve the application.")} +   + {_t("Privacy is important to us, so we don't collect any personal or " + + "identifiable data for our analytics.")} + + {_t("Learn more about how we use analytics.")} + +
+ +
+ {this._renderIgnoredUsers()} + {this._renderRejectInvites()} +
+ ); + } +} diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js new file mode 100644 index 0000000000..f03711fb96 --- /dev/null +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -0,0 +1,177 @@ +/* +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 CallMediaHandler from "../../../../CallMediaHandler"; +import Field from "../../elements/Field"; +import AccessibleButton from "../../elements/AccessibleButton"; +import {SettingLevel} from "../../../../settings/SettingsStore"; +const Modal = require("../../../../Modal"); +const sdk = require("../../../../index"); +const MatrixClientPeg = require("../../../../MatrixClientPeg"); + +export default class VoiceSettingsTab extends React.Component { + constructor() { + super(); + + this.state = { + mediaDevices: null, + activeAudioOutput: null, + activeAudioInput: null, + activeVideoInput: null, + }; + } + + componentWillMount(): void { + this._refreshMediaDevices(); + } + + _refreshMediaDevices = async () => { + this.setState({ + mediaDevices: await CallMediaHandler.getDevices(), + activeAudioOutput: CallMediaHandler.getAudioOutput(), + activeAudioInput: CallMediaHandler.getAudioInput(), + activeVideoInput: CallMediaHandler.getVideoInput(), + }); + }; + + _requestMediaPermissions = () => { + const getUserMedia = ( + window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia + ); + if (getUserMedia) { + return getUserMedia.apply(window.navigator, [ + { video: true, audio: true }, + this._refreshMediaDevices, + function() { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { + title: _t('No media permissions'), + description: _t('You may need to manually permit Riot to access your microphone/webcam'), + }); + }, + ]); + } + }; + + _setAudioOutput = (e) => { + CallMediaHandler.setAudioOutput(e.target.value); + }; + + _setAudioInput = (e) => { + CallMediaHandler.setAudioInput(e.target.value); + }; + + _setVideoInput = (e) => { + CallMediaHandler.setVideoInput(e.target.value); + }; + + _setForceTurn = (forced) => { + MatrixClientPeg.get().setForceTURN(forced); + }; + + _renderDeviceOptions(devices, category) { + return devices.map((d) => ); + } + + render() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + + let requestButton = null; + let speakerDropdown = null; + let microphoneDropdown = null; + let webcamDropdown = null; + if (this.state.mediaDevices === false) { + requestButton = ( +
+

{_t("Missing media permissions, click the button below to request.")}

+ + {_t("Request media permissions")} + +
+ ); + } else if (this.state.mediaDevices) { + speakerDropdown =

{ _t('No Audio Outputs detected') }

; + microphoneDropdown =

{ _t('No Microphones detected') }

; + webcamDropdown =

{ _t('No Webcams detected') }

; + + const defaultOption = { + deviceId: '', + label: _t('Default Device'), + }; + const getDefaultDevice = (devices) => { + if (!devices.some((i) => i.deviceId === 'default')) { + devices.unshift(defaultOption); + return ''; + } else { + return 'default'; + } + }; + + const audioOutputs = this.state.mediaDevices.audiooutput.slice(0); + if (audioOutputs.length > 0) { + const defaultDevice = getDefaultDevice(audioOutputs); + speakerDropdown = ( + + {this._renderDeviceOptions(audioOutputs, 'audioOutput')} + + ); + } + + const audioInputs = this.state.mediaDevices.audioinput.slice(0); + if (audioInputs.length > 0) { + const defaultDevice = getDefaultDevice(audioInputs); + microphoneDropdown = ( + + {this._renderDeviceOptions(audioInputs, 'audioInput')} + + ); + } + + const videoInputs = this.state.mediaDevices.videoinput.slice(0); + if (videoInputs.length > 0) { + const defaultDevice = getDefaultDevice(videoInputs); + webcamDropdown = ( + + {this._renderDeviceOptions(videoInputs, 'videoInput')} + + ); + } + } + + // TODO: Make 'webRtcForceTURN' be positively worded + return ( +
+
{_t("Voice & Video")}
+
+ {requestButton} + {speakerDropdown} + {microphoneDropdown} + {webcamDropdown} + + +
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 24e5f27e15..d8b8b12479 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -327,8 +327,8 @@ "You've successfully verified this user.": "You've successfully verified this user.", "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.", "Got It": "Got It", - "Verify this user by confirming the following number appears on their screen": "Verify this user by confirming the following number appears on their screen", - "For maximum security, we reccommend you do this in person or use another trusted means of communication": "For maximum security, we reccommend you do this in person or use another trusted means of communication", + "Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.", + "For maximum security, we reccommend you do this in person or use another trusted means of communication.": "For maximum security, we reccommend you do this in person or use another trusted means of communication.", "To continue, click on each pair to confirm it's correct.": "To continue, click on each pair to confirm it's correct.", "Continue": "Continue", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", @@ -362,6 +362,17 @@ "Last seen": "Last seen", "Select devices": "Select devices", "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", "Enable Notifications": "Enable Notifications", "Delete Backup": "Delete Backup", @@ -398,7 +409,7 @@ "Notify me for anything else": "Notify me for anything else", "Enable notifications for this account": "Enable notifications for this account", "All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.", - "Add an email address above to configure email notifications": "Add an email address above to configure email notifications", + "Add an email address to configure email notifications": "Add an email address to configure email notifications", "Enable email notifications": "Enable email notifications", "Notifications on the following keywords follow rules which can’t be displayed here:": "Notifications on the following keywords follow rules which can’t be displayed here:", "Unable to fetch notification target list": "Unable to fetch notification target list", @@ -412,10 +423,89 @@ "Off": "Off", "On": "On", "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", "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", + "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", + "Legal": "Legal", + "For help with using Riot, click here.": "For help with using Riot, click here.", + "For help with using Riot, click here or start a chat with our bot using the button below.": "For help with using Riot, click here or start a chat with our bot using the button below.", + "Start a chat with Riot Bot": "Start a chat with Riot Bot", + "Check for update": "Check for update", + "Help & About": "Help & About", + "Bug reporting": "Bug reporting", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", + "Submit debug logs": "Submit debug logs", + "Clear Cache and Reload": "Clear Cache and Reload", + "FAQ": "FAQ", + "Versions": "Versions", + "matrix-react-sdk version:": "matrix-react-sdk version:", + "riot-web version:": "riot-web version:", + "olm version:": "olm version:", + "Advanced": "Advanced", + "Homeserver is": "Homeserver is", + "Identity Server is": "Identity Server is", + "Access Token:": "Access Token:", + "click to reveal": "click to reveal", + "Lazy loading members not supported": "Lazy loading members not supported", + "Lazy loading is not supported by your current homeserver.": "Lazy loading is not supported by your current homeserver.", + "Labs": "Labs", + "Notifications": "Notifications", + "Start automatically after system login": "Start automatically after system login", + "Preferences": "Preferences", + "Composer": "Composer", + "Room list": "Room list", + "Timeline": "Timeline", + "Autocomplete delay (ms)": "Autocomplete delay (ms)", + "Unignore": "Unignore", + "": "", + "Import E2E room keys": "Import E2E room keys", + "Cryptography": "Cryptography", + "Device ID:": "Device ID:", + "Device key:": "Device key:", + "Ignored users": "Ignored users", + "Bulk options": "Bulk options", + "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", + "Key backup": "Key backup", + "Security & Privacy": "Security & Privacy", + "Devices": "Devices", + "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", + "Learn more about how we use analytics.": "Learn more about how we use analytics.", + "No media permissions": "No media permissions", + "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", + "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", + "Request media permissions": "Request media permissions", + "No Audio Outputs detected": "No Audio Outputs detected", + "No Microphones detected": "No Microphones detected", + "No Webcams detected": "No Webcams detected", + "Default Device": "Default Device", + "Audio Output": "Audio Output", + "Microphone": "Microphone", + "Camera": "Camera", + "Voice & Video": "Voice & Video", "Cannot add any more widgets": "Cannot add any more widgets", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "Add a widget": "Add a widget", @@ -463,10 +553,7 @@ "Failed to toggle moderator status": "Failed to toggle moderator status", "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.", - "Are you sure?": "Are you sure?", "No devices with registered encryption keys": "No devices with registered encryption keys", - "Devices": "Devices", - "Unignore": "Unignore", "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", @@ -590,11 +677,13 @@ "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", - "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.": "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.", + "Set up": "Set up", + "Secure Message Recovery has been set up on another device: ": "Secure Message Recovery has been set up on another device: ", + "To view your secure message history and ensure you can view new messages on future devices, verify that device now.": "To view your secure message history and ensure you can view new messages on future devices, verify that device now.", + "Verify device": "Verify device", "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", "Secure Message Recovery": "Secure Message Recovery", "Don't ask again": "Don't ask again", - "Set up": "Set up", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", "To change the room's name, you must be a": "To change the room's name, you must be a", "To change the room's main address, you must be a": "To change the room's main address, you must be a", @@ -651,7 +740,6 @@ "Members only (since they were invited)": "Members only (since they were invited)", "Members only (since they joined)": "Members only (since they joined)", "Permissions": "Permissions", - "Advanced": "Advanced", "Internal room ID: ": "Internal room ID: ", "Room version number: ": "Room version number: ", "Add a topic": "Add a topic", @@ -682,7 +770,6 @@ "New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)", "Invalid community ID": "Invalid 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:", "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)", @@ -695,7 +782,6 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", - "Notifications": "Notifications", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -736,7 +822,6 @@ "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?", "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 '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "Something went wrong!": "Something went wrong!", @@ -874,7 +959,6 @@ "Logs sent": "Logs sent", "Thank you!": "Thank you!", "Failed to send logs: ": "Failed to send logs: ", - "Submit debug logs": "Submit debug logs", "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", "Before submitting logs, you must create a GitHub issue to describe your problem.": "Before submitting logs, you must create a GitHub issue to describe your problem.", "What GitHub issue are these logs for?": "What GitHub issue are these logs for?", @@ -919,7 +1003,6 @@ "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)", "To continue, please enter your password:": "To continue, please enter your password:", "password": "password", - "Verify device": "Verify device", "Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", "Verify by comparing a short text string.": "Verify by comparing a short text string.", "For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.", @@ -948,7 +1031,7 @@ "Developer Tools": "Developer Tools", "An error has occurred.": "An error has occurred.", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", - "Verifying this user will mark their device as trusted, and also mark your device as trusted to them": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them", + "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", "Waiting for partner to confirm...": "Waiting for partner to confirm...", "Incoming Verification Request": "Incoming Verification Request", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", @@ -969,11 +1052,13 @@ "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", "Set a Recovery Method": "Set a Recovery Method", "I understand, log out without": "I understand, log out without", - "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery key.": "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery key.", + "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).": "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).", "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", "Go back": "Go back", + "Roles & Permissions": "Roles & Permissions", + "Visit old settings": "Visit old settings", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", @@ -991,12 +1076,8 @@ "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.", "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", "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.", - "Unable to add email address": "Unable to add email address", - "Unable to verify email address.": "Unable to verify email address.", "Email address": "Email address", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", "Skip": "Skip", @@ -1013,7 +1094,6 @@ "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.", "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)", "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.", @@ -1029,11 +1109,6 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", - "Preferences": "Preferences", - "Voice & Video": "Voice & Video", - "Security & Privacy": "Security & Privacy", - "Help & About": "Help & About", - "Visit old settings": "Visit old settings", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", "No backup found!": "No backup found!", @@ -1111,13 +1186,12 @@ "Username on %(hs)s": "Username on %(hs)s", "User name": "User name", "Mobile phone number": "Mobile phone number", - "Forgot your password?": "Forgot your password?", + "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", "Sign in with": "Sign in with", "Sign in": "Sign in", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "Email address (optional)": "Email address (optional)", - "You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s", "Mobile phone number (optional)": "Mobile phone number (optional)", "Default server": "Default server", "Custom server": "Custom server", @@ -1255,72 +1329,30 @@ "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|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", "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 %(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:", "Interface Language": "Interface Language", "User Interface": "User Interface", "Autocomplete Delay (ms):": "Autocomplete Delay (ms):", - "": "", - "Import E2E room keys": "Import E2E room keys", "Key Backup": "Key Backup", - "Cryptography": "Cryptography", - "Device ID:": "Device ID:", - "Device key:": "Device key:", "Ignored Users": "Ignored Users", "Submit Debug Logs": "Submit Debug Logs", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", - "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", - "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", - "Learn more about how we use analytics.": "Learn more about how we use analytics.", - "Labs": "Labs", "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", "Use with caution": "Use with caution", - "Lazy loading members not supported": "Lazy loading members not supported", - "Lazy loading is not supported by your current homeserver.": "Lazy loading is not supported by your current homeserver.", "Deactivate my account": "Deactivate my account", - "Legal": "Legal", "Clear Cache": "Clear Cache", - "Clear Cache and Reload": "Clear Cache and Reload", "Updates": "Updates", - "Check for update": "Check for update", - "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", "Bulk Options": "Bulk Options", "Desktop specific": "Desktop specific", - "Start automatically after system login": "Start automatically after system login", - "No media permissions": "No media permissions", - "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.", - "No Audio Outputs detected": "No Audio Outputs detected", - "No Microphones detected": "No Microphones detected", - "No Webcams detected": "No Webcams detected", - "Default Device": "Default Device", - "Audio Output": "Audio Output", - "Microphone": "Microphone", - "Camera": "Camera", "VoIP": "VoIP", "Email": "Email", "Add email address": "Add email address", "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", "Logged in as:": "Logged in as:", - "Access Token:": "Access Token:", - "click to reveal": "click to reveal", - "Homeserver is": "Homeserver is", - "Identity Server is": "Identity Server is", - "matrix-react-sdk version:": "matrix-react-sdk version:", - "riot-web version:": "riot-web version:", - "olm version:": "olm version:", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "A new password must be entered.": "A new password must be entered.", @@ -1335,7 +1367,8 @@ "New password": "New password", "Confirm your new password": "Confirm your new password", "Send Reset Email": "Send Reset Email", - "Create an account": "Create an account", + "Sign in instead": "Sign in instead", + "Set a new password": "Set a new password", "Invalid homeserver discovery response": "Invalid homeserver discovery response", "Invalid identity server discovery response": "Invalid identity server discovery response", "General failure": "General failure", @@ -1353,6 +1386,8 @@ "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", "Sign in with single sign-on": "Sign in with single sign-on", "Try the app first": "Try the app first", + "Sign in to your account": "Sign in to your account", + "Create account": "Create account", "Failed to fetch avatar URL": "Failed to fetch avatar URL", "Set a display name:": "Set a display name:", "Upload an avatar:": "Upload an avatar:", @@ -1367,7 +1402,7 @@ "A phone number is required to register on this homeserver.": "A phone number is required to register on this homeserver.", "You need to enter a user name.": "You need to enter a user name.", "An unknown error occurred.": "An unknown error occurred.", - "I already have an account": "I already have an account", + "Create your account": "Create your account", "Commands": "Commands", "Results from DuckDuckGo": "Results from DuckDuckGo", "Emoji": "Emoji", diff --git a/src/index.js b/src/index.js index 8c290fcb64..7d0547d9c9 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,6 @@ limitations under the License. */ import Skinner from './Skinner'; -import RtsClient from './RtsClient'; module.exports.loadSkin = function(skinObject) { Skinner.load(skinObject); @@ -28,7 +27,3 @@ module.exports.resetSkin = function() { module.exports.getComponent = function(componentName) { return Skinner.getComponent(componentName); }; - -module.exports.setFetch = function(fetchFunction) { - RtsClient.setFetch(fetchFunction); -}; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 9e048e5d8e..036a7c04fc 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -20,6 +20,7 @@ import MatrixClientPeg from '../MatrixClientPeg'; import sdk from '../index'; import Modal from '../Modal'; import { _t } from '../languageHandler'; +import SettingsStore from "../settings/SettingsStore"; const INITIAL_STATE = { // Whether we're joining the currently viewed room (see isJoining()) @@ -119,6 +120,16 @@ class RoomViewStore extends Store { }); break; case 'open_room_settings': + if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) { + const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); + Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {}, 'mx_SettingsDialog'); + } else { + this._setState({ + isEditingSettings: true, + }); + } + break; + case 'open_old_room_settings': this._setState({ isEditingSettings: true, }); diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js index 545686cdb6..647436c131 100644 --- a/src/utils/PasswordScorer.js +++ b/src/utils/PasswordScorer.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Zxcvbn from 'zxcvbn'; +import zxcvbn from 'zxcvbn'; import MatrixClientPeg from '../MatrixClientPeg'; import { _t, _td } from '../languageHandler'; @@ -59,6 +59,9 @@ _td("Short keyboard patterns are easy to guess"); * Wrapper around zxcvbn password strength estimation * Include this only from async components: it pulls in zxcvbn * (obviously) which is large. + * + * @param {string} password Password to score + * @returns {object} Score result with `score` and `feedback` properties */ export function scorePassword(password) { if (password.length === 0) return null; @@ -66,10 +69,10 @@ export function scorePassword(password) { const userInputs = ZXCVBN_USER_INPUTS.slice(); userInputs.push(MatrixClientPeg.get().getUserIdLocalpart()); - let zxcvbnResult = Zxcvbn(password, userInputs); + let zxcvbnResult = zxcvbn(password, userInputs); // Work around https://github.com/dropbox/zxcvbn/issues/216 if (password.includes(' ')) { - const resultNoSpaces = Zxcvbn(password.replace(/ /g, ''), userInputs); + const resultNoSpaces = zxcvbn(password.replace(/ /g, ''), userInputs); if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces; } diff --git a/src/utils/Timer.js b/src/utils/Timer.js index 6d6dbb0996..ca06237fbf 100644 --- a/src/utils/Timer.js +++ b/src/utils/Timer.js @@ -26,7 +26,6 @@ Once a timer is finished or aborted, it can't be started again a new one through `clone()` or `cloneIfRun()`. */ export default class Timer { - constructor(timeout) { this._timeout = timeout; this._onTimeout = this._onTimeout.bind(this); diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js deleted file mode 100644 index 4827bf4c12..0000000000 --- a/test/components/structures/auth/Registration-test.js +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright 2017 Vector Creations 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. -*/ - -const jest = require('jest-mock'); -const React = require('react'); -const ReactDOM = require('react-dom'); -const ReactTestUtils = require('react-addons-test-utils'); -const expect = require('expect'); - -const testUtils = require('test-utils'); - -const sdk = require('matrix-react-sdk'); -const Registration = sdk.getComponent('structures.auth.Registration'); - -let rtsClient; -let client; - -const TEAM_CONFIG = { - supportEmail: 'support@some.domain', - teamServerURL: 'http://someteamserver.bla', -}; - -const CREDENTIALS = {userId: '@me:here'}; -const MOCK_REG_RESPONSE = { - user_id: CREDENTIALS.userId, - device_id: 'mydevice', - access_token: '2234569864534231', -}; - -describe('Registration', function() { - beforeEach(function() { - testUtils.beforeEach(this); - client = testUtils.createTestClient(); - client.credentials = CREDENTIALS; - - // Mock an RTS client that supports one team and naively returns team tokens when - // tracking by mapping email SIDs to team tokens. This is fine because we only - // want to assert the client behaviour such that a user recognised by the - // rtsClient (which would normally talk to the RTS server) as a team member is - // correctly logged in as one (and other such assertions). - rtsClient = testUtils.createTestRtsClient( - { - 'myawesometeam123': { - name: 'Team Awesome', - domain: 'team.awesome.net', - }, - }, - {'someEmailSid1234': 'myawesometeam123'}, - ); - }); - - it('should track a referral following successful registration of a team member', function(done) { - const expectedCreds = { - userId: MOCK_REG_RESPONSE.user_id, - deviceId: MOCK_REG_RESPONSE.device_id, - homeserverUrl: client.getHomeserverUrl(), - identityServerUrl: client.getIdentityServerUrl(), - accessToken: MOCK_REG_RESPONSE.access_token, - }; - const onLoggedIn = function(creds, teamToken) { - expect(creds).toEqual(expectedCreds); - expect(teamToken).toBe('myawesometeam123'); - done(); - }; - - const res = ReactTestUtils.renderIntoDocument( - , - ); - - res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someEmailSid1234'}); - }); - - it('should NOT track a referral following successful registration of a non-team member', function(done) { - const onLoggedIn = jest.fn(function(creds, teamToken) { - expect(teamToken).toBeFalsy(); - done(); - }); - - const res = ReactTestUtils.renderIntoDocument( - , - ); - - res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someOtherEmailSid11'}); - }); -}); diff --git a/test/components/views/auth/RegistrationForm-test.js b/test/components/views/auth/RegistrationForm-test.js deleted file mode 100644 index 265f158bb9..0000000000 --- a/test/components/views/auth/RegistrationForm-test.js +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2017 Vector Creations 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. -*/ - -const jest = require('jest-mock'); -const React = require('react'); -const ReactDOM = require("react-dom"); -const ReactTestUtils = require('react-addons-test-utils'); -const expect = require('expect'); - -const testUtils = require('test-utils'); - -const sdk = require('matrix-react-sdk'); -const RegistrationForm = sdk.getComponent('views.auth.RegistrationForm'); - -const TEAM_CONFIG = { - supportEmail: "support@some.domain", - teams: [ - { name: "The Team Org.", domain: "team.ac.uk" }, - { name: "The Super Team", domain: "superteam.ac.uk" }, - ], -}; - -function doInputEmail(inputEmail, onTeamSelected) { - const res = ReactTestUtils.renderIntoDocument( - , - ); - - const teamInput = res.refs.email; - teamInput.value = inputEmail; - - ReactTestUtils.Simulate.change(teamInput); - ReactTestUtils.Simulate.blur(teamInput); - - return res; -} - -function expectTeamSelectedFromEmailInput(inputEmail, expectedTeam) { - const onTeamSelected = jest.fn(); - doInputEmail(inputEmail, onTeamSelected); - - expect(onTeamSelected).toHaveBeenCalledWith(expectedTeam); -} - -function expectSupportFromEmailInput(inputEmail, isSupportShown) { - const onTeamSelected = jest.fn(); - const res = doInputEmail(inputEmail, onTeamSelected); - - expect(res.state.showSupportEmail).toBe(isSupportShown); -} - -describe('RegistrationForm', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should select a team when a team email is entered', function() { - expectTeamSelectedFromEmailInput("member@team.ac.uk", TEAM_CONFIG.teams[0]); - }); - - it('should not select a team when an unrecognised team email is entered', function() { - expectTeamSelectedFromEmailInput("member@someunknownteam.ac.uk", null); - }); - - it('should show support when an unrecognised team email is entered', function() { - expectSupportFromEmailInput("member@someunknownteam.ac.uk", true); - }); - - it('should NOT show support when an unrecognised non-team email is entered', function() { - expectSupportFromEmailInput("someone@yahoo.com", false); - }); -}); diff --git a/test/test-utils.js b/test/test-utils.js index d5bcd9397a..f4f00effbb 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -104,20 +104,6 @@ export function createTestClient() { }; } -export function createTestRtsClient(teamMap, sidMap) { - return { - getTeamsConfig() { - return Promise.resolve(Object.keys(teamMap).map((token) => teamMap[token])); - }, - trackReferral(referrer, emailSid, clientSecret) { - return Promise.resolve({team_token: sidMap[emailSid]}); - }, - getTeam(teamToken) { - return Promise.resolve(teamMap[teamToken]); - }, - }; -} - /** * Create an Event. * @param {Object} opts Values for the event.