diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index 505af9691d..a3916f321a 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -32,9 +32,9 @@ limitations under the License. width: 4px; height: 4px; border-radius: 16px; - overflow: hidden; background-color: $secondary-accent-color; border: 6px solid $accent-color; + pointer-events: none; } .mx_TopUnreadMessagesBar_scrollUp { diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index a560c956f1..694b2b0a25 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -43,7 +43,28 @@ export class AccessCancelledError extends Error { } } -async function getSecretStorageKey({ keys: keyInfos }) { +async function confirmToDismiss(name) { + let description; + if (name === "m.cross_signing.user_signing") { + description = _t("If you cancel now, you won't complete verifying the other user."); + } else if (name === "m.cross_signing.self_signing") { + description = _t("If you cancel now, you won't complete verifying your other session."); + } else { + description = _t("If you cancel now, you won't complete your secret storage operation."); + } + + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const [sure] = await Modal.createDialog(QuestionDialog, { + title: _t("Cancel entering passphrase?"), + description, + danger: true, + cancelButton: _t("Enter passphrase"), + button: _t("Cancel"), + }).finished; + return sure; +} + +async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); @@ -70,6 +91,7 @@ async function getSecretStorageKey({ keys: keyInfos }) { sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", AccessSecretStorageDialog, + /* props= */ { keyInfo: info, checkPrivateKey: async (input) => { @@ -77,6 +99,17 @@ async function getSecretStorageKey({ keys: keyInfos }) { return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); }, }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(ssssItemName); + } + return true; + }, + }, ); const [input] = await finished; if (!input) { diff --git a/src/Modal.js b/src/Modal.js index b6215b2b5a..de441740f1 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -47,7 +47,7 @@ class ModalManager { } */ ]; - this.closeAll = this.closeAll.bind(this); + this.onBackgroundClick = this.onBackgroundClick.bind(this); } hasDialogs() { @@ -106,7 +106,7 @@ class ModalManager { return this.appendDialogAsync(...rest); } - _buildModal(prom, props, className) { + _buildModal(prom, props, className, options) { const modal = {}; // never call this from onFinished() otherwise it will loop @@ -124,13 +124,27 @@ class ModalManager { ); modal.onFinished = props ? props.onFinished : null; modal.className = className; + modal.onBeforeClose = options.onBeforeClose; + modal.beforeClosePromise = null; + modal.close = closeDialog; + modal.closeReason = null; return {modal, closeDialog, onFinishedProm}; } _getCloseFn(modal, props) { const deferred = defer(); - return [(...args) => { + return [async (...args) => { + if (modal.beforeClosePromise) { + await modal.beforeClosePromise; + } else if (modal.onBeforeClose) { + modal.beforeClosePromise = modal.onBeforeClose(modal.closeReason); + const shouldClose = await modal.beforeClosePromise; + modal.beforeClosePromise = null; + if (!shouldClose) { + return; + } + } deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); const i = this._modals.indexOf(modal); @@ -156,6 +170,12 @@ class ModalManager { }, deferred.promise]; } + /** + * @callback onBeforeClose + * @param {string?} reason either "backgroundClick" or null + * @return {Promise} whether the dialog should close + */ + /** * Open a modal view. * @@ -183,11 +203,12 @@ class ModalManager { * also be removed from the stack. This is not compatible * with being a priority modal. Only one modal can be * static at a time. + * @param {Object} options? extra options for the dialog + * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @returns {object} Object with 'close' parameter being a function that will close the dialog */ - createDialogAsync(prom, props, className, isPriorityModal, isStaticModal) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className); - + createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) { + const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options); if (isPriorityModal) { // XXX: This is destructive this._priorityModal = modal; @@ -206,7 +227,7 @@ class ModalManager { } appendDialogAsync(prom, props, className) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className); + const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {}); this._modals.push(modal); this._reRender(); @@ -216,24 +237,22 @@ class ModalManager { }; } - closeAll() { - const modalsToClose = [...this._modals, this._priorityModal]; - this._modals = []; - this._priorityModal = null; - - if (this._staticModal && modalsToClose.length === 0) { - modalsToClose.push(this._staticModal); - this._staticModal = null; + onBackgroundClick() { + const modal = this._getCurrentModal(); + if (!modal) { + return; } + // we want to pass a reason to the onBeforeClose + // callback, but close is currently defined to + // pass all number of arguments to the onFinished callback + // so, pass the reason to close through a member variable + modal.closeReason = "backgroundClick"; + modal.close(); + modal.closeReason = null; + } - for (let i = 0; i < modalsToClose.length; i++) { - const m = modalsToClose[i]; - if (m && m.onFinished) { - m.onFinished(false); - } - } - - this._reRender(); + _getCurrentModal() { + return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal); } _reRender() { @@ -264,7 +283,7 @@ class ModalManager {
{ this._staticModal.elem }
-
+
); @@ -274,8 +293,8 @@ class ModalManager { ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); } - const modal = this._priorityModal ? this._priorityModal : this._modals[0]; - if (modal) { + const modal = this._getCurrentModal(); + if (modal !== this._staticModal) { const classes = "mx_Dialog_wrapper " + (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '') + (modal.className ? modal.className : ''); @@ -285,7 +304,7 @@ class ModalManager {
{modal.elem}
-
+
); diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index b98fecf22f..5ae90b694e 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -144,8 +144,10 @@ export default class ManageEventIndexDialog extends React.Component {
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
- {_t("Number of rooms:")} {formatCountLong(this.state.crawlingRoomsCount)} {_t("of ")} - {formatCountLong(this.state.roomCount)}
+ {_t("Indexed rooms:")} {_t("%(crawlingRooms)s out of %(totalRooms)s", { + crawlingRooms: formatCountLong(this.state.crawlingRoomsCount), + totalRooms: formatCountLong(this.state.roomCount), + })}
{crawlerState}
d.deviceId === otherDeviceId); + if (device) qrProps.otherUserDeviceKey = device.getFingerprint(); + } + + // Either direction shares these next few props + + const xsignInfo = cli.getStoredCrossSigningForUser(myUserId); + qrProps.otherUserKey = xsignInfo.getId("master"); + + qrProps.keys = [ + [myDeviceId, cli.getDeviceEd25519Key()], + [xsignInfo.getId("master"), xsignInfo.getId("master")], + ]; + } else { + // Doesn't matter which direction the verification is, we always show the same QR code + // for not-ourself verification. + const myXsignInfo = cli.getStoredCrossSigningForUser(myUserId); + const otherXsignInfo = cli.getStoredCrossSigningForUser(otherUserId); + const otherDevices = (await cli.getStoredDevicesForUser(otherUserId)) || []; + const otherDevice = otherDevices.find(d => d.deviceId === otherDeviceId); + + qrProps.keys = [ + [myDeviceId, cli.getDeviceEd25519Key()], + [myXsignInfo.getId("master"), myXsignInfo.getId("master")], + ]; + qrProps.otherUserKey = otherXsignInfo.getId("master"); + if (otherDevice) qrProps.otherUserDeviceKey = otherDevice.getFingerprint(); + } + + return qrProps; + } + + constructor(props) { + super(props); + } + render() { const query = { request: this.props.requestEventId, diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index 3527747a66..ad1aaf598c 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -20,7 +20,6 @@ import PropTypes from "prop-types"; import * as sdk from '../../../index'; import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import VerificationQRCode from "../elements/crypto/VerificationQRCode"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {_t} from "../../../languageHandler"; import E2EIcon from "../rooms/E2EIcon"; import { @@ -29,7 +28,7 @@ import { PHASE_READY, PHASE_DONE, PHASE_STARTED, - PHASE_CANCELLED, + PHASE_CANCELLED, VerificationRequest, } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import Spinner from "../elements/Spinner"; @@ -50,12 +49,24 @@ export default class VerificationPanel extends React.PureComponent { constructor(props) { super(props); - this.state = {}; + this.state = { + qrCodeProps: null, // generated by the VerificationQRCode component itself + }; this._hasVerifier = false; + this._generateQRCodeProps(props.request); + } + + async _generateQRCodeProps(verificationRequest: VerificationRequest) { + try { + this.setState({qrCodeProps: await VerificationQRCode.getPropsForRequest(verificationRequest)}); + } catch (e) { + console.error(e); + // Do nothing - we won't render a QR code. + } } renderQRPhase(pending) { - const {member, request} = this.props; + const {member} = this.props; const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let button; @@ -69,10 +80,7 @@ export default class VerificationPanel extends React.PureComponent { ); } - const cli = MatrixClientPeg.get(); - const crossSigningInfo = cli.getStoredCrossSigningForUser(request.otherUserId); - if (!crossSigningInfo || !request.requestEvent || !request.requestEvent.getId()) { - // for whatever reason we can't generate a QR code, offer only SAS Verification + if (!this.state.qrCodeProps) { return

Verify by emoji

{_t("Verify by comparing unique emoji.")}

@@ -81,12 +89,6 @@ export default class VerificationPanel extends React.PureComponent {
; } - const myKeyId = cli.getCrossSigningId(); - const qrCodeKeys = [ - [cli.getDeviceId(), cli.getDeviceEd25519Key()], - [myKeyId, myKeyId], - ]; - // TODO: add way to open camera to scan a QR code return
@@ -96,13 +98,7 @@ export default class VerificationPanel extends React.PureComponent { })}

- +
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d125d10cfb..83c15fc385 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -60,6 +60,12 @@ "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "The server does not support the room version specified.": "The server does not support the room version specified.", "Failure to create room": "Failure to create room", + "If you cancel now, you won't complete verifying the other user.": "If you cancel now, you won't complete verifying the other user.", + "If you cancel now, you won't complete verifying your other session.": "If you cancel now, you won't complete verifying your other session.", + "If you cancel now, you won't complete your secret storage operation.": "If you cancel now, you won't complete your secret storage operation.", + "Cancel entering passphrase?": "Cancel entering passphrase?", + "Enter passphrase": "Enter passphrase", + "Cancel": "Cancel", "Setting up keys": "Setting up keys", "Send anyway": "Send anyway", "Send": "Send", @@ -450,7 +456,6 @@ "Verify this device by confirming the following number appears on its screen.": "Verify this device by confirming the following number appears on its screen.", "Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.", "Unable to find a supported verification method.": "Unable to find a supported verification method.", - "Cancel": "Cancel", "Waiting for %(displayName)s to verify…": "Waiting for %(displayName)s to verify…", "They match": "They match", "They don't match": "They don't match", @@ -2020,7 +2025,6 @@ "Export room keys": "Export room keys", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", - "Enter passphrase": "Enter passphrase", "Confirm passphrase": "Confirm passphrase", "Export": "Export", "Import room keys": "Import room keys", @@ -2094,8 +2098,8 @@ "Riot is securely caching encrypted messages locally for them to appear in search results:": "Riot is securely caching encrypted messages locally for them to appear in search results:", "Space used:": "Space used:", "Indexed messages:": "Indexed messages:", - "Number of rooms:": "Number of rooms:", - "of ": "of ", + "Indexed rooms:": "Indexed rooms:", + "%(crawlingRooms)s out of %(totalRooms)s": "%(crawlingRooms)s out of %(totalRooms)s", "Message downloading sleep time(ms)": "Message downloading sleep time(ms)", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 9bcc2815e6..64dfd56b2f 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -66,6 +66,20 @@ class RoomViewStore extends Store { } _setState(newState) { + // If values haven't changed, there's nothing to do. + // This only tries a shallow comparison, so unchanged objects will slip + // through, but that's probably okay for now. + let stateChanged = false; + for (const key of Object.keys(newState)) { + if (this._state[key] !== newState[key]) { + stateChanged = true; + break; + } + } + if (!stateChanged) { + return; + } + this._state = Object.assign(this._state, newState); this.__emitChange(); }