Merge branches 'develop' and 't3chguy/password_completion' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/password_completion

This commit is contained in:
Michael Telatynski 2020-02-09 14:19:44 +00:00
commit 150f2b3f84
8 changed files with 196 additions and 57 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -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<bool>} 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 {
<div className="mx_Dialog">
{ this._staticModal.elem }
</div>
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.closeAll}></div>
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick}></div>
</div>
);
@ -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 {
<div className="mx_Dialog">
{modal.elem}
</div>
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick}></div>
</div>
);

View File

@ -144,8 +144,10 @@ export default class ManageEventIndexDialog extends React.Component {
<div className='mx_SettingsTab_subsectionText'>
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
{_t("Number of rooms:")} {formatCountLong(this.state.crawlingRoomsCount)} {_t("of ")}
{formatCountLong(this.state.roomCount)}<br />
{_t("Indexed rooms:")} {_t("%(crawlingRooms)s out of %(totalRooms)s", {
crawlingRooms: formatCountLong(this.state.crawlingRoomsCount),
totalRooms: formatCountLong(this.state.roomCount),
})} <br />
{crawlerState}<br />
<Field
id={"crawlerSleepTimeMs"}

View File

@ -19,6 +19,9 @@ import PropTypes from "prop-types";
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import * as qs from "qs";
import QRCode from "qrcode-react";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {ToDeviceChannel} from "matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel";
@replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent {
@ -31,13 +34,81 @@ export default class VerificationQRCode extends React.PureComponent {
// User verification use case only
secret: PropTypes.string,
otherUserKey: PropTypes.string, // Base64 key being verified
requestEventId: PropTypes.string,
otherUserDeviceKey: PropTypes.string, // Base64 key of the other user's device (or what we think it is; optional)
requestEventId: PropTypes.string, // for DM verification only
};
static defaultProps = {
action: "verify",
};
static async getPropsForRequest(verificationRequest: VerificationRequest) {
const cli = MatrixClientPeg.get();
const myUserId = cli.getUserId();
const otherUserId = verificationRequest.otherUserId;
const myDeviceId = cli.getDeviceId();
const otherDevice = verificationRequest.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const qrProps = {
secret: verificationRequest.encodedSharedSecret,
keyholderUserId: myUserId,
action: "verify",
keys: [], // array of pairs: keyId, base64Key
otherUserKey: "", // base64key
otherUserDeviceKey: "", // base64key
requestEventId: "", // we figure this out in a moment
};
const requestEvent = verificationRequest.requestEvent;
qrProps.requestEventId = requestEvent.getId()
? requestEvent.getId()
: ToDeviceChannel.getTransactionId(requestEvent);
// Populate the keys we need depending on which direction and users are involved in the verification.
if (myUserId === otherUserId) {
if (!otherDeviceId) {
// Existing scanning New session's QR code
qrProps.otherUserDeviceKey = null;
} else {
// New scanning Existing session's QR code
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
const device = myDevices.find(d => 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,

View File

@ -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 <div className="mx_UserInfo_container">
<h3>Verify by emoji</h3>
<p>{_t("Verify by comparing unique emoji.")}</p>
@ -81,12 +89,6 @@ export default class VerificationPanel extends React.PureComponent {
</div>;
}
const myKeyId = cli.getCrossSigningId();
const qrCodeKeys = [
[cli.getDeviceId(), cli.getDeviceEd25519Key()],
[myKeyId, myKeyId],
];
// TODO: add way to open camera to scan a QR code
return <React.Fragment>
<div className="mx_UserInfo_container">
@ -96,13 +98,7 @@ export default class VerificationPanel extends React.PureComponent {
})}</p>
<div className="mx_VerificationPanel_qrCode">
<VerificationQRCode
keyholderUserId={MatrixClientPeg.get().getUserId()}
requestEventId={request.requestEvent.getId()}
otherUserKey={crossSigningInfo.getId("master")}
secret={request.encodedSharedSecret}
keys={qrCodeKeys}
/>
<VerificationQRCode {...this.state.qrCodeProps} />
</div>
</div>

View File

@ -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",

View File

@ -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();
}