mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge branch 'develop' into travis/integs/account_set
						commit
						beb6ec4327
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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"], | ||||
|         }], | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 <input>.
 | ||||
|     }; | ||||
| 
 | ||||
|  | @ -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 = <Tooltip | ||||
|                 tooltipClassName="mx_Field_tooltip" | ||||
|                 visible={this.state.feedbackVisible} | ||||
|                 label={this.props.tooltip || this.state.feedback} | ||||
|                 label={tooltipContent || this.state.feedback} | ||||
|             />; | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 <div> | ||||
|                 <InlineSpinner /> | ||||
|                 { _t("Checking Server") } | ||||
|                 { _t("Checking server") } | ||||
|             </div>; | ||||
|         } 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: | ||||
|                 <div> | ||||
|                     {_t( | ||||
|                         "Disconnect from the identity server <idserver />?", {}, | ||||
|                         {idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>}, | ||||
|                     )}, | ||||
|                 </div>, | ||||
|             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 = <div> | ||||
|                 <span className="mx_SettingsTab_subsectionText">{_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.", | ||||
|                 )}</span> | ||||
|                 <AccessibleButton onClick={this._onDisconnectClicked} kind="danger"> | ||||
|                     {_t("Disconnect")} | ||||
|                 </AccessibleButton> | ||||
|             </div>; | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <form className="mx_SettingsTab_section mx_SetIdServer" onSubmit={this._saveIdServer}> | ||||
|                 <span className="mx_SettingsTab_subheading"> | ||||
|  | @ -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()} | ||||
|                 /> | ||||
|                 <input className="mx_Dialog_primary" | ||||
|                     type="submit" value={_t("Change")} | ||||
|                 <AccessibleButton type="submit" kind="primary_sm" | ||||
|                     onClick={this._saveIdServer} | ||||
|                     disabled={!this._idServerChangeEnabled()} | ||||
|                 /> | ||||
|                 >{_t("Change")}</AccessibleButton> | ||||
|                 {discoSection} | ||||
|             </form> | ||||
|         ); | ||||
|     } | ||||
|  |  | |||
|  | @ -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() ? <div> | ||||
|         const threepidSection = this.state.haveIdServer ? <div> | ||||
|             <span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span> | ||||
|             <EmailAddresses /> | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 <idserver />?": "Disconnect from the identity server <idserver />?", | ||||
|     "Disconnect": "Disconnect", | ||||
|     "Identity Server (%(server)s)": "Identity Server (%(server)s)", | ||||
|     "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using <server></server> 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.", | ||||
|  |  | |||
|  | @ -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 <span> 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]; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston