diff --git a/res/css/_components.scss b/res/css/_components.scss
index d19d07132c..fb6058df00 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -71,6 +71,7 @@
@import "./views/dialogs/_SettingsDialog.scss";
@import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
+@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss";
diff --git a/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss b/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss
new file mode 100644
index 0000000000..0ab59c44a7
--- /dev/null
+++ b/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss
@@ -0,0 +1,62 @@
+/*
+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;
+ padding: 10px 8px;
+ margin-right: 5px;
+}
+
+.mx_TabbedIntegrationManagerDialog_currentTab {
+ background-color: $accent-color;
+ color: $accent-fg-color;
+}
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index b2bd579b74..8915c1412f 100644
--- a/src/FromWidgetPostMessageApi.js
+++ b/src/FromWidgetPostMessageApi.js
@@ -23,6 +23,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore';
import MatrixClientPeg from "./MatrixClientPeg";
import RoomViewStore from "./stores/RoomViewStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers";
+import SettingsStore from "./settings/SettingsStore";
const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
@@ -194,11 +195,19 @@ export default class FromWidgetPostMessageApi {
const integId = (data && data.integId) ? data.integId : null;
// TODO: Open the right integration manager for the widget
- IntegrationManagers.sharedInstance().getPrimaryManager().open(
- MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
- `type_${integType}`,
- integId,
- );
+ if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
+ IntegrationManagers.sharedInstance().openAll(
+ MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
+ `type_${integType}`,
+ integId,
+ );
+ } else {
+ IntegrationManagers.sharedInstance().getPrimaryManager().open(
+ MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
+ `type_${integType}`,
+ integId,
+ );
+ }
} else if (action === 'set_always_on_screen') {
// This is a new message: there is no reason to support the deprecated widgetData here
const data = event.data.data;
diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js
index 0d61755519..910a6c4f13 100644
--- a/src/ScalarMessaging.js
+++ b/src/ScalarMessaging.js
@@ -548,8 +548,8 @@ const onMessage = function(event) {
// (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
let configUrl;
try {
- // TODO: Support multiple integration managers
- configUrl = new URL(IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl);
+ if (!openManagerUrl) openManagerUrl = IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl;
+ configUrl = new URL(openManagerUrl);
} catch (e) {
// No integrations UI URL, ignore silently.
return;
@@ -657,6 +657,7 @@ const onMessage = function(event) {
};
let listenerCount = 0;
+let openManagerUrl = null;
module.exports = {
startListening: function() {
if (listenerCount === 0) {
@@ -679,4 +680,8 @@ module.exports = {
console.error(e);
}
},
+
+ setOpenManagerUrl: function(url) {
+ openManagerUrl = url;
+ },
};
diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
new file mode 100644
index 0000000000..5ef7aef9ab
--- /dev/null
+++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
@@ -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 (
+ this.openManager(i)}
+ key={`tab_${i}`}
+ disabled={this.state.busy}
+ >
+ {m.name}
+
+ );
+ });
+ }
+
+ _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 {/* no-op */}}
+ />;
+ }
+
+ render() {
+ return (
+
+
+ {this._renderTabs()}
+
+
+ {this._renderTab()}
+
+
+ );
+ }
+}
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 9e3570a608..ef6c45e0a3 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -34,6 +34,7 @@ import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
+import SettingsStore from "../../../settings/SettingsStore";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@@ -264,11 +265,19 @@ export default class AppTile extends React.Component {
this.props.onEditClick();
} else {
// TODO: Open the right manager for the widget
- IntegrationManagers.sharedInstance().getPrimaryManager().open(
- this.props.room,
- 'type_' + this.props.type,
- this.props.id,
- );
+ if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
+ IntegrationManagers.sharedInstance().openAll(
+ this.props.room,
+ 'type_' + this.props.type,
+ this.props.id,
+ );
+ } else {
+ IntegrationManagers.sharedInstance().getPrimaryManager().open(
+ this.props.room,
+ 'type_' + this.props.type,
+ this.props.id,
+ );
+ }
}
}
diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js
index ca7391329f..3503d1713b 100644
--- a/src/components/views/elements/ManageIntegsButton.js
+++ b/src/components/views/elements/ManageIntegsButton.js
@@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
+import SettingsStore from "../../../settings/SettingsStore";
export default class ManageIntegsButton extends React.Component {
constructor(props) {
@@ -33,7 +34,11 @@ export default class ManageIntegsButton extends React.Component {
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} 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);
+ }
}
};
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index 4d2c1e0380..19a5a6c468 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -30,6 +30,7 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
+import SettingsStore from "../../../settings/SettingsStore";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
@@ -128,7 +129,11 @@ module.exports = React.createClass({
},
_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) {
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 2d3508c404..abecb1781d 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -24,6 +24,7 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
+import SettingsStore from "../../../settings/SettingsStore";
const widgetType = 'm.stickerpicker';
@@ -349,11 +350,19 @@ export default class Stickerpicker extends React.Component {
*/
_launchManageIntegrations() {
// TODO: Open the right integration manager for the widget
- IntegrationManagers.sharedInstance().getPrimaryManager().open(
- this.props.room,
- `type_${widgetType}`,
- this.state.widgetId,
- );
+ if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
+ IntegrationManagers.sharedInstance().openAll(
+ this.props.room,
+ `type_${widgetType}`,
+ this.state.widgetId,
+ );
+ } else {
+ IntegrationManagers.sharedInstance().getPrimaryManager().open(
+ this.props.room,
+ `type_${widgetType}`,
+ this.state.widgetId,
+ );
+ }
}
render() {
diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js
index 149d66eef6..d463b043d5 100644
--- a/src/components/views/settings/IntegrationsManager.js
+++ b/src/components/views/settings/IntegrationsManager.js
@@ -43,7 +43,7 @@ export default class IntegrationsManager extends React.Component {
configured: true,
connected: true,
loading: false,
- }
+ };
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 1c3c75ebd5..62b6467b94 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -327,6 +327,7 @@
"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",
"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",
"Use compact timeline layout": "Use compact timeline layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js
index 98f605353c..a0fbff56fb 100644
--- a/src/integrations/IntegrationManagers.js
+++ b/src/integrations/IntegrationManagers.js
@@ -18,7 +18,7 @@ import SdkConfig from '../SdkConfig';
import sdk from "../index";
import Modal from '../Modal';
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 MatrixClientPeg from "../MatrixClientPeg";
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) {
// TODO: TravisR - We should be logging out of scalar clients.
await WidgetUtils.removeIntegrationManagerWidgets();
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index 37a777913b..70abf406b8 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -121,6 +121,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
+ "feature_many_integration_managers": {
+ isFeature: true,
+ displayName: _td("Multiple integration managers"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"MessageComposerInput.suggestEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable Emoji suggestions while typing'),