Avoid visual glitch when terms appear for IM

This avoids a visual glitch where the Integration Manager portal would briefly
appear, but then be replaced by a smaller Terms dialog when there's something to
agree to.

To resolve this minimal code churn, this cheats a bit and customises the size of
the terms dialog to match the IM portal modal when terms are shown for IM
purposes.

Fixes https://github.com/vector-im/riot-web/issues/10386
pull/21833/head
J. Ryan Stinnett 2019-07-23 15:11:38 +01:00
parent 2eb8a8879b
commit 39d5aa7cf4
5 changed files with 49 additions and 19 deletions

View File

@ -14,6 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/*
* To avoid visual glitching of two modals stacking briefly, we customise the
* terms dialog sizing when it will appear for the integrations manager so that
* it gets the same basic size as the IM's own modal.
*/
.mx_TermsDialog_forIntegrationsManager .mx_Dialog {
width: 60%;
height: 70%;
box-sizing: border-box;
}
.mx_TermsDialog_termsTableHeader { .mx_TermsDialog_termsTableHeader {
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
@ -21,6 +32,7 @@ limitations under the License.
.mx_TermsDialog_termsTable { .mx_TermsDialog_termsTable {
font-size: 12px; font-size: 12px;
width: 100%;
} }
.mx_TermsDialog_service, .mx_TermsDialog_summary { .mx_TermsDialog_service, .mx_TermsDialog_summary {

View File

@ -18,7 +18,7 @@ limitations under the License.
import url from 'url'; import url from 'url';
import Promise from 'bluebird'; import Promise from 'bluebird';
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import { Service, presentTermsForServices, TermsNotSignedError } from './Terms'; import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
const request = require('browser-request'); const request = require('browser-request');
const SdkConfig = require('./SdkConfig'); const SdkConfig = require('./SdkConfig');
@ -32,6 +32,9 @@ const imApiVersion = "1.1";
class ScalarAuthClient { class ScalarAuthClient {
constructor() { constructor() {
this.scalarToken = null; this.scalarToken = null;
// `undefined` to allow `startTermsFlow` to fallback to a default
// callback if this is unset.
this.termsInteractionCallback = undefined;
} }
/** /**
@ -42,6 +45,10 @@ class ScalarAuthClient {
return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url']; return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url'];
} }
setTermsInteractionCallback(callback) {
this.termsInteractionCallback = callback;
}
connect() { connect() {
return this.getScalarToken().then((tok) => { return this.getScalarToken().then((tok) => {
this.scalarToken = tok; this.scalarToken = tok;
@ -122,11 +129,11 @@ class ScalarAuthClient {
const parsedImRestUrl = url.parse(SdkConfig.get().integrations_rest_url); const parsedImRestUrl = url.parse(SdkConfig.get().integrations_rest_url);
parsedImRestUrl.path = ''; parsedImRestUrl.path = '';
parsedImRestUrl.pathname = ''; parsedImRestUrl.pathname = '';
return presentTermsForServices([new Service( return startTermsFlow([new Service(
Matrix.SERVICE_TYPES.IM, Matrix.SERVICE_TYPES.IM,
parsedImRestUrl.format(), parsedImRestUrl.format(),
token, token,
)]).then(() => { )], this.termsInteractionCallback).then(() => {
return token; return token;
}); });
} else { } else {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import Promise from 'bluebird'; import Promise from 'bluebird';
import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import sdk from './'; import sdk from './';
@ -39,17 +40,6 @@ export class Service {
} }
} }
/**
* Present a popup to the user prompting them to agree to terms and conditions
*
* @param {Service[]} services Object with keys 'serviceType', 'baseUrl', 'accessToken'
* @returns {Promise} resolves when the user agreed to all necessary terms or rejects
* if they cancel.
*/
export function presentTermsForServices(services) {
return startTermsFlow(services, dialogTermsInteractionCallback);
}
/** /**
* Start a flow where the user is presented with terms & conditions for some services * Start a flow where the user is presented with terms & conditions for some services
* *
@ -61,7 +51,10 @@ export function presentTermsForServices(services) {
* @returns {Promise} resolves when the user agreed to all necessary terms or rejects * @returns {Promise} resolves when the user agreed to all necessary terms or rejects
* if they cancel. * if they cancel.
*/ */
export async function startTermsFlow(services, interactionCallback) { export async function startTermsFlow(
services,
interactionCallback = dialogTermsInteractionCallback,
) {
const termsPromises = services.map( const termsPromises = services.map(
(s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl), (s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl),
); );
@ -160,7 +153,11 @@ export async function startTermsFlow(services, interactionCallback) {
return Promise.all(agreePromises); return Promise.all(agreePromises);
} }
function dialogTermsInteractionCallback(policiesAndServicePairs, agreedUrls) { export function dialogTermsInteractionCallback(
policiesAndServicePairs,
agreedUrls,
extraClassNames,
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log("Terms that need agreement", policiesAndServicePairs); console.log("Terms that need agreement", policiesAndServicePairs);
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
@ -175,6 +172,6 @@ function dialogTermsInteractionCallback(policiesAndServicePairs, agreedUrls) {
} }
resolve(agreedUrls); resolve(agreedUrls);
}, },
}); }, classNames("mx_TermsDialog", extraClassNames));
}); });
} }

View File

@ -176,7 +176,7 @@ export default class TermsDialog extends React.PureComponent {
} }
return ( return (
<BaseDialog className='mx_TermsDialog' <BaseDialog
fixedWidth={false} fixedWidth={false}
onFinished={this._onCancelClick} onFinished={this._onCancelClick}
title={_t("Terms of Service")} title={_t("Terms of Service")}

View File

@ -17,7 +17,7 @@ limitations under the License.
import sdk from "../index"; import sdk from "../index";
import ScalarAuthClient from '../ScalarAuthClient'; import ScalarAuthClient from '../ScalarAuthClient';
import Modal from '../Modal'; import Modal from '../Modal';
import { TermsNotSignedError } from '../Terms'; import { TermsNotSignedError, dialogTermsInteractionCallback } from '../Terms';
export async function showIntegrationsManager(opts) { export async function showIntegrationsManager(opts) {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
@ -38,6 +38,7 @@ export async function showIntegrationsManager(opts) {
} }
const scalarClient = new ScalarAuthClient(); const scalarClient = new ScalarAuthClient();
scalarClient.setTermsInteractionCallback(integrationsTermsInteractionCallback);
try { try {
await scalarClient.connect(); await scalarClient.connect();
if (!scalarClient.hasCredentials()) { if (!scalarClient.hasCredentials()) {
@ -63,3 +64,16 @@ export async function showIntegrationsManager(opts) {
close(); close();
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager"); Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager");
} }
/*
* To avoid visual glitching of two modals stacking briefly, we customise the
* terms dialog sizing when it will appear for the integrations manager so that
* it gets the same basic size as the IM's own modal.
*/
function integrationsTermsInteractionCallback(policiesAndServicePairs, agreedUrls) {
return dialogTermsInteractionCallback(
policiesAndServicePairs,
agreedUrls,
"mx_TermsDialog_forIntegrationsManager",
);
}