diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index e7f6ee1f84..c129f801a1 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -50,7 +50,6 @@ src/components/views/settings/Notifications.js src/GroupAddressPicker.js src/HtmlUtils.js src/ImageUtils.js -src/languageHandler.js src/linkify-matrix.js src/Markdown.js src/MatrixClientPeg.js diff --git a/.stylelintrc.js b/.stylelintrc.js index 97e1ec8023..f028c76cc0 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -15,6 +15,9 @@ module.exports = { "number-leading-zero": null, "selector-list-comma-newline-after": null, "at-rule-no-unknown": null, - "scss/at-rule-no-unknown": true, + "scss/at-rule-no-unknown": [true, { + // https://github.com/vector-im/riot-web/issues/10544 + "ignoreAtRules": ["define-mixin"], + }], } } diff --git a/res/css/_common.scss b/res/css/_common.scss index 517ced43fb..1b7c8ec938 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -559,3 +559,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Username_color8 { color: $username-variant8-color; } + +@define-mixin mx_Settings_fullWidthField { + margin-right: 200px; +} diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 3e97a0ff6d..afac75986f 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -26,6 +26,10 @@ limitations under the License. height: 4em; } +.mx_ProfileSettings_controls .mx_Field { + margin-right: 100px; +} + .mx_ProfileSettings_controls .mx_Field:first-child { margin-top: 0; } diff --git a/res/css/views/settings/_SetIdServer.scss b/res/css/views/settings/_SetIdServer.scss index c6fcfc8af5..55ad6eef02 100644 --- a/res/css/views/settings/_SetIdServer.scss +++ b/res/css/views/settings/_SetIdServer.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,5 +15,5 @@ limitations under the License. */ .mx_SetIdServer .mx_Field_input { - width: 300px; + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 3b330f2c30..16467165cf 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_themeSection .mx_Field { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } .mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child { @@ -26,5 +26,5 @@ limitations under the License. .mx_GeneralUserSettingsTab_accountSection .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_languageInput { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index b3430f47af..d003e175d9 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -15,5 +15,5 @@ limitations under the License. */ .mx_PreferencesUserSettingsTab .mx_Field { - margin-right: 100px; // Align with the rest of the controls + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss index 36c8cfd896..69d57bdba1 100644 --- a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_VoiceUserSettingsTab .mx_Field { - margin-right: 100px; // align with the rest of the fields + @mixin mx_Settings_fullWidthField; } .mx_VoiceUserSettingsTab_missingMediaPermissions { diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 084ec1bd6a..8272b36639 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -48,7 +48,7 @@ export default class Field extends React.PureComponent { onValidate: PropTypes.func, // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. - tooltip: PropTypes.node, + tooltipContent: PropTypes.node, // All other props pass through to the . }; @@ -137,8 +137,7 @@ export default class Field extends React.PureComponent { }, VALIDATION_THROTTLE_MS); render() { - const { element, prefix, onValidate, children, ...inputProps } = this.props; - delete inputProps.tooltip; // needs to be removed from props but we don't need it here + const { element, prefix, onValidate, children, tooltipContent, ...inputProps } = this.props; const inputElement = element || "input"; @@ -170,11 +169,11 @@ export default class Field extends React.PureComponent { // Handle displaying feedback on validity const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; - if (this.props.tooltip || this.state.feedback) { + if (tooltipContent || this.state.feedback) { fieldTooltip = ; } diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index a87fe034a1..398e578e8d 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import request from 'browser-request'; import url from 'url'; import React from 'react'; import {_t} from "../../../languageHandler"; import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; import SdkConfig from "../../../SdkConfig"; -import Field from "../elements/Field"; +import Modal from '../../../Modal'; +import dis from "../../../dispatcher"; /** * If a url has no path component, etc. abbreviate it to just the hostname @@ -58,41 +58,39 @@ function unabbreviateUrl(u) { /** * Check an IS URL is valid, including liveness check * - * @param {string} isUrl The url to check + * @param {string} u The url to check * @returns {string} null if url passes all checks, otherwise i18ned error string */ -async function checkIsUrl(isUrl) { - const parsedUrl = url.parse(isUrl); +async function checkIdentityServerUrl(u) { + const parsedUrl = url.parse(u); if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); // XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it - return new Promise((resolve) => { - request( - // also XXX: we don't really know whether to hit /v1 or /v2 for this: we - // probably want a /versions endpoint like the C/S API. - { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, - (err, response, body) => { - if (err) { - resolve(_t("Could not connect to ID Server")); - } else if (response.status < 200 || response.status >= 300) { - resolve(_t("Not a valid ID Server (status code %(code)s)", {code: response.status})); - } else { - resolve(null); - } - }, - ); - }); + try { + const response = await fetch(u + '/_matrix/identity/api/v1'); + if (response.ok) { + return null; + } else if (response.status < 200 || response.status >= 300) { + return _t("Not a valid Identity Server (status code %(code)s)", {code: response.status}); + } else { + return _t("Could not connect to Identity Server"); + } + } catch (e) { + return _t("Could not connect to Identity Server"); + } } export default class SetIdServer extends React.Component { constructor() { super(); - let defaultIdServer = abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()); - if (!defaultIdServer) { - defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['idServer']) || ''; + let defaultIdServer = ''; + if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) { + // If no ID server is configured but there's one in the config, prepopulate + // the field to help the user. + defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.state = { @@ -114,7 +112,7 @@ export default class SetIdServer extends React.Component { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
- { _t("Checking Server") } + { _t("Checking server") }
; } else if (this.state.error) { return this.state.error; @@ -127,18 +125,21 @@ export default class SetIdServer extends React.Component { return !!this.state.idServer && !this.state.busy; }; - _saveIdServer = async () => { + _saveIdServer = async (e) => { + e.preventDefault(); + this.setState({busy: true}); const fullUrl = unabbreviateUrl(this.state.idServer); - const errStr = await checkIsUrl(fullUrl); + const errStr = await checkIdentityServerUrl(fullUrl); let newFormValue = this.state.idServer; if (!errStr) { MatrixClientPeg.get().setIdentityServerUrl(fullUrl); localStorage.removeItem("mx_is_access_token"); localStorage.setItem("mx_is_url", fullUrl); + dis.dispatch({action: 'id_server_changed'}); newFormValue = ''; } this.setState({ @@ -149,7 +150,49 @@ export default class SetIdServer extends React.Component { }); }; + _onDisconnectClicked = () => { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, { + title: _t("Disconnect Identity Server"), + description: +
+ {_t( + "Disconnect from the identity server ?", {}, + {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, + )}, +
, + button: _t("Disconnect"), + onFinished: (confirmed) => { + if (confirmed) { + this._disconnectIdServer(); + } + }, + }); + }; + + _disconnectIdServer = () => { + MatrixClientPeg.get().setIdentityServerUrl(null); + localStorage.removeItem("mx_is_access_token"); + localStorage.removeItem("mx_is_url"); + + let newFieldVal = ''; + if (SdkConfig.get()['validated_server_config']['isUrl']) { + // Prepopulate the client's default so the user at least has some idea of + // a valid value they might enter + newFieldVal = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); + } + + this.setState({ + busy: false, + error: null, + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), + idServer: newFieldVal, + }); + }; + render() { + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + const Field = sdk.getComponent('elements.Field'); const idServerUrl = this.state.currentClientIdServer; let sectionTitle; let bodyText; @@ -164,12 +207,26 @@ export default class SetIdServer extends React.Component { } else { sectionTitle = _t("Identity Server"); bodyText = _t( - "You are not currently using an Identity Server. " + + "You are not currently using an identity server. " + "To discover and be discoverable by existing contacts you know, " + - "add one below", + "add one below.", ); } + let discoSection; + if (idServerUrl) { + discoSection =
+ {_t( + "Disconnecting from your identity server will mean you " + + "won't be discoverable by other users and you won't be " + + "able to invite others by email or phone.", + )} + + {_t("Disconnect")} + +
; + } + return (
@@ -182,12 +239,13 @@ export default class SetIdServer extends React.Component { id="mx_SetIdServer_idServer" type="text" value={this.state.idServer} autoComplete="off" onChange={this._onIdentityServerChanged} - tooltip={this._getTooltip()} + tooltipContent={this._getTooltip()} /> - + >{_t("Change")} + {discoSection} ); } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 0bf396c740..08550db1d1 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -45,9 +45,22 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), + haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), }; + + this.dispatcherRef = dis.register(this._onAction); } + componentWillUnmount() { + dis.unregister(this.dispatcherRef); + } + + _onAction = (payload) => { + if (payload.action === 'id_server_changed') { + this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); + } + }; + _onLanguageChange = (newLanguage) => { if (this.state.language === newLanguage) return; @@ -124,7 +137,7 @@ export default class GeneralUserSettingsTab extends React.Component { onFinished={this._onPasswordChanged} /> ); - const threepidSection = MatrixClientPeg.get().getIdentityServerUrl() ?
+ const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a925010f6e..e31a604fe3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -540,13 +540,17 @@ "Display Name": "Display Name", "Save": "Save", "Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", - "Could not connect to ID Server": "Could not connect to ID Server", - "Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", - "Checking Server": "Checking Server", + "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", + "Could not connect to Identity Server": "Could not connect to Identity Server", + "Checking server": "Checking server", + "Disconnect Identity Server": "Disconnect Identity Server", + "Disconnect from the identity server ?": "Disconnect from the identity server ?", + "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "Identity Server": "Identity Server", - "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", "Change": "Change", "Checking server": "Checking server", "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", diff --git a/src/languageHandler.js b/src/languageHandler.js index c1a426383b..474cd2b3cd 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -2,6 +2,7 @@ Copyright 2017 MTRNord and Cooperative EITA Copyright 2017 Vector Creations Ltd. Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -102,7 +103,7 @@ function safeCounterpartTranslate(text, options) { * @return a React component if any non-strings were used in substitutions, otherwise a string */ export function _t(text, variables, tags) { - // Don't do subsitutions in counterpart. We handle it ourselves so we can replace with React components + // Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components // However, still pass the variables to counterpart so that it can choose the correct plural if count is given // It is enough to pass the count variable, but in the future counterpart might make use of other information too const args = Object.assign({ interpolate: false }, variables); @@ -289,7 +290,7 @@ export function setLanguage(preferredLangs) { console.log("set language to " + langToUse); // Set 'en' as fallback language: - if (langToUse != "en") { + if (langToUse !== "en") { return getLanguage(i18nFolder + availLangs['en'].fileName); } }).then((langData) => { @@ -329,13 +330,13 @@ export function getLanguagesFromBrowser() { */ export function getNormalizedLanguageKeys(language) { const languageKeys = []; - const normalizedLanguage = this.normalizeLanguageKey(language); + const normalizedLanguage = normalizeLanguageKey(language); const languageParts = normalizedLanguage.split('-'); - if (languageParts.length == 2 && languageParts[0] == languageParts[1]) { + if (languageParts.length === 2 && languageParts[0] === languageParts[1]) { languageKeys.push(languageParts[0]); } else { languageKeys.push(normalizedLanguage); - if (languageParts.length == 2) { + if (languageParts.length === 2) { languageKeys.push(languageParts[0]); } } @@ -345,6 +346,9 @@ export function getNormalizedLanguageKeys(language) { /** * Returns a language string with underscores replaced with * hyphens, and lowercased. + * + * @param {string} language The language string to be normalized + * @returns {string} The normalized language string */ export function normalizeLanguageKey(language) { return language.toLowerCase().replace("_", "-"); @@ -373,8 +377,8 @@ export function pickBestLanguage(langs) { } { - // Failing that, a different dialect of the same lnguage - const closeLangIndex = normalisedLangs.find((l) => l.substr(0,2) === currentLang.substr(0,2)); + // Failing that, a different dialect of the same language + const closeLangIndex = normalisedLangs.find((l) => l.substr(0, 2) === currentLang.substr(0, 2)); if (closeLangIndex > -1) return langs[closeLangIndex]; }