diff --git a/src/Modal.js b/src/Modal.js index f0ab97a91e..89e8b1361c 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -21,6 +21,8 @@ var React = require('react'); var ReactDOM = require('react-dom'); import sdk from './index'; +const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; + /** * Wrap an asynchronous loader function with a react component which shows a * spinner until the real component loads. @@ -67,26 +69,37 @@ const AsyncWrapper = React.createClass({ }, }); -let _counter = 0; +class ModalManager { + constructor() { + this._counter = 0; -module.exports = { - DialogContainerId: "mx_Dialog_Container", + /** list of the modals we have stacked up, with the most recent at [0] */ + this._modals = [ + /* { + elem: React component for this dialog + onFinished: caller-supplied onFinished callback + className: CSS class for the dialog wrapper div + } */ + ]; - getOrCreateContainer: function() { - var container = document.getElementById(this.DialogContainerId); + this.closeAll = this.closeAll.bind(this); + } + + getOrCreateContainer() { + let container = document.getElementById(DIALOG_CONTAINER_ID); if (!container) { container = document.createElement("div"); - container.id = this.DialogContainerId; + container.id = DIALOG_CONTAINER_ID; document.body.appendChild(container); } return container; - }, + } - createDialog: function(Element, props, className) { + createDialog(Element, props, className) { return this.createDialogAsync((cb) => {cb(Element);}, props, className); - }, + } /** * Open a modal view. @@ -107,31 +120,73 @@ module.exports = { * * @param {String} className CSS class to apply to the modal wrapper */ - createDialogAsync: function(loader, props, className) { + createDialogAsync(loader, props, className) { var self = this; - // never call this via modal.close() from onFinished() otherwise it will loop + const modal = {}; + + // never call this from onFinished() otherwise it will loop + // + // nb explicit function() rather than arrow function, to get `arguments` var closeDialog = function() { if (props && props.onFinished) props.onFinished.apply(null, arguments); - ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); + var i = self._modals.indexOf(modal); + if (i >= 0) { + self._modals.splice(i, 1); + } + self._reRender(); }; // don't attempt to reuse the same AsyncWrapper for different dialogs, // otherwise we'll get confused. - const modalCount = _counter++; + const modalCount = this._counter++; // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // property set here so you can't close the dialog from a button click! + modal.elem = ( + + ); + modal.onFinished = props ? props.onFinished : null; + modal.className = className; + + this._modals.unshift(modal); + + this._reRender(); + return {close: closeDialog}; + } + + closeAll() { + const modals = this._modals; + this._modals = []; + + for (let i = 0; i < modals.length; i++) { + const m = modals[i]; + if (m.onFinished) { + m.onFinished(false); + } + } + + this._reRender(); + } + + _reRender() { + if (this._modals.length == 0) { + ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); + return; + } + + var modal = this._modals[0]; var dialog = ( -
+
- + {modal.elem}
-
+
); ReactDOM.render(dialog, this.getOrCreateContainer()); + } +} - return {close: closeDialog}; - }, -}; +export default new ModalManager(); diff --git a/src/SdkConfig.js b/src/SdkConfig.js index 1452aaa64b..8d8e93a889 100644 --- a/src/SdkConfig.js +++ b/src/SdkConfig.js @@ -19,6 +19,8 @@ var DEFAULTS = { integrations_ui_url: "https://scalar.vector.im/", // Base URL to the REST interface of the integrations server integrations_rest_url: "https://scalar.vector.im/api", + // Where to send bug reports. If not specified, bugs cannot be sent. + bug_report_endpoint_url: null, }; class SdkConfig { diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1e060ae7ff..cf4a63e2f7 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -26,6 +26,7 @@ var UserSettingsStore = require('../../UserSettingsStore'); var GeminiScrollbar = require('react-gemini-scrollbar'); var Email = require('../../email'); var AddThreepid = require('../../AddThreepid'); +var SdkConfig = require('../../SdkConfig'); import AccessibleButton from '../views/elements/AccessibleButton'; // if this looks like a release, use the 'version' from package.json; else use @@ -388,6 +389,14 @@ module.exports = React.createClass({ Modal.createDialog(DeactivateAccountDialog, {}); }, + _onBugReportClicked: function() { + const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); + if (!BugReportDialog) { + return; + } + Modal.createDialog(BugReportDialog, {}); + }, + _onInviteStateChange: function(event, member, oldMembership) { if (member.userId === this._me && oldMembership === "invite") { this.forceUpdate(); @@ -547,6 +556,23 @@ module.exports = React.createClass({ ); }, + _renderBugReport: function() { + if (!SdkConfig.get().bug_report_endpoint_url) { + return
+ } + return ( +
+

Bug Report

+
+

Found a bug?

+ +
+
+ ); + }, + _renderLabs: function() { // default to enabled if undefined if (this.props.enableLabs === false) return null; @@ -800,6 +826,7 @@ module.exports = React.createClass({ {this._renderDevicesPanel()} {this._renderCryptoInfo()} {this._renderBulkOptions()} + {this._renderBugReport()}

Advanced

diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index fa0c63dfdd..0842d0f4dd 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -27,6 +27,7 @@ var linkify = require('linkifyjs'); var linkifyElement = require('linkifyjs/element'); var linkifyMatrix = require('../../../linkify-matrix'); import AccessibleButton from '../elements/AccessibleButton'; +import {CancelButton} from './SimpleRoomHeader'; linkifyMatrix(linkify); @@ -184,7 +185,7 @@ module.exports = React.createClass({ ); save_button = Save; - cancel_button = Cancel ; + cancel_button = ; } if (this.props.saving) { diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index bc2f4bca69..40995d2a72 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -16,16 +16,27 @@ limitations under the License. 'use strict'; -var React = require('react'); -var sdk = require('../../../index'); -var dis = require("../../../dispatcher"); +import React from 'react'; +import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; +// cancel button which is shared between room header and simple room header +export function CancelButton(props) { + const {onClick} = props; + + return ( + + Cancel + + ); +} + /* * A stripped-down room header used for things like the user settings * and room directory. */ -module.exports = React.createClass({ +export default React.createClass({ displayName: 'SimpleRoomHeader', propTypes: { @@ -41,15 +52,15 @@ module.exports = React.createClass({ }, render: function() { - var TintableSvg = sdk.getComponent("elements.TintableSvg"); - - var cancelButton; + let cancelButton; if (this.props.onCancelClick) { - cancelButton = Cancel ; + cancelButton = ; } - var showRhsButton; + let showRhsButton; /* // don't bother cluttering things up with this for now. + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + if (this.props.collapsedRhs) { showRhsButton =