Initial support for ToS dialogs for IS/IM

as per MSC2140
This commit is contained in:
David Baker 2019-07-09 18:51:56 +01:00
parent 7a482461dd
commit 54aaabac74
10 changed files with 395 additions and 76 deletions

View file

@ -70,6 +70,7 @@
@import "./views/dialogs/_SetPasswordDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss";
@import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_SettingsDialog.scss";
@import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss";

View file

@ -0,0 +1,35 @@
/*
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.
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_TermsDialog_termsTableHeader {
font-weight: bold;
text-align: left;
}
.mx_TermsDialog_termsTable {
font-size: 12px;
}
.mx_TermsDialog_service, .mx_TermsDialog_summary {
padding-right: 10px;
}
.mx_TermsDialog_link {
mask-image: url('$(res)/img/external-link.svg');
background-color: $accent-color;
width: 10px;
height: 10px;
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,11 +17,14 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import { Service, presentTermsForServices, TermsNotSignedError } from './Terms';
const request = require('browser-request'); const request = require('browser-request');
const SdkConfig = require('./SdkConfig'); const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require('./MatrixClientPeg'); const MatrixClientPeg = require('./MatrixClientPeg');
import * as Matrix from 'matrix-js-sdk';
// The version of the integration manager API we're intending to work with // The version of the integration manager API we're intending to work with
const imApiVersion = "1.1"; const imApiVersion = "1.1";
@ -55,23 +59,11 @@ class ScalarAuthClient {
if (!token) { if (!token) {
return this.registerForToken(); return this.registerForToken();
} else { } else {
return this.validateToken(token).then(userId => { return this._checkToken(token);
const me = MatrixClientPeg.get().getUserId();
if (userId !== me) {
throw new Error("Scalar token is owned by someone else: " + me);
}
return token;
}).catch(err => {
console.error(err);
// Something went wrong - try to get a new token.
console.warn("Registering for new scalar token");
return this.registerForToken();
});
} }
} }
validateToken(token) { _getAccountName(token) {
const url = SdkConfig.get().integrations_rest_url + "/account"; const url = SdkConfig.get().integrations_rest_url + "/account";
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
@ -83,8 +75,10 @@ class ScalarAuthClient {
}, (err, response, body) => { }, (err, response, body) => {
if (err) { if (err) {
reject(err); reject(err);
} else if (body && body.errcode === 'M_TERMS_NOT_SIGNED') {
reject(new TermsNotSignedError());
} else if (response.statusCode / 100 !== 2) { } else if (response.statusCode / 100 !== 2) {
reject({statusCode: response.statusCode}); reject(body);
} else if (!body || !body.user_id) { } else if (!body || !body.user_id) {
reject(new Error("Missing user_id in response")); reject(new Error("Missing user_id in response"));
} else { } else {
@ -94,11 +88,35 @@ class ScalarAuthClient {
}); });
} }
_checkToken(token) {
return this._getAccountName(token).then(userId => {
const me = MatrixClientPeg.get().getUserId();
if (userId !== me) {
throw new Error("Scalar token is owned by someone else: " + me);
}
return token;
}).catch((e) => {
if (e instanceof TermsNotSignedError) {
console.log("Integrations manager requires new terms to be agreed to");
return presentTermsForServices([new Service(
Matrix.SERVICETYPES.IM,
SdkConfig.get().integrations_rest_url,
token,
)]).then(() => {
return token;
});
}
});
}
registerForToken() { registerForToken() {
// Get openid bearer token from the HS as the first part of our dance // Get openid bearer token from the HS as the first part of our dance
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
// Now we can send that to scalar and exchange it for a scalar token // Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(tokenObject); return this.exchangeForScalarToken(tokenObject);
}).then((tokenObject) => {
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
return this._checkToken(tokenObject);
}).then((tokenObject) => { }).then((tokenObject) => {
window.localStorage.setItem("mx_scalar_token", tokenObject); window.localStorage.setItem("mx_scalar_token", tokenObject);
return tokenObject; return tokenObject;

View file

@ -0,0 +1,201 @@
/*
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.
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 url from 'url';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t, pickBestLanguage } from '../../../languageHandler';
import Matrix from 'matrix-js-sdk';
class TermsCheckbox extends React.Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
url: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
}
onChange = (ev) => {
this.props.onChange(this.props.url, ev.target.checked);
}
render() {
return <input type="checkbox"
onChange={this.onChange}
checked={this.props.checked}
/>;
}
}
export default class TermsDialog extends React.Component {
static propTypes = {
/**
* Array of TermsWithService
*/
termsWithServices: PropTypes.arrayOf(PropTypes.object).isRequired,
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
}
constructor() {
super();
this.state = {
// url -> boolean
agreedUrls: {},
};
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onNextClick = () => {
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
}
_nameForServiceType(serviceType, host) {
switch (serviceType) {
case Matrix.SERVICETYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>;
case Matrix.SERVICETYPES.IM:
return <div>{_t("Integrations Manager")}<br />({host})</div>;
}
}
_summaryForServiceType(serviceType, docName) {
switch (serviceType) {
case Matrix.SERVICETYPES.IS:
return <div>
{_t("Find others by phone or email")}
<br />
{_t("Be found by phone or email")}
{docName !== null ? <br /> : ''}
{docName !== null ? '('+docName+')' : ''}
</div>;
case Matrix.SERVICETYPES.IM:
return <div>
{_t("Use Bots, bridges, widgets and sticker packs")}
{docName !== null ? <br /> : ''}
{docName !== null ? '('+docName+')' : ''}
</div>;
}
}
_onTermsCheckboxChange = (url, checked) => {
this.state.agreedUrls[url] = checked;
this.setState({agreedUrls: this.state.agreedUrls});
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const rows = [];
for (const termsWithService of this.props.termsWithServices) {
const parsedBaseUrl = url.parse(termsWithService.service.baseUrl);
const termsValues = Object.values(termsWithService.terms);
for (let i = 0; i < termsValues.length; ++i) {
const termDoc = termsValues[i];
const termsLang = pickBestLanguage(Object.keys(termDoc).filter((k) => k !== 'version'));
let serviceName;
if (i === 0) {
serviceName = this._nameForServiceType(termsWithService.service.serviceType, parsedBaseUrl.host);
}
const summary = this._summaryForServiceType(
termsWithService.service.serviceType,
termsValues.length > 1 ? termDoc[termsLang].name : null,
);
rows.push(<tr key={termDoc[termsLang].url}>
<td className="mx_TermsDialog_service">{serviceName}</td>
<td className="mx_TermsDialog_summary">{summary}</td>
<td><a rel="noopener" target="_blank" href={termDoc[termsLang].url}>
<div className="mx_TermsDialog_link" />
</a></td>
<td><TermsCheckbox
url={termDoc[termsLang].url}
onChange={this._onTermsCheckboxChange}
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
/></td>
</tr>);
}
}
// if all the documents for at least one service have been checked, we can enable
// the submit button
let enableSubmit = false;
for (const termsWithService of this.props.termsWithServices) {
let docsAgreedForService = 0;
for (const terms of Object.values(termsWithService.terms)) {
let docAgreed = false;
for (const lang of Object.keys(terms)) {
if (lang === 'version') continue;
if (this.state.agreedUrls[terms[lang].url]) {
docAgreed = true;
break;
}
}
if (docAgreed) {
++docsAgreedForService;
}
}
if (docsAgreedForService === Object.keys(termsWithService.terms).length) {
enableSubmit = true;
break;
}
}
return (
<BaseDialog className='mx_TermsDialog'
fixedWidth={false}
onFinished={this._onCancelClick}
title={_t("Terms of Service")}
contentId='mx_Dialog_content'
hasCancel={false}
>
<div id='mx_Dialog_content'>
<p>{_t("To continue you need to accept the Terms of this service.")}</p>
<table className="mx_TermsDialog_termsTable"><tbody>
<tr className="mx_TermsDialog_termsTableHeader">
<th>{_t("Service")}</th>
<th >{_t("Summary")}</th>
<th>{_t("Terms")}</th>
<th>{_t("Accept")}</th>
</tr>
{rows}
</tbody></table>
</div>
<DialogButtons primaryButton={_t('Next')}
hasCancel={true}
onCancel={this._onCancelClick}
onPrimaryButtonClick={this._onNextClick}
focus={true}
primaryDisabled={!enableSubmit}
/>
</BaseDialog>
);
}
}

View file

@ -21,6 +21,7 @@ import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { showIntegrationsManager } from '../../../integrations/integrations';
export default class ManageIntegsButton extends React.Component { export default class ManageIntegsButton extends React.Component {
constructor(props) { constructor(props) {
@ -30,10 +31,7 @@ export default class ManageIntegsButton extends React.Component {
onManageIntegrations = (ev) => { onManageIntegrations = (ev) => {
ev.preventDefault(); ev.preventDefault();
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); showIntegrationsManager({ room: this.props.room });
Modal.createDialog(IntegrationsManager, {
room: this.props.room,
}, "mx_IntegrationsManager");
}; };
render() { render() {

View file

@ -29,6 +29,7 @@ import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore"; import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { showIntegrationsManager } from '../../../integrations/integrations';
// The maximum number of widgets that can be added in a room // The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2; const MAX_WIDGETS = 2;
@ -127,11 +128,10 @@ module.exports = React.createClass({
}, },
_launchManageIntegrations: function() { _launchManageIntegrations: function() {
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager'); showIntegrationsManager({
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
room: this.props.room, room: this.props.room,
screen: 'add_integ', screen: 'add_integ',
}, 'mx_IntegrationsManager'); });
}, },
onClickAddWidget: function(e) { onClickAddWidget: function(e) {

View file

@ -24,60 +24,26 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
export default class IntegrationsManager extends React.Component { export default class IntegrationsManager extends React.Component {
static propTypes = { static propTypes = {
// the room object where the integrations manager should be opened in // false to display an error saying that there is no integrations manager configured
room: PropTypes.object.isRequired, configured: PropTypes.bool.isRequired,
// the screen name to open // false to display an error saying that we couldn't connect to the integrations manager
screen: PropTypes.string, connected: PropTypes.bool.isRequired,
// the integration ID to open // true to display a loading spinner
integrationId: PropTypes.string, loading: PropTypes.bool.isRequired,
// The source URL to load
url: PropTypes.string,
// callback when the manager is dismissed // callback when the manager is dismissed
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}; };
constructor(props) { static defaultProps = {
super(props); configured: true,
connected: true,
this.state = { loading: false,
loading: true,
configured: ScalarAuthClient.isPossible(),
connected: false, // true if a `src` is set and able to be connected to
src: null, // string for where to connect to
};
}
componentWillMount() {
if (!this.state.configured) return;
const scalarClient = new ScalarAuthClient();
scalarClient.connect().then(() => {
const hasCredentials = scalarClient.hasCredentials();
if (!hasCredentials) {
this.setState({
connected: false,
loading: false,
});
} else {
const src = scalarClient.getScalarInterfaceUrlForRoom(
this.props.room,
this.props.screen,
this.props.integrationId,
);
this.setState({
loading: false,
connected: true,
src: src,
});
}
}).catch(err => {
console.error(err);
this.setState({
loading: false,
connected: false,
});
});
} }
componentDidMount() { componentDidMount() {
@ -105,7 +71,7 @@ export default class IntegrationsManager extends React.Component {
}; };
render() { render() {
if (!this.state.configured) { if (!this.props.configured) {
return ( return (
<div className='mx_IntegrationsManager_error'> <div className='mx_IntegrationsManager_error'>
<h3>{_t("No integrations server configured")}</h3> <h3>{_t("No integrations server configured")}</h3>
@ -114,7 +80,7 @@ export default class IntegrationsManager extends React.Component {
); );
} }
if (this.state.loading) { if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
return ( return (
<div className='mx_IntegrationsManager_loading'> <div className='mx_IntegrationsManager_loading'>
@ -124,7 +90,7 @@ export default class IntegrationsManager extends React.Component {
); );
} }
if (!this.state.connected) { if (!this.props.connected) {
return ( return (
<div className='mx_IntegrationsManager_error'> <div className='mx_IntegrationsManager_error'>
<h3>{_t("Cannot connect to integrations server")}</h3> <h3>{_t("Cannot connect to integrations server")}</h3>
@ -133,6 +99,6 @@ export default class IntegrationsManager extends React.Component {
); );
} }
return <iframe src={this.state.src}></iframe>; return <iframe src={this.props.url}></iframe>;
} }
} }

View file

@ -93,7 +93,6 @@
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
"Unnamed Room": "Unnamed Room", "Unnamed Room": "Unnamed Room",
"Error": "Error", "Error": "Error",
"You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss", "Dismiss": "Dismiss",
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
@ -928,6 +927,7 @@
"Saturday": "Saturday", "Saturday": "Saturday",
"Today": "Today", "Today": "Today",
"Yesterday": "Yesterday", "Yesterday": "Yesterday",
"View Source": "View Source",
"Error decrypting audio": "Error decrypting audio", "Error decrypting audio": "Error decrypting audio",
"Reply": "Reply", "Reply": "Reply",
"Edit": "Edit", "Edit": "Edit",
@ -1127,6 +1127,7 @@
"Start chatting": "Start chatting", "Start chatting": "Start chatting",
"Click on the button below to start chatting!": "Click on the button below to start chatting!", "Click on the button below to start chatting!": "Click on the button below to start chatting!",
"Start Chatting": "Start Chatting", "Start Chatting": "Start Chatting",
"You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)",
"Removing…": "Removing…", "Removing…": "Removing…",
"Confirm Removal": "Confirm Removal", "Confirm Removal": "Confirm Removal",
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.",
@ -1265,6 +1266,17 @@
"Missing session data": "Missing session data", "Missing session data": "Missing session data",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
"Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.",
"Identity Server": "Identity Server",
"Integrations Manager": "Integrations Manager",
"Find others by phone or email": "Find others by phone or email",
"Be found by phone or email": "Be found by phone or email",
"Use Bots, bridges, widgets and sticker packs": "Use Bots, bridges, widgets and sticker packs",
"Terms of Service": "Terms of Service",
"To continue you need to accept the Terms of this service.": "To continue you need to accept the Terms of this service.",
"Service": "Service",
"Summary": "Summary",
"Terms": "Terms",
"Next": "Next",
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.", "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.",
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.", "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.",
"Room contains unknown devices": "Room contains unknown devices", "Room contains unknown devices": "Room contains unknown devices",
@ -1298,7 +1310,6 @@
"Enter Recovery Passphrase": "Enter Recovery Passphrase", "Enter Recovery Passphrase": "Enter Recovery Passphrase",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Warning</b>: you should only set up key backup from a trusted computer.", "<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Warning</b>: you should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
"Next": "Next",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>", "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
"Enter Recovery Key": "Enter Recovery Key", "Enter Recovery Key": "Enter Recovery Key",
"This looks like a valid recovery key!": "This looks like a valid recovery key!", "This looks like a valid recovery key!": "This looks like a valid recovery key!",
@ -1319,7 +1330,6 @@
"Cancel Sending": "Cancel Sending", "Cancel Sending": "Cancel Sending",
"Forward Message": "Forward Message", "Forward Message": "Forward Message",
"Pin Message": "Pin Message", "Pin Message": "Pin Message",
"View Source": "View Source",
"View Decrypted Source": "View Decrypted Source", "View Decrypted Source": "View Decrypted Source",
"Unhide Preview": "Unhide Preview", "Unhide Preview": "Unhide Preview",
"Share Permalink": "Share Permalink", "Share Permalink": "Share Permalink",

View file

@ -0,0 +1,55 @@
/*
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.
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 sdk from "../index";
import ScalarAuthClient from '../ScalarAuthClient';
import Modal from '../Modal';
import { TermsNotSignedError } from '../Terms';
export async function showIntegrationsManager(opts) {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const close = Modal.createTrackedDialog(
'Integrations Manager', '', IntegrationsManager, { loading: true }, "mx_IntegrationsManager",
).close;
const scalarClient = new ScalarAuthClient();
let props;
try {
await scalarClient.connect();
if (!scalarClient.hasCredentials()) {
props = { connected: false };
} else {
props = {
url: scalarClient.getScalarInterfaceUrlForRoom(
opts.room,
opts.screen,
opts.integrationId,
),
};
}
} catch (err) {
if (err instanceof TermsNotSignedError) {
// user canceled terms dialog, so just cancel the action
close();
return;
}
console.error(err);
props = { connected: false };
}
close();
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager");
}

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2017 MTRNord and Cooperative EITA Copyright 2017 MTRNord and Cooperative EITA
Copyright 2017 Vector Creations Ltd. Copyright 2017 Vector Creations Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -353,6 +354,40 @@ export function getCurrentLanguage() {
return counterpart.getLocale(); return counterpart.getLocale();
} }
/**
* Given a list of language codes, pick the most appropriate one
* given the current language (ie. getCurrentLanguage())
* English is assumed to be a reasonable default.
*
* @param {string[]} langs List of language codes to pick from
* @returns {string} The most appropriate language code from langs
*/
export function pickBestLanguage(langs) {
const currentLang = getCurrentLanguage();
const normalisedLangs = langs.map(normalizeLanguageKey);
{
// Best is an exact match
const currentLangIndex = normalisedLangs.indexOf(currentLang);
if (currentLangIndex > -1) return langs[currentLangIndex];
}
{
// Failing that, a different dialect of the same lnguage
const closeLangIndex = normalisedLangs.find((l) => l.substr(0,2) === currentLang.substr(0,2));
if (closeLangIndex > -1) return langs[closeLangIndex];
}
{
// Neither of those? Try an english variant.
const enIndex = normalisedLangs.find((l) => l.startsWith('en'));
if (enIndex > -1) return langs[enIndex];
}
// if nothing else, use the first
return langs[0];
}
function getLangsJson() { function getLangsJson() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let url; let url;