Initial support for ToS dialogs for IS/IM
as per MSC2140
This commit is contained in:
parent
7a482461dd
commit
54aaabac74
10 changed files with 395 additions and 76 deletions
|
@ -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";
|
||||||
|
|
35
res/css/views/dialogs/_TermsDialog.scss
Normal file
35
res/css/views/dialogs/_TermsDialog.scss
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
201
src/components/views/dialogs/TermsDialog.js
Normal file
201
src/components/views/dialogs/TermsDialog.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
55
src/integrations/integrations.js
Normal file
55
src/integrations/integrations.js
Normal 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");
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue