diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index d5eeebf1f2..c7d5804d66 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -18,7 +18,6 @@ src/components/views/create_room/RoomAlias.js src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/SetPasswordDialog.js src/components/views/dialogs/UnknownDeviceDialog.js -src/components/views/directory/NetworkDropdown.js src/components/views/elements/AddressSelector.js src/components/views/elements/DirectorySearchBox.js src/components/views/elements/ImageView.js diff --git a/package.json b/package.json index 84d5632023..124707294d 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "file-loader": "^3.0.1", "flow-parser": "^0.57.3", "jest-mock": "^23.2.0", - "karma": "^3.0.0", + "karma": "^3.1.2", "karma-chrome-launcher": "^0.2.3", "karma-cli": "^1.0.1", "karma-junit-reporter": "^0.4.2", diff --git a/res/css/_common.scss b/res/css/_common.scss index 9725340978..1e388c4531 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -228,6 +228,17 @@ textarea { color: $roomsublist-label-bg-color; } +/* Expected z-indexes for dialogs: + 4000 - Default wrapper index + 4009 - Static dialog background + 4010 - Static dialog itself + 4011 - Standard dialog background + 4012 - Standard dialog itself + + These are set up such that the static dialog always appears + underneath the standard dialogs. + */ + .mx_Dialog_wrapper { position: fixed; z-index: 4000; @@ -252,7 +263,7 @@ textarea { .mx_Dialog { background-color: $primary-bg-color; color: $light-fg-color; - z-index: 4010; + z-index: 4012; font-weight: 300; font-size: 15px; position: relative; @@ -264,6 +275,10 @@ textarea { overflow-y: auto; } +.mx_Dialog_staticWrapper .mx_Dialog { + z-index: 4010; +} + .mx_Dialog_background { position: fixed; top: 0; @@ -272,6 +287,17 @@ textarea { height: 100%; background-color: $dialog-backdrop-color; opacity: 0.8; + z-index: 4011; +} + +.mx_Dialog_background.mx_Dialog_staticBackground { + z-index: 4009; +} + +.mx_Dialog_wrapperWithStaticUnder .mx_Dialog_background { + // Roughly half of what it would normally be - we don't want to black out + // the app, just make it clear that the dialogs are stacked. + opacity: 0.4; } .mx_Dialog_lightbox .mx_Dialog_background { diff --git a/res/css/_components.scss b/res/css/_components.scss index f3b07255ae..6f66a8c15e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -85,6 +85,7 @@ @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_MemberEventListSummary.scss"; +@import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_ReplyThread.scss"; @import "./views/elements/_ResizeHandle.scss"; diff --git a/res/css/views/elements/_PowerSelector.scss b/res/css/views/elements/_PowerSelector.scss new file mode 100644 index 0000000000..69f3a8eebb --- /dev/null +++ b/res/css/views/elements/_PowerSelector.scss @@ -0,0 +1,25 @@ +/* +Copyright 2019 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. +*/ + +.mx_PowerSelector { + width: 100%; +} + +.mx_PowerSelector .mx_Field select, +.mx_PowerSelector .mx_Field input { + width: 100%; + box-sizing: border-box; +} diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index c095fc26af..adf16d6c4a 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -24,9 +24,14 @@ limitations under the License. .mx_CreateEvent_image { float: left; - padding-right: 20px; + margin-right: 20px; width: 72px; height: 34px; + + background-color: $primary-fg-color; + mask: url('$(res)/img/room-continuation.svg'); + mask-repeat: no-repeat; + mask-position: center; } .mx_CreateEvent_header { diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index 8f89b83003..c3b3ca2f7d 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -27,7 +27,7 @@ limitations under the License. } .mx_MemberInfo_name > .mx_E2EIcon { - margin-left: 0; + margin-right: 0; } .mx_MemberInfo_cancel { diff --git a/scripts/travis/unit-tests.sh b/scripts/travis/unit-tests.sh index a8e0a63b31..a7e8425fa0 100755 --- a/scripts/travis/unit-tests.sh +++ b/scripts/travis/unit-tests.sh @@ -7,4 +7,4 @@ set -ev scripts/travis/build.sh -npm run test +CHROME_BIN='/usr/bin/google-chrome-stable' npm run test diff --git a/src/Modal.js b/src/Modal.js index 960e0e5c30..4d90e313ce 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -26,6 +26,7 @@ import dis from './dispatcher'; import { _t } from './languageHandler'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; +const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; /** * Wrap an asynchronous loader function with a react component which shows a @@ -106,7 +107,12 @@ class ModalManager { // this modal. Remove all other modals from the stack when this modal // is closed. this._priorityModal = null; + // The modal to keep open underneath other modals if possible. Useful + // for cases like Settings where the modal should remain open while the + // user is prompted for more information/errors. + this._staticModal = null; // A list of the modals we have stacked up, with the most recent at [0] + // Neither the static nor priority modal will be in this list. this._modals = [ /* { elem: React component for this dialog @@ -130,6 +136,18 @@ class ModalManager { return container; } + getOrCreateStaticContainer() { + let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); + + if (!container) { + container = document.createElement("div"); + container.id = STATIC_DIALOG_CONTAINER_ID; + document.body.appendChild(container); + } + + return container; + } + createTrackedDialog(analyticsAction, analyticsInfo, ...rest) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); return this.createDialog(...rest); @@ -166,8 +184,13 @@ class ModalManager { * of other modals that are currently in the stack. * Also, when closed, all modals will be removed * from the stack. + * @param {boolean} isStaticModal if true, this modal will be displayed under other + * modals in the stack. When closed, all modals will + * also be removed from the stack. This is not compatible + * with being a priority modal. Only one modal can be + * static at a time. */ - createDialogAsync(prom, props, className, isPriorityModal) { + createDialogAsync(prom, props, className, isPriorityModal, isStaticModal) { const self = this; const modal = {}; @@ -188,6 +211,13 @@ class ModalManager { self._modals = []; } + if (self._staticModal === modal) { + self._staticModal = null; + + // XXX: This is destructive + self._modals = []; + } + self._reRender(); }; @@ -207,6 +237,9 @@ class ModalManager { if (isPriorityModal) { // XXX: This is destructive this._priorityModal = modal; + } else if (isStaticModal) { + // This is intentionally destructive + this._staticModal = modal; } else { this._modals.unshift(modal); } @@ -216,12 +249,18 @@ class ModalManager { } closeAll() { - const modals = this._modals; + const modalsToClose = [...this._modals, this._priorityModal]; this._modals = []; + this._priorityModal = null; - for (let i = 0; i < modals.length; i++) { - const m = modals[i]; - if (m.onFinished) { + if (this._staticModal && modalsToClose.length === 0) { + modalsToClose.push(this._staticModal); + this._staticModal = null; + } + + for (let i = 0; i < modalsToClose.length; i++) { + const m = modalsToClose[i]; + if (m && m.onFinished) { m.onFinished(false); } } @@ -230,13 +269,14 @@ class ModalManager { } _reRender() { - if (this._modals.length == 0 && !this._priorityModal) { + if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) { // If there is no modal to render, make all of Riot available // to screen reader users again dis.dispatch({ action: 'aria_unhide_main_app', }); ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); + ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); return; } @@ -247,17 +287,45 @@ class ModalManager { action: 'aria_hide_main_app', }); - const modal = this._priorityModal ? this._priorityModal : this._modals[0]; - const dialog = ( -