Merge branch 'develop' into travis/integs/account_set

pull/21833/head
Travis Ralston 2019-08-14 08:45:01 -06:00
commit beb6ec4327
13 changed files with 147 additions and 59 deletions

View File

@ -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

View File

@ -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"],
}],
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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}
/>;
}

View File

@ -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>
);
}

View File

@ -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 />

View File

@ -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.",

View File

@ -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];
}