diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles
index a89c0835183..9ecd39ffc2b 100644
--- a/.eslintignore.errorfiles
+++ b/.eslintignore.errorfiles
@@ -47,7 +47,6 @@ src/components/views/rooms/UserTile.js
src/components/views/settings/ChangeAvatar.js
src/components/views/settings/ChangePassword.js
src/components/views/settings/DevicesPanel.js
-src/components/views/settings/IntegrationsManager.js
src/components/views/settings/Notifications.js
src/GroupAddressPicker.js
src/HtmlUtils.js
diff --git a/res/css/views/settings/_IntegrationsManager.scss b/res/css/views/settings/_IntegrationsManager.scss
index 93ee0e20fe0..c5769d3645d 100644
--- a/res/css/views/settings/_IntegrationsManager.scss
+++ b/res/css/views/settings/_IntegrationsManager.scss
@@ -29,3 +29,16 @@ limitations under the License.
width: 100%;
height: 100%;
}
+
+.mx_IntegrationsManager_loading h3 {
+ text-align: center;
+}
+
+.mx_IntegrationsManager_error {
+ text-align: center;
+ padding-top: 20px;
+}
+
+.mx_IntegrationsManager_error h3 {
+ color: $warning-color;
+}
\ No newline at end of file
diff --git a/src/CallHandler.js b/src/CallHandler.js
index e47209eebe9..5b58400ae65 100644
--- a/src/CallHandler.js
+++ b/src/CallHandler.js
@@ -344,7 +344,7 @@ function _onAction(payload) {
}
async function _startCallApp(roomId, type) {
- // check for a working intgrations manager. Technically we could put
+ // check for a working integrations manager. Technically we could put
// the state event in anyway, but the resulting widget would then not
// work for us. Better that the user knows before everyone else in the
// room sees it.
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index 61c51d4a204..79e5206f50a 100644
--- a/src/FromWidgetPostMessageApi.js
+++ b/src/FromWidgetPostMessageApi.js
@@ -17,9 +17,12 @@ limitations under the License.
import URL from 'url';
import dis from './dispatcher';
-import IntegrationManager from './IntegrationManager';
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
+import sdk from "./index";
+import Modal from "./Modal";
+import MatrixClientPeg from "./MatrixClientPeg";
+import RoomViewStore from "./stores/RoomViewStore";
const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
@@ -189,7 +192,14 @@ export default class FromWidgetPostMessageApi {
const data = event.data.data || event.data.widgetData;
const integType = (data && data.integType) ? data.integType : null;
const integId = (data && data.integId) ? data.integId : null;
- IntegrationManager.open(integType, integId);
+
+ // The dialog will take care of scalar auth for us
+ const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
+ Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
+ room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
+ screen: 'type_' + integType,
+ integrationId: integId,
+ }, "mx_IntegrationsManager");
} 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/IntegrationManager.js b/src/IntegrationManager.js
deleted file mode 100644
index 165ee6390dc..00000000000
--- a/src/IntegrationManager.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-Copyright 2017 New Vector Ltd
-
-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 Modal from './Modal';
-import sdk from './index';
-import SdkConfig from './SdkConfig';
-import ScalarMessaging from './ScalarMessaging';
-import ScalarAuthClient from './ScalarAuthClient';
-import RoomViewStore from './stores/RoomViewStore';
-
-if (!global.mxIntegrationManager) {
- global.mxIntegrationManager = {};
-}
-
-export default class IntegrationManager {
- static _init() {
- if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) {
- if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
- ScalarMessaging.startListening();
- global.mxIntegrationManager.client = new ScalarAuthClient();
-
- return global.mxIntegrationManager.client.connect().then(() => {
- global.mxIntegrationManager.connected = true;
- }).catch((e) => {
- console.error("Failed to connect to integrations server", e);
- global.mxIntegrationManager.error = e;
- });
- } else {
- console.error('Invalid integration manager config', SdkConfig.get());
- }
- }
- }
-
- /**
- * Launch the integrations manager on the stickers integration page
- * @param {string} integName integration / widget type
- * @param {string} integId integration / widget ID
- * @param {function} onFinished Callback to invoke on integration manager close
- */
- static async open(integName, integId, onFinished) {
- await IntegrationManager._init();
- if (global.mxIntegrationManager.client) {
- await global.mxIntegrationManager.client.connect();
- } else {
- return;
- }
- const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
- if (global.mxIntegrationManager.error ||
- !(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) {
- console.error("Scalar error", global.mxIntegrationManager);
- return;
- }
- const integType = 'type_' + integName;
- const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ?
- global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom(
- {roomId: RoomViewStore.getRoomId()},
- integType,
- integId,
- ) :
- null;
- Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
- src: src,
- onFinished: onFinished,
- }, "mx_IntegrationsManager");
- }
-}
diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js
index 24979aff65e..27d8f0d0da5 100644
--- a/src/ScalarAuthClient.js
+++ b/src/ScalarAuthClient.js
@@ -29,6 +29,14 @@ class ScalarAuthClient {
this.scalarToken = null;
}
+ /**
+ * Determines if setting up a ScalarAuthClient is even possible
+ * @returns {boolean} true if possible, false otherwise.
+ */
+ static isPossible() {
+ return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url'];
+ }
+
connect() {
return this.getScalarToken().then((tok) => {
this.scalarToken = tok;
@@ -41,7 +49,8 @@ class ScalarAuthClient {
// Returns a scalar_token string
getScalarToken() {
- const token = window.localStorage.getItem("mx_scalar_token");
+ let token = this.scalarToken;
+ if (!token) token = window.localStorage.getItem("mx_scalar_token");
if (!token) {
return this.registerForToken();
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 959cee7ace8..034a3318a5d 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -240,19 +240,13 @@ export default class AppTile extends React.Component {
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
+ // The dialog handles scalar auth for us
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
- this._scalarClient.connect().done(() => {
- const src = this._scalarClient.getScalarInterfaceUrlForRoom(
- this.props.room, 'type_' + this.props.type, this.props.id);
- Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
- src: src,
- }, "mx_IntegrationsManager");
- }, (err) => {
- this.setState({
- error: err.message,
- });
- console.error('Error ensuring a valid scalar_token exists', err);
- });
+ Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
+ room: this.props.room,
+ screen: 'type_' + this.props.type,
+ integrationId: this.props.id,
+ }, "mx_IntegrationsManager");
}
}
diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js
index 165cd20eb5f..ef5604dba60 100644
--- a/src/components/views/elements/ManageIntegsButton.js
+++ b/src/components/views/elements/ManageIntegsButton.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector Ltd
+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.
@@ -17,95 +18,34 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
-import classNames from 'classnames';
-import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
-import ScalarMessaging from '../../../ScalarMessaging';
import Modal from "../../../Modal";
import { _t } from '../../../languageHandler';
-import AccessibleButton from './AccessibleButton';
export default class ManageIntegsButton extends React.Component {
constructor(props) {
super(props);
-
- this.state = {
- scalarError: null,
- };
-
- this.onManageIntegrations = this.onManageIntegrations.bind(this);
}
- componentWillMount() {
- ScalarMessaging.startListening();
- this.scalarClient = null;
-
- if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
- this.scalarClient = new ScalarAuthClient();
- this.scalarClient.connect().done(() => {
- this.forceUpdate();
- }, (err) => {
- this.setState({scalarError: err});
- console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
- });
- }
- }
-
- componentWillUnmount() {
- ScalarMessaging.stopListening();
- }
-
- onManageIntegrations(ev) {
+ onManageIntegrations = (ev) => {
ev.preventDefault();
- if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
- return;
- }
+
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
- this.scalarClient.connect().done(() => {
- Modal.createDialog(IntegrationsManager, {
- src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
- this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
- null,
- }, "mx_IntegrationsManager");
- }, (err) => {
- this.setState({scalarError: err});
- console.error('Error ensuring a valid scalar_token exists', err);
- });
- }
+ Modal.createDialog(IntegrationsManager, {
+ room: this.props.room,
+ }, "mx_IntegrationsManager");
+ };
render() {
let integrationsButton =
;
- let integrationsWarningTriangle = ;
- let integrationsErrorPopup = ;
- if (this.scalarClient !== null) {
- const integrationsButtonClasses = classNames({
- mx_RoomHeader_button: true,
- mx_RoomHeader_manageIntegsButton: true,
- mx_ManageIntegsButton_error: !!this.state.scalarError,
- });
-
- if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
- integrationsWarningTriangle = ;
- // Popup shown when hovering over integrationsButton_error (via CSS)
- integrationsErrorPopup = (
-
- { _t('Could not connect to the integration server') }
-
- );
- }
-
+ if (ScalarAuthClient.isPossible()) {
+ const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = (
-
- { integrationsWarningTriangle }
- { integrationsErrorPopup }
-
+ />
);
}
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index e0e7a48b8c8..3e5528996fd 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -24,8 +24,6 @@ import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
import dis from '../../../dispatcher';
import sdk from '../../../index';
-import SdkConfig from '../../../SdkConfig';
-import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../utils/WidgetUtils';
@@ -63,20 +61,6 @@ module.exports = React.createClass({
},
componentDidMount: function() {
- this.scalarClient = null;
- if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
- this.scalarClient = new ScalarAuthClient();
- this.scalarClient.connect().then(() => {
- this.forceUpdate();
- }).catch((e) => {
- console.log('Failed to connect to integrations server');
- // TODO -- Handle Scalar errors
- // this.setState({
- // scalar_error: err,
- // });
- });
- }
-
this.dispatcherRef = dis.register(this.onAction);
},
@@ -144,16 +128,10 @@ module.exports = React.createClass({
_launchManageIntegrations: function() {
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
- this.scalarClient.connect().done(() => {
- const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
- this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
- null;
- Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
- src: src,
- }, 'mx_IntegrationsManager');
- }, (err) => {
- console.error('Error ensuring a valid scalar_token exists', err);
- });
+ Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
+ room: this.props.room,
+ screen: 'add_integ',
+ }, 'mx_IntegrationsManager');
},
onClickAddWidget: function(e) {
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index a0e3f1b7a9e..6918810842d 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
-import { _t } from '../../../languageHandler';
+import {_t, _td} from '../../../languageHandler';
import AppTile from '../elements/AppTile';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import sdk from '../../../index';
-import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
@@ -53,6 +52,9 @@ export default class Stickerpicker extends React.Component {
this.popoverWidth = 300;
this.popoverHeight = 300;
+ // This is loaded by _acquireScalarClient on an as-needed basis.
+ this.scalarClient = null;
+
this.state = {
showStickers: false,
imError: null,
@@ -63,14 +65,34 @@ export default class Stickerpicker extends React.Component {
};
}
- _removeStickerpickerWidgets() {
+ _acquireScalarClient() {
+ if (this.scalarClient) return Promise.resolve(this.scalarClient);
+ if (ScalarAuthClient.isPossible()) {
+ this.scalarClient = new ScalarAuthClient();
+ return this.scalarClient.connect().then(() => {
+ this.forceUpdate();
+ return this.scalarClient;
+ }).catch((e) => {
+ this._imError(_td("Failed to connect to integrations server"), e);
+ });
+ } else {
+ this._imError(_td("No integrations server is configured to manage stickers with"));
+ }
+ }
+
+ async _removeStickerpickerWidgets() {
+ const scalarClient = await this._acquireScalarClient();
console.warn('Removing Stickerpicker widgets');
if (this.state.widgetId) {
- this.scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
- console.warn('Assets disabled');
- }).catch((err) => {
- console.error('Failed to disable assets');
- });
+ if (scalarClient) {
+ scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
+ console.warn('Assets disabled');
+ }).catch((err) => {
+ console.error('Failed to disable assets');
+ });
+ } else {
+ console.error("Cannot disable assets: no scalar client");
+ }
} else {
console.warn('No widget ID specified, not disabling assets');
}
@@ -87,19 +109,7 @@ export default class Stickerpicker extends React.Component {
// Close the sticker picker when the window resizes
window.addEventListener('resize', this._onResize);
- this.scalarClient = null;
- if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
- this.scalarClient = new ScalarAuthClient();
- this.scalarClient.connect().then(() => {
- this.forceUpdate();
- }).catch((e) => {
- this._imError("Failed to connect to integrations server", e);
- });
- }
-
- if (!this.state.imError) {
- this.dispatcherRef = dis.register(this._onWidgetAction);
- }
+ this.dispatcherRef = dis.register(this._onWidgetAction);
// Track updates to widget state in account data
MatrixClientPeg.get().on('accountData', this._updateWidget);
@@ -126,7 +136,7 @@ export default class Stickerpicker extends React.Component {
console.error(errorMsg, e);
this.setState({
showStickers: false,
- imError: errorMsg,
+ imError: _t(errorMsg),
});
}
@@ -339,22 +349,13 @@ export default class Stickerpicker extends React.Component {
*/
_launchManageIntegrations() {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
- this.scalarClient.connect().done(() => {
- const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
- this.scalarClient.getScalarInterfaceUrlForRoom(
- this.props.room,
- 'type_' + widgetType,
- this.state.widgetId,
- ) :
- null;
- Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
- src: src,
- }, "mx_IntegrationsManager");
- this.setState({showStickers: false});
- }, (err) => {
- this.setState({imError: err});
- console.error('Error ensuring a valid scalar_token exists', err);
- });
+
+ // The integrations manager will handle scalar auth for us.
+ Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
+ room: this.props.room,
+ screen: `type_${widgetType}`,
+ integrationId: this.state.widgetId,
+ }, "mx_IntegrationsManager");
}
render() {
diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js
index a517771f1da..754693b73e4 100644
--- a/src/components/views/settings/IntegrationsManager.js
+++ b/src/components/views/settings/IntegrationsManager.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+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.
@@ -14,50 +15,124 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
+import React from 'react';
+import PropTypes from 'prop-types';
+import sdk from '../../../index';
+import { _t } from '../../../languageHandler';
+import dis from '../../../dispatcher';
+import ScalarAuthClient from '../../../ScalarAuthClient';
-const React = require('react');
-const sdk = require('../../../index');
-const MatrixClientPeg = require('../../../MatrixClientPeg');
-const dis = require('../../../dispatcher');
+export default class IntegrationsManager extends React.Component {
+ static propTypes = {
+ // the room object where the integrations manager should be opened in
+ room: PropTypes.object.isRequired,
-module.exports = React.createClass({
- displayName: 'IntegrationsManager',
+ // the screen name to open
+ screen: PropTypes.string,
- propTypes: {
- src: React.PropTypes.string.isRequired, // the source of the integration manager being embedded
- onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
- },
+ // the integration ID to open
+ integrationId: PropTypes.string,
- // XXX: keyboard shortcuts for managing dialogs should be done by the modal
- // dialog base class somehow, surely...
- componentDidMount: function() {
+ // callback when the manager is dismissed
+ onFinished: PropTypes.func.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ 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() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onKeyDown);
- },
+ }
- componentWillUnmount: function() {
+ componentWillUnmount() {
dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown);
- },
+ }
- onKeyDown: function(ev) {
- if (ev.keyCode == 27) { // escape
+ onKeyDown = (ev) => {
+ if (ev.keyCode === 27) { // escape
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
- },
+ };
- onAction: function(payload) {
+ onAction = (payload) => {
if (payload.action === 'close_scalar') {
this.props.onFinished();
}
- },
-
- render: function() {
- return (
-
- );
- },
-});
+ };
+
+ render() {
+ if (!this.state.configured) {
+ return (
+
+
{_t("No integrations server configured")}
+
{_t("This Riot instance does not have an integrations server configured.")}
+
+ );
+ }
+
+ if (this.state.loading) {
+ const Spinner = sdk.getComponent("elements.Spinner");
+ return (
+
+
{_t("Connecting to integrations server...")}
+
+
+ );
+ }
+
+ if (!this.state.connected) {
+ return (
+
+
{_t("Cannot connect to integrations server")}
+
{_t("The integrations server is offline or it cannot reach your homeserver.")}
+
+ );
+ }
+
+ return ;
+ }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 53fd82f6f25..c1fd3662ee2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -483,6 +483,11 @@
"Email Address": "Email Address",
"Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications",
+ "No integrations server configured": "No integrations server configured",
+ "This Riot instance does not have an integrations server configured.": "This Riot instance does not have an integrations server configured.",
+ "Connecting to integrations server...": "Connecting to integrations server...",
+ "Cannot connect to integrations server": "Cannot connect to integrations server",
+ "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.",
"Delete Backup": "Delete Backup",
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
@@ -864,6 +869,8 @@
"This Room": "This Room",
"All Rooms": "All Rooms",
"Search…": "Search…",
+ "Failed to connect to integrations server": "Failed to connect to integrations server",
+ "No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with",
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
"Add some now": "Add some now",
"Stickerpack": "Stickerpack",
@@ -1017,7 +1024,6 @@
"Rotate Right": "Rotate Right",
"Rotate clockwise": "Rotate clockwise",
"Download this file": "Download this file",
- "Integrations Error": "Integrations Error",
"Manage Integrations": "Manage Integrations",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",