Support multiple integration managers behind a labs flag

Fixes https://github.com/vector-im/riot-web/issues/10622
Implements [MSC1957](https://github.com/matrix-org/matrix-doc/pull/1957)

Design is not final.
This commit is contained in:
Travis Ralston 2019-08-23 09:12:40 -06:00
parent 602c338a26
commit b3cda4b19a
13 changed files with 318 additions and 21 deletions

View file

@ -71,6 +71,7 @@
@import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_SettingsDialog.scss";
@import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_SlashCommandHelpDialog.scss"; @import "./views/dialogs/_SlashCommandHelpDialog.scss";
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.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";

View file

@ -0,0 +1,67 @@
/*
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_TabbedIntegrationManagerDialog .mx_Dialog {
width: 60%;
height: 70%;
overflow: hidden;
padding: 0;
max-width: initial;
max-height: initial;
position: relative;
}
.mx_TabbedIntegrationManagerDialog_container {
// Full size of the dialog, whatever it is
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
.mx_TabbedIntegrationManagerDialog_currentManager {
width: 100%;
height: 100%;
border-top: 1px solid $accent-color;
iframe {
background-color: #fff;
border: 0;
width: 100%;
height: 100%;
}
}
}
.mx_TabbedIntegrationManagerDialog_tab {
display: inline-block;
border: 1px solid $accent-color;
border-bottom: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
//background-color: $accent-color-50pct;
padding: 10px 8px;
margin-right: 5px;
}
.mx_TabbedIntegrationManagerDialog_tab:first-child {
//margin-left: 8px;
}
.mx_TabbedIntegrationManagerDialog_currentTab {
background-color: $accent-color;
color: $accent-fg-color;
}

View file

@ -23,6 +23,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore';
import MatrixClientPeg from "./MatrixClientPeg"; import MatrixClientPeg from "./MatrixClientPeg";
import RoomViewStore from "./stores/RoomViewStore"; import RoomViewStore from "./stores/RoomViewStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {IntegrationManagers} from "./integrations/IntegrationManagers";
import SettingsStore from "./settings/SettingsStore";
const WIDGET_API_VERSION = '0.0.2'; // Current API version const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [ const SUPPORTED_WIDGET_API_VERSIONS = [
@ -194,11 +195,19 @@ export default class FromWidgetPostMessageApi {
const integId = (data && data.integId) ? data.integId : null; const integId = (data && data.integId) ? data.integId : null;
// TODO: Open the right integration manager for the widget // TODO: Open the right integration manager for the widget
IntegrationManagers.sharedInstance().getPrimaryManager().open( if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), IntegrationManagers.sharedInstance().openAll(
`type_${integType}`, MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
integId, `type_${integType}`,
); integId,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
`type_${integType}`,
integId,
);
}
} else if (action === 'set_always_on_screen') { } else if (action === 'set_always_on_screen') {
// This is a new message: there is no reason to support the deprecated widgetData here // This is a new message: there is no reason to support the deprecated widgetData here
const data = event.data.data; const data = event.data.data;

View file

@ -548,8 +548,8 @@ const onMessage = function(event) {
// (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) // (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
let configUrl; let configUrl;
try { try {
// TODO: Support multiple integration managers if (!openManagerUrl) openManagerUrl = IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl;
configUrl = new URL(IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl); configUrl = new URL(openManagerUrl);
} catch (e) { } catch (e) {
// No integrations UI URL, ignore silently. // No integrations UI URL, ignore silently.
return; return;
@ -657,6 +657,7 @@ const onMessage = function(event) {
}; };
let listenerCount = 0; let listenerCount = 0;
let openManagerUrl = null;
module.exports = { module.exports = {
startListening: function() { startListening: function() {
if (listenerCount === 0) { if (listenerCount === 0) {
@ -679,4 +680,8 @@ module.exports = {
console.error(e); console.error(e);
} }
}, },
setOpenManagerUrl: function(url) {
openManagerUrl = url;
}
}; };

View file

@ -0,0 +1,172 @@
/*
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 React from 'react';
import PropTypes from 'prop-types';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {Room} from "matrix-js-sdk";
import sdk from '../../../index';
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
import classNames from 'classnames';
import ScalarMessaging from "../../../ScalarMessaging";
export default class TabbedIntegrationManagerDialog extends React.Component {
static propTypes = {
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
/**
* Optional room where the integration manager should be open to
*/
room: PropTypes.instanceOf(Room),
/**
* Optional screen to open on the integration manager
*/
screen: PropTypes.string,
/**
* Optional integration ID to open in the integration manager
*/
integrationId: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
managers: IntegrationManagers.sharedInstance().getOrderedManagers(),
busy: true,
currentIndex: 0,
currentConnected: false,
currentLoading: true,
currentScalarClient: null,
};
}
componentDidMount(): void {
this.openManager(0, true);
}
openManager = async (i: number, force = false) => {
if (i === this.state.currentIndex && !force) return;
const manager = this.state.managers[i];
const client = manager.getScalarClient();
this.setState({
busy: true,
currentIndex: i,
currentLoading: true,
currentConnected: false,
currentScalarClient: client,
});
ScalarMessaging.setOpenManagerUrl(manager.uiUrl);
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
// 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.
return dialogTermsInteractionCallback(
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager',
);
});
try {
await client.connect();
if (!client.hasCredentials()) {
this.setState({
busy: false,
currentLoading: false,
currentConnected: false,
});
} else {
this.setState({
busy: false,
currentLoading: false,
currentConnected: true,
});
}
} catch (e) {
if (e instanceof TermsNotSignedError) {
return;
}
console.error(e);
this.setState({
busy: false,
currentLoading: false,
currentConnected: false,
});
}
};
_renderTabs() {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
return this.state.managers.map((m, i) => {
const classes = classNames({
'mx_TabbedIntegrationManagerDialog_tab': true,
'mx_TabbedIntegrationManagerDialog_currentTab': this.state.currentIndex === i,
});
return (
<AccessibleButton
className={classes}
onClick={() => this.openManager(i)}
key={`tab_${i}`}
disabled={this.state.busy}
>
{m.name}
</AccessibleButton>
);
})
}
_renderTab() {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
let uiUrl = null;
if (this.state.currentScalarClient) {
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
this.props.room,
this.props.screen,
this.props.integrationId,
);
}
return <IntegrationsManager
configured={true}
loading={this.state.currentLoading}
connected={this.state.currentConnected}
url={uiUrl}
onFinished={() => {/* no-op */}}
/>;
}
render() {
return (
<div className='mx_TabbedIntegrationManagerDialog_container'>
<div className='mx_TabbedIntegrationManagerDialog_tabs'>
{this._renderTabs()}
</div>
<div className='mx_TabbedIntegrationManagerDialog_currentManager'>
{this._renderTab()}
</div>
</div>
)
}
}

View file

@ -34,6 +34,7 @@ import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames'; import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false; const ENABLE_REACT_PERF = false;
@ -264,11 +265,19 @@ export default class AppTile extends React.Component {
this.props.onEditClick(); this.props.onEditClick();
} else { } else {
// TODO: Open the right manager for the widget // TODO: Open the right manager for the widget
IntegrationManagers.sharedInstance().getPrimaryManager().open( if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
this.props.room, IntegrationManagers.sharedInstance().openAll(
'type_' + this.props.type, this.props.room,
this.props.id, 'type_' + this.props.type,
); this.props.id,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
'type_' + this.props.type,
this.props.id,
);
}
} }
} }

View file

@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
export default class ManageIntegsButton extends React.Component { export default class ManageIntegsButton extends React.Component {
constructor(props) { constructor(props) {
@ -33,7 +34,11 @@ export default class ManageIntegsButton extends React.Component {
if (!managers.hasManager()) { if (!managers.hasManager()) {
managers.openNoManagerDialog(); managers.openNoManagerDialog();
} else { } else {
managers.getPrimaryManager().open(this.props.room); if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
managers.openAll(this.props.room);
} else {
managers.getPrimaryManager().open(this.props.room);
}
} }
}; };

View file

@ -30,6 +30,7 @@ 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 {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
// 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;
@ -128,7 +129,11 @@ module.exports = React.createClass({
}, },
_launchManageIntegrations: function() { _launchManageIntegrations: function() {
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ'); if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll();
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
}
}, },
onClickAddWidget: function(e) { onClickAddWidget: function(e) {

View file

@ -24,6 +24,7 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement"; import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
const widgetType = 'm.stickerpicker'; const widgetType = 'm.stickerpicker';
@ -349,11 +350,19 @@ export default class Stickerpicker extends React.Component {
*/ */
_launchManageIntegrations() { _launchManageIntegrations() {
// TODO: Open the right integration manager for the widget // TODO: Open the right integration manager for the widget
IntegrationManagers.sharedInstance().getPrimaryManager().open( if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
this.props.room, IntegrationManagers.sharedInstance().openAll(
`type_${widgetType}`, this.props.room,
this.state.widgetId, `type_${widgetType}`,
); this.state.widgetId,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
`type_${widgetType}`,
this.state.widgetId,
);
}
} }
render() { render() {

View file

@ -43,7 +43,7 @@ export default class IntegrationsManager extends React.Component {
configured: true, configured: true,
connected: true, connected: true,
loading: false, loading: false,
} };
componentDidMount() { componentDidMount() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);

View file

@ -327,6 +327,7 @@
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Render simple counters in room header": "Render simple counters in room header", "Render simple counters in room header": "Render simple counters in room header",
"Use the new, faster, but still experimental composer for writing messages (requires refresh)": "Use the new, faster, but still experimental composer for writing messages (requires refresh)", "Use the new, faster, but still experimental composer for writing messages (requires refresh)": "Use the new, faster, but still experimental composer for writing messages (requires refresh)",
"Multiple integration managers": "Multiple integration managers",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
"Use compact timeline layout": "Use compact timeline layout", "Use compact timeline layout": "Use compact timeline layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show a placeholder for removed messages": "Show a placeholder for removed messages",

View file

@ -18,7 +18,7 @@ import SdkConfig from '../SdkConfig';
import sdk from "../index"; import sdk from "../index";
import Modal from '../Modal'; import Modal from '../Modal';
import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER} from "./IntegrationManagerInstance"; import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER} from "./IntegrationManagerInstance";
import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk";
import WidgetUtils from "../utils/WidgetUtils"; import WidgetUtils from "../utils/WidgetUtils";
import MatrixClientPeg from "../MatrixClientPeg"; import MatrixClientPeg from "../MatrixClientPeg";
import {AutoDiscovery} from "matrix-js-sdk"; import {AutoDiscovery} from "matrix-js-sdk";
@ -180,6 +180,14 @@ export class IntegrationManagers {
); );
} }
openAll(room: Room = null, screen: string = null, integrationId: string = null): void {
const TabbedIntegrationManagerDialog = sdk.getComponent("views.dialogs.TabbedIntegrationManagerDialog");
Modal.createTrackedDialog(
'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog,
{room, screen, integrationId}, 'mx_TabbedIntegrationManagerDialog',
);
}
async overwriteManagerOnAccount(manager: IntegrationManagerInstance) { async overwriteManagerOnAccount(manager: IntegrationManagerInstance) {
// TODO: TravisR - We should be logging out of scalar clients. // TODO: TravisR - We should be logging out of scalar clients.
await WidgetUtils.removeIntegrationManagerWidgets(); await WidgetUtils.removeIntegrationManagerWidgets();

View file

@ -121,6 +121,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
}, },
"feature_many_integration_managers": {
isFeature: true,
displayName: _td("Multiple integration managers"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"MessageComposerInput.suggestEmoji": { "MessageComposerInput.suggestEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable Emoji suggestions while typing'), displayName: _td('Enable Emoji suggestions while typing'),