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'),