Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/semicolon

This commit is contained in:
Jorik Schellekens 2020-06-18 14:54:20 +01:00
commit 291997421b
21 changed files with 506 additions and 487 deletions

View file

@ -98,7 +98,3 @@ limitations under the License.
} }
} }
} }
.mx_CompleteSecurity_resetText {
padding-top: 20px;
}

View file

@ -73,42 +73,33 @@ limitations under the License.
margin-left: 20px; margin-left: 20px;
} }
.mx_CreateSecretStorageDialog_recoveryKeyHeader {
margin-bottom: 1em;
}
.mx_CreateSecretStorageDialog_recoveryKeyContainer { .mx_CreateSecretStorageDialog_recoveryKeyContainer {
width: 380px; display: flex;
margin-left: auto;
margin-right: auto;
} }
.mx_CreateSecretStorageDialog_recoveryKey { .mx_CreateSecretStorageDialog_recoveryKey {
font-weight: bold; width: 262px;
text-align: center;
padding: 20px; padding: 20px;
color: $info-plinth-fg-color; color: $info-plinth-fg-color;
background-color: $info-plinth-bg-color; background-color: $info-plinth-bg-color;
border-radius: 6px; margin-right: 12px;
word-spacing: 1em;
margin-bottom: 20px;
} }
.mx_CreateSecretStorageDialog_recoveryKeyButtons { .mx_CreateSecretStorageDialog_recoveryKeyButtons {
flex: 1;
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
} }
.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton {
width: 160px; margin-right: 10px;
padding-left: 0px; }
padding-right: 0px;
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
flex: 1;
white-space: nowrap; white-space: nowrap;
} }
.mx_CreateSecretStorageDialog_continueSpinner {
margin-top: 33px;
text-align: right;
}
.mx_CreateSecretStorageDialog_continueSpinner img {
width: 20px;
height: 20px;
}

View file

@ -20,7 +20,6 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import SettingsStore from './settings/SettingsStore';
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
// This stores the secret storage private keys in memory for the JS SDK. This is // This stores the secret storage private keys in memory for the JS SDK. This is
@ -30,14 +29,9 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
// operation ends. // operation ends.
let secretStorageKeys = {}; let secretStorageKeys = {};
let secretStorageBeingAccessed = false; let secretStorageBeingAccessed = false;
// Stores the 'passphraseOnly' option for the active storage access operation
let passphraseOnlyOption = null;
function isCachingAllowed() { function isCachingAllowed() {
return ( return secretStorageBeingAccessed;
secretStorageBeingAccessed ||
SettingsStore.getValue("keepSecretStoragePassphraseForSession")
);
} }
export class AccessCancelledError extends Error { export class AccessCancelledError extends Error {
@ -101,7 +95,6 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
const key = await inputToKey(input); const key = await inputToKey(input);
return await MatrixClientPeg.get().checkSecretStorageKey(key, info); return await MatrixClientPeg.get().checkSecretStorageKey(key, info);
}, },
passphraseOnly: passphraseOnlyOption,
}, },
/* className= */ null, /* className= */ null,
/* isPriorityModal= */ false, /* isPriorityModal= */ false,
@ -216,27 +209,19 @@ export async function promptForBackupPassphrase() {
* *
* @param {Function} [func] An operation to perform once secret storage has been * @param {Function} [func] An operation to perform once secret storage has been
* bootstrapped. Optional. * bootstrapped. Optional.
* @param {object} [opts] Named options * @param {bool} [forceReset] Reset secret storage even if it's already set up
* @param {bool} [opts.forceReset] Reset secret storage even if it's already set up
* @param {object} [opts.withKeys] Map of key ID to key for SSSS keys that the client
* already has available. If a key is not supplied here, the user will be prompted.
* @param {bool} [opts.passphraseOnly] If true, do not prompt for recovery key or to reset keys
*/ */
export async function accessSecretStorage( export async function accessSecretStorage(func = async () => { }, forceReset = false) {
func = async () => { }, opts = {},
) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
secretStorageBeingAccessed = true; secretStorageBeingAccessed = true;
passphraseOnlyOption = opts.passphraseOnly;
secretStorageKeys = Object.assign({}, opts.withKeys || {});
try { try {
if (!await cli.hasSecretStorageKey() || opts.forceReset) { if (!await cli.hasSecretStorageKey() || forceReset) {
// This dialog calls bootstrap itself after guiding the user through // This dialog calls bootstrap itself after guiding the user through
// passphrase creation. // passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
{ {
force: opts.forceReset, force: forceReset,
}, },
null, /* priority = */ false, /* static = */ true, null, /* priority = */ false, /* static = */ true,
); );
@ -274,6 +259,5 @@ export async function accessSecretStorage(
if (!isCachingAllowed()) { if (!isCachingAllowed()) {
secretStorageKeys = {}; secretStorageKeys = {};
} }
passphraseOnlyOption = null;
} }
} }

View file

@ -20,23 +20,25 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import {_t} from '../../../../languageHandler'; import {_t, _td} from '../../../../languageHandler';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import {copyNode} from "../../../../utils/strings"; import {copyNode} from "../../../../utils/strings";
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import PassphraseField from "../../../../components/views/auth/PassphraseField";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
const PHASE_LOADING = 0; const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1; const PHASE_LOADERROR = 1;
const PHASE_MIGRATE = 2; const PHASE_MIGRATE = 2;
const PHASE_INTRO = 3; const PHASE_PASSPHRASE = 3;
const PHASE_SHOWKEY = 4; const PHASE_PASSPHRASE_CONFIRM = 4;
const PHASE_STORING = 5; const PHASE_SHOWKEY = 5;
const PHASE_CONFIRM_SKIP = 6; const PHASE_KEEPITSAFE = 6;
const PHASE_STORING = 7;
const PHASE_DONE = 8;
const PHASE_CONFIRM_SKIP = 9;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
/* /*
* Walks the user through the process of creating a passphrase to guard Secure * Walks the user through the process of creating a passphrase to guard Secure
@ -63,32 +65,34 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.state = { this.state = {
phase: PHASE_LOADING, phase: PHASE_LOADING,
downloaded: false, passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
copied: false, copied: false,
downloaded: false,
backupInfo: null, backupInfo: null,
backupInfoFetched: false,
backupInfoFetchError: null,
backupSigStatus: null, backupSigStatus: null,
// does the server offer a UI auth flow with just m.login.password // does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload? (If we have an account password, we // for /keys/device_signing/upload?
// assume that it can)
canUploadKeysWithPasswordOnly: null, canUploadKeysWithPasswordOnly: null,
canUploadKeyCheckInProgress: false,
accountPassword: props.accountPassword || "", accountPassword: props.accountPassword || "",
accountPasswordCorrect: null, accountPasswordCorrect: null,
// No toggle for this: if we really don't want one, remove it & just hard code true // status of the key backup toggle switch
useKeyBackup: true, useKeyBackup: true,
}; };
if (props.accountPassword) {
// If we have an account password, we assume we can upload keys with
// just a password (otherwise leave it as null so we poll to check)
this.state.canUploadKeysWithPasswordOnly = true;
}
this._passphraseField = createRef(); this._passphraseField = createRef();
this.loadData(); this._fetchBackupInfo();
if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
this.state.canUploadKeysWithPasswordOnly = true;
} else {
this._queryKeyUploadAuth();
}
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
} }
@ -105,11 +109,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
); );
const { force } = this.props;
const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE;
this.setState({ this.setState({
backupInfoFetched: true, phase,
backupInfo, backupInfo,
backupSigStatus, backupSigStatus,
backupInfoFetchError: null,
}); });
return { return {
@ -117,25 +123,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
backupSigStatus, backupSigStatus,
}; };
} catch (e) { } catch (e) {
this.setState({backupInfoFetchError: e}); this.setState({phase: PHASE_LOADERROR});
} }
} }
async _queryKeyUploadAuth() { async _queryKeyUploadAuth() {
try { try {
this.setState({canUploadKeyCheckInProgress: true});
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
// We should never get here: the server should always require // We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload // UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op. // no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
this.setState({canUploadKeyCheckInProgress: false});
} catch (error) { } catch (error) {
if (!error.data || !error.data.flows) { if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!"); console.log("uploadDeviceSigningKeys advertised no flows!");
this.setState({
canUploadKeyCheckInProgress: false,
});
return; return;
} }
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
@ -143,18 +144,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}); });
this.setState({ this.setState({
canUploadKeysWithPasswordOnly, canUploadKeysWithPasswordOnly,
canUploadKeyCheckInProgress: false,
}); });
} }
} }
async _createRecoveryKey() {
this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
phase: PHASE_SHOWKEY,
});
}
_onKeyBackupStatusChange = () => { _onKeyBackupStatusChange = () => {
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo();
} }
@ -163,6 +156,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._recoveryKeyNode = n; this._recoveryKeyNode = n;
} }
_onUseKeyBackupChange = (enabled) => {
this.setState({
useKeyBackup: enabled,
});
}
_onMigrateFormSubmit = (e) => { _onMigrateFormSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (this.state.backupSigStatus.usable) { if (this.state.backupSigStatus.usable) {
@ -172,15 +171,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
} }
_onIntroContinueClick = () => {
this._createRecoveryKey();
}
_onCopyClick = () => { _onCopyClick = () => {
const successful = copyNode(this._recoveryKeyNode); const successful = copyNode(this._recoveryKeyNode);
if (successful) { if (successful) {
this.setState({ this.setState({
copied: true, copied: true,
phase: PHASE_KEEPITSAFE,
}); });
} }
} }
@ -190,8 +186,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type: 'text/plain;charset=us-ascii', type: 'text/plain;charset=us-ascii',
}); });
FileSaver.saveAs(blob, 'recovery-key.txt'); FileSaver.saveAs(blob, 'recovery-key.txt');
this.setState({ this.setState({
downloaded: true, downloaded: true,
phase: PHASE_KEEPITSAFE,
}); });
} }
@ -247,9 +245,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_bootstrapSecretStorage = async () => { _bootstrapSecretStorage = async () => {
this.setState({ this.setState({
// we use LOADING here rather than STORING as STORING still shows the 'show key' phase: PHASE_STORING,
// screen which is not relevant: LOADING is just a generic spinner.
phase: PHASE_LOADING,
error: null, error: null,
}); });
@ -290,7 +286,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}, },
}); });
} }
this.props.onFinished(true); this.setState({
phase: PHASE_DONE,
});
} catch (e) { } catch (e) {
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
this.setState({ this.setState({
@ -309,6 +307,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.props.onFinished(false); this.props.onFinished(false);
} }
_onDone = () => {
this.props.onFinished(true);
}
_restoreBackup = async () => { _restoreBackup = async () => {
// It's possible we'll need the backup key later on for bootstrapping, // It's possible we'll need the backup key later on for bootstrapping,
// so let's stash it here, rather than prompting for it twice. // so let's stash it here, rather than prompting for it twice.
@ -335,41 +337,88 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
} }
_onShowKeyContinueClick = () => {
this._bootstrapSecretStorage();
}
_onLoadRetryClick = () => { _onLoadRetryClick = () => {
this.loadData();
}
async loadData() {
this.setState({phase: PHASE_LOADING}); this.setState({phase: PHASE_LOADING});
const proms = []; this._fetchBackupInfo();
if (!this.state.backupInfoFetched) proms.push(this._fetchBackupInfo());
if (this.state.canUploadKeysWithPasswordOnly === null) proms.push(this._queryKeyUploadAuth());
await Promise.all(proms);
if (this.state.canUploadKeysWithPasswordOnly === null || this.state.backupInfoFetchError) {
this.setState({phase: PHASE_LOADERROR});
} else if (this.state.backupInfo && !this.props.force) {
this.setState({phase: PHASE_MIGRATE});
} else {
this.setState({phase: PHASE_INTRO});
}
} }
_onSkipSetupClick = () => { _onSkipSetupClick = () => {
this.setState({phase: PHASE_CONFIRM_SKIP}); this.setState({phase: PHASE_CONFIRM_SKIP});
} }
_onGoBackClick = () => { _onSetUpClick = () => {
if (this.state.backupInfo && !this.props.force) { this.setState({phase: PHASE_PASSPHRASE});
this.setState({phase: PHASE_MIGRATE}); }
} else {
this.setState({phase: PHASE_INTRO}); _onSkipPassPhraseClick = async () => {
this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseNextClick = async (e) => {
e.preventDefault();
if (!this._passphraseField.current) return; // unmounting
await this._passphraseField.current.validate({ allowEmpty: false });
if (!this._passphraseField.current.state.valid) {
this._passphraseField.current.focus();
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
return;
} }
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
};
_onPassPhraseConfirmNextClick = async (e) => {
e.preventDefault();
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
});
}
_onKeepItSafeBackClick = () => {
this.setState({
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseValidate = (result) => {
this.setState({
passPhraseValid: result.valid,
});
};
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
}
_onPassPhraseConfirmChange = (e) => {
this.setState({
passPhraseConfirm: e.target.value,
});
} }
_onAccountPasswordChange = (e) => { _onAccountPasswordChange = (e) => {
@ -384,14 +433,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// Once we're confident enough in this (and it's supported enough) we can do // Once we're confident enough in this (and it's supported enough) we can do
// it automatically. // it automatically.
// https://github.com/vector-im/riot-web/issues/11696 // https://github.com/vector-im/riot-web/issues/11696
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field'); const Field = sdk.getComponent('views.elements.Field');
let authPrompt; let authPrompt;
let nextCaption = _t("Next"); let nextCaption = _t("Next");
if (!this.state.backupSigStatus.usable) { if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = null;
nextCaption = _t("Upload");
} else if (this.state.canUploadKeysWithPasswordOnly && !this.props.accountPassword) {
authPrompt = <div> authPrompt = <div>
<div>{_t("Enter your account password to confirm the upgrade:")}</div> <div>{_t("Enter your account password to confirm the upgrade:")}</div>
<div><Field <div><Field
@ -403,6 +450,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
autoFocus={true} autoFocus={true}
/></div> /></div>
</div>; </div>;
} else if (!this.state.backupSigStatus.usable) {
authPrompt = <div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
</div>;
nextCaption = _t("Restore");
} else { } else {
authPrompt = <p> authPrompt = <p>
{_t("You'll need to authenticate with the server to confirm the upgrade.")} {_t("You'll need to authenticate with the server to confirm the upgrade.")}
@ -411,9 +463,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return <form onSubmit={this._onMigrateFormSubmit}> return <form onSubmit={this._onMigrateFormSubmit}>
<p>{_t( <p>{_t(
"Upgrade your Recovery Key to store encryption keys & secrets " + "Upgrade this session to allow it to verify other sessions, " +
"with your account data. If you lose access to this login you'll " + "granting them access to encrypted messages and marking them " +
"need it to unlock your data.", "as trusted for other users.",
)}</p> )}</p>
<div>{authPrompt}</div> <div>{authPrompt}</div>
<DialogButtons <DialogButtons
@ -429,49 +481,185 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</form>; </form>;
} }
_renderPhaseShowKey() { _renderPhasePassPhrase() {
let continueButton; const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
if (this.state.phase === PHASE_SHOWKEY) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
continueButton = <DialogButtons primaryButton={_t("Continue")} const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
disabled={!this.state.downloaded && !this.state.copied}
onPrimaryButtonClick={this._onShowKeyContinueClick} return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t(
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
"This should be different to your account password:",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this._onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
autoFocus={true}
label={_td("Enter a recovery passphrase")}
labelEnterPassword={_td("Enter a recovery passphrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
/>
</div>
<LabelledToggleSwitch
label={ _t("Back up encrypted message keys")}
onChange={this._onUseKeyBackupChange} value={this.state.useKeyBackup}
/>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
hasCancel={false} hasCancel={false}
/>; disabled={!this.state.passPhraseValid}
} else { >
continueButton = <div className="mx_CreateSecretStorageDialog_continueSpinner"> <button type="button"
<InlineSpinner /> onClick={this._onSkipSetupClick}
</div>; className="danger"
>{_t("Skip")}</button>
</DialogButtons>
<details>
<summary>{_t("Advanced")}</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")}
</AccessibleButton>
</details>
</form>;
}
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Field = sdk.getComponent('views.elements.Field');
let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended
// on this screen, this would make it easy for a malicious person to guess
// your passphrase one letter at a time, but they could get this faster by
// just opening the browser's developer tools and reading it.
// Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint.
matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
} }
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div>
<div>{matchText}</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText}
</AccessibleButton>
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Enter your recovery passphrase a second time to confirm it.",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
type="password"
onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your recovery passphrase")}
autoFocus={true}
autoComplete="new-password"
/>
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
{passPhraseMatch}
</div>
</div>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
>
<button type="button"
onClick={this._onSkipSetupClick}
className="danger"
>{_t("Skip")}</button>
</DialogButtons>
</form>;
}
_renderPhaseShowKey() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div> return <div>
<p>{_t( <p>{_t(
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", "Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your recovery passphrase.",
)}</p>
<p>{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
)}</p> )}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer"> <div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyHeader">
{_t("Your recovery key")}
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer"> <div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey"> <div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code> <code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons"> <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary"
onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING}
>
{_t("Download")}
</AccessibleButton>
<span>{_t("or")}</span>
<AccessibleButton <AccessibleButton
kind='primary' kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn" className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick} onClick={this._onCopyClick}
disabled={this.state.phase === PHASE_STORING}
> >
{this.state.copied ? _t("Copied!") : _t("Copy")} {_t("Copy")}
</AccessibleButton>
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</AccessibleButton> </AccessibleButton>
</div> </div>
</div> </div>
</div> </div>
{continueButton} </div>;
}
_renderPhaseKeepItSafe() {
let introText;
if (this.state.copied) {
introText = _t(
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your recovery key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{introText}
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
</ul>
<DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>; </div>;
} }
@ -483,6 +671,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
_renderPhaseLoadError() { _renderPhaseLoadError() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
<p>{_t("Unable to query secret storage status")}</p> <p>{_t("Unable to query secret storage status")}</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
@ -495,44 +684,29 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>; </div>;
} }
_renderPhaseIntro() { _renderPhaseDone() {
let cancelButton; const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
if (this.props.force) {
// if this is a forced key reset then aborting will just leave the old keys
// in place, and is thereforece just 'cancel'
cancelButton = <button type="button" onClick={this._onCancel}>{_t('Cancel')}</button>;
} else {
// if it's setting up from scratch then aborting leaves the user without
// crypto set up, so they skipping the setup.
cancelButton = <button type="button"
className="danger" onClick={this._onSkipSetupClick}
>{_t('Skip')}</button>;
}
return <div> return <div>
<p>{_t( <p>{_t(
"Create a Recovery Key to store encryption keys & secrets with your account data. " + "You can now verify your other devices, " +
"If you lose access to this login youll need it to unlock your data.", "and other users to keep your chats safe.",
)}</p> )}</p>
<div className="mx_Dialog_buttons"> <DialogButtons primaryButton={_t('OK')}
<DialogButtons primaryButton={_t('Continue')} onPrimaryButtonClick={this._onDone}
onPrimaryButtonClick={this._onIntroContinueClick} hasCancel={false}
hasCancel={false} />
>
{cancelButton}
</DialogButtons>
</div>
</div>; </div>;
} }
_renderPhaseSkipConfirm() { _renderPhaseSkipConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
{_t( {_t(
"Without completing security on this session, it wont have " + "Without completing security on this session, it wont have " +
"access to encrypted messages.", "access to encrypted messages.",
)} )}
<DialogButtons primaryButton={_t('Go back')} <DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onGoBackClick} onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false} hasCancel={false}
> >
<button type="button" className="danger" onClick={this._onCancel}>{_t('Skip')}</button> <button type="button" className="danger" onClick={this._onCancel}>{_t('Skip')}</button>
@ -542,15 +716,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_titleForPhase(phase) { _titleForPhase(phase) {
switch (phase) { switch (phase) {
case PHASE_INTRO:
return _t('Create a Recovery Key');
case PHASE_MIGRATE: case PHASE_MIGRATE:
return _t('Upgrade your Recovery Key'); return _t('Upgrade your encryption');
case PHASE_PASSPHRASE:
return _t('Set up encryption');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm recovery passphrase');
case PHASE_CONFIRM_SKIP: case PHASE_CONFIRM_SKIP:
return _t('Are you sure?'); return _t('Are you sure?');
case PHASE_SHOWKEY: case PHASE_SHOWKEY:
case PHASE_KEEPITSAFE:
return _t('Make a copy of your recovery key');
case PHASE_STORING: case PHASE_STORING:
return _t('Store your Recovery Key'); return _t('Setting up keys');
case PHASE_DONE:
return _t("You're done!");
default: default:
return ''; return '';
} }
@ -561,6 +741,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let content; let content;
if (this.state.error) { if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div> content = <div>
<p>{_t("Unable to set up secret storage")}</p> <p>{_t("Unable to set up secret storage")}</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
@ -579,16 +760,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_LOADERROR: case PHASE_LOADERROR:
content = this._renderPhaseLoadError(); content = this._renderPhaseLoadError();
break; break;
case PHASE_INTRO:
content = this._renderPhaseIntro();
break;
case PHASE_MIGRATE: case PHASE_MIGRATE:
content = this._renderPhaseMigrate(); content = this._renderPhaseMigrate();
break; break;
case PHASE_PASSPHRASE:
content = this._renderPhasePassPhrase();
break;
case PHASE_PASSPHRASE_CONFIRM:
content = this._renderPhasePassPhraseConfirm();
break;
case PHASE_SHOWKEY: case PHASE_SHOWKEY:
case PHASE_STORING:
content = this._renderPhaseShowKey(); content = this._renderPhaseShowKey();
break; break;
case PHASE_KEEPITSAFE:
content = this._renderPhaseKeepItSafe();
break;
case PHASE_STORING:
content = this._renderBusyPhase();
break;
case PHASE_DONE:
content = this._renderPhaseDone();
break;
case PHASE_CONFIRM_SKIP: case PHASE_CONFIRM_SKIP:
content = this._renderPhaseSkipConfirm(); content = this._renderPhaseSkipConfirm();
break; break;
@ -605,7 +797,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)} title={this._titleForPhase(this.state.phase)}
headerImage={headerImage} headerImage={headerImage}
hasCancel={this.props.hasCancel} hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
fixedWidth={false} fixedWidth={false}
> >
<div> <div>

View file

@ -108,6 +108,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist2_headerContainer_sticky");
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `${headerStickyWidth}px`; header.style.width = `${headerStickyWidth}px`;
header.style.top = `unset`;
gotBottom = true; gotBottom = true;
} else if (slRect.top < top) { } else if (slRect.top < top) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist2_headerContainer_sticky");
@ -119,6 +120,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop"); header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom"); header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `unset`; header.style.width = `unset`;
header.style.top = `unset`;
} }
} }
}; };

View file

@ -21,7 +21,6 @@ import * as sdk from '../../../index';
import { import {
SetupEncryptionStore, SetupEncryptionStore,
PHASE_INTRO, PHASE_INTRO,
PHASE_RECOVERY_KEY,
PHASE_BUSY, PHASE_BUSY,
PHASE_DONE, PHASE_DONE,
PHASE_CONFIRM_SKIP, PHASE_CONFIRM_SKIP,
@ -62,9 +61,6 @@ export default class CompleteSecurity extends React.Component {
if (phase === PHASE_INTRO) { if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />; icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login"); title = _t("Verify this login");
} else if (phase === PHASE_RECOVERY_KEY) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Recovery Key");
} else if (phase === PHASE_DONE) { } else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />; icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified"); title = _t("Session verified");

View file

@ -19,26 +19,15 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import withValidation from '../../views/elements/Validation';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { import {
SetupEncryptionStore, SetupEncryptionStore,
PHASE_INTRO, PHASE_INTRO,
PHASE_RECOVERY_KEY,
PHASE_BUSY, PHASE_BUSY,
PHASE_DONE, PHASE_DONE,
PHASE_CONFIRM_SKIP, PHASE_CONFIRM_SKIP,
PHASE_FINISHED, PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore'; } from '../../../stores/SetupEncryptionStore';
function keyHasPassphrase(keyInfo) {
return (
keyInfo.passphrase &&
keyInfo.passphrase.salt &&
keyInfo.passphrase.iterations
);
}
export default class SetupEncryptionBody extends React.Component { export default class SetupEncryptionBody extends React.Component {
static propTypes = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
@ -56,11 +45,6 @@ export default class SetupEncryptionBody extends React.Component {
// Because of the latter, it lives in the state. // Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest, verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo, backupInfo: store.backupInfo,
recoveryKey: '',
// whether the recovery key is a valid recovery key
recoveryKeyValid: null,
// whether the recovery key is the correct key or not
recoveryKeyCorrect: null,
}; };
} }
@ -83,19 +67,9 @@ export default class SetupEncryptionBody extends React.Component {
store.stop(); store.stop();
} }
_onResetClick = () => { _onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.startKeyReset(); store.usePassPhrase();
}
_onUseRecoveryKeyClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.useRecoveryKey();
}
_onRecoveryKeyCancelClick() {
const store = SetupEncryptionStore.sharedInstance();
store.cancelUseRecoveryKey();
} }
onSkipClick = () => { onSkipClick = () => {
@ -118,66 +92,6 @@ export default class SetupEncryptionBody extends React.Component {
store.done(); store.done();
} }
_onUsePassphraseClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase();
}
_onRecoveryKeyChange = (e) => {
this.setState({recoveryKey: e.target.value});
}
_onRecoveryKeyValidate = async (fieldState) => {
const result = await this._validateRecoveryKey(fieldState);
this.setState({recoveryKeyValid: result.valid});
return result;
}
_validateRecoveryKey = withValidation({
rules: [
{
key: "required",
test: async (state) => {
try {
const decodedKey = decodeRecoveryKey(state.value);
const correct = await MatrixClientPeg.get().checkSecretStorageKey(
decodedKey, SetupEncryptionStore.sharedInstance().keyInfo,
);
this.setState({
recoveryKeyValid: true,
recoveryKeyCorrect: correct,
});
return correct;
} catch (e) {
this.setState({
recoveryKeyValid: false,
recoveryKeyCorrect: false,
});
return false;
}
},
invalid: function() {
if (this.state.recoveryKeyValid) {
return _t("This isn't the recovery key for your account");
} else {
return _t("This isn't a valid recovery key");
}
},
valid: function() {
return _t("Looks good!");
},
},
],
})
_onRecoveryKeyFormSubmit = (e) => {
e.preventDefault();
if (!this.state.recoveryKeyCorrect) return;
const store = SetupEncryptionStore.sharedInstance();
store.setupWithRecoveryKey(decodeRecoveryKey(this.state.recoveryKey));
}
render() { render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@ -194,13 +108,6 @@ export default class SetupEncryptionBody extends React.Component {
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>; />;
} else if (phase === PHASE_INTRO) { } else if (phase === PHASE_INTRO) {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
} else {
recoveryKeyPrompt = _t("Use Recovery Key");
}
return ( return (
<div> <div>
<p>{_t( <p>{_t(
@ -224,67 +131,15 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
<div className="mx_CompleteSecurity_actionRow"> <div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="link" onClick={this._onUseRecoveryKeyClick}> <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
{recoveryKeyPrompt} {_t("Use Recovery Passphrase or Key")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton kind="danger" onClick={this.onSkipClick}> <AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")} {_t("Skip")}
</AccessibleButton> </AccessibleButton>
</div> </div>
<div className="mx_CompleteSecurity_resetText">{_t(
"If you've forgotten your recovery key you can " +
"<button>set up new recovery options</button>", {}, {
button: sub => <AccessibleButton
element="span" className="mx_linkButton" onClick={this._onResetClick}
>
{sub}
</AccessibleButton>,
},
)}</div>
</div> </div>
); );
} else if (phase === PHASE_RECOVERY_KEY) {
const store = SetupEncryptionStore.sharedInstance();
let keyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
keyPrompt = _t(
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.", {},
{
a: sub => <AccessibleButton
element="span"
className="mx_linkButton"
onClick={this._onUsePassphraseClick}
>{sub}</AccessibleButton>,
},
);
} else {
keyPrompt = _t("Enter your Recovery Key to continue.");
}
const Field = sdk.getComponent('elements.Field');
return <form onSubmit={this._onRecoveryKeyFormSubmit}>
<p>{keyPrompt}</p>
<div className="mx_CompleteSecurity_recoveryKeyEntry">
<Field
type="text"
label={_t('Recovery Key')}
value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange}
onValidate={this._onRecoveryKeyValidate}
/>
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="secondary" onClick={this._onRecoveryKeyCancelClick}>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton kind="primary"
disabled={!this.state.recoveryKeyCorrect}
onClick={this._onRecoveryKeyFormSubmit}
>
{_t("Continue")}
</AccessibleButton>
</div>
</form>;
} else if (phase === PHASE_DONE) { } else if (phase === PHASE_DONE) {
let message; let message;
if (this.state.backupInfo) { if (this.state.backupInfo) {

View file

@ -88,7 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
_onResetRecoveryClick = () => { _onResetRecoveryClick = () => {
this.props.onFinished(false); this.props.onFinished(false);
accessSecretStorage(() => {}, {forceReset: true}); accessSecretStorage(() => {}, /* forceReset = */ true);
} }
_onRecoveryKeyChange = (e) => { _onRecoveryKeyChange = (e) => {

View file

@ -32,9 +32,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
keyInfo: PropTypes.object.isRequired, keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean // Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired, checkPrivateKey: PropTypes.func.isRequired,
// If true, only prompt for a passphrase and do not offer to restore with
// a recovery key or reset keys.
passphraseOnly: PropTypes.bool,
} }
constructor(props) { constructor(props) {
@ -61,7 +58,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
_onResetRecoveryClick = () => { _onResetRecoveryClick = () => {
// Re-enter the access flow, but resetting storage this time around. // Re-enter the access flow, but resetting storage this time around.
this.props.onFinished(false); this.props.onFinished(false);
accessSecretStorage(() => {}, {forceReset: true}); accessSecretStorage(() => {}, /* forceReset = */ true);
} }
_onRecoveryKeyChange = (e) => { _onRecoveryKeyChange = (e) => {
@ -167,7 +164,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.passPhrase.length === 0} primaryDisabled={this.state.passPhrase.length === 0}
/> />
</form> </form>
{this.props.passphraseOnly ? null : _t( {_t(
"If you've forgotten your recovery passphrase you can "+ "If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " + "<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>." "<button2>set up new recovery options</button2>."
@ -237,7 +234,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={!this.state.recoveryKeyValid} primaryDisabled={!this.state.recoveryKeyValid}
/> />
</form> </form>
{this.props.passphraseOnly ? null : _t( {_t(
"If you've forgotten your recovery key you can "+ "If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>." "<button>set up new recovery options</button>."
, {}, { , {}, {

View file

@ -41,6 +41,11 @@ import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorith
* warning disappears. * * warning disappears. *
*******************************************************************/ *******************************************************************/
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
interface IProps { interface IProps {
forRooms: boolean; forRooms: boolean;
rooms?: Room[]; rooms?: Room[];
@ -105,7 +110,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}; };
private onShowAllClick = () => { private onShowAllClick = () => {
this.props.layout.visibleTiles = this.numTiles; this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render this.forceUpdate(); // because the layout doesn't trigger a re-render
}; };
@ -359,7 +364,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{showMoreText} {showMoreText}
</div> </div>
); );
} else if (tiles.length <= nVisible) { } else if (tiles.length <= nVisible && tiles.length > this.props.layout.minVisibleTiles) {
// we have all tiles visible - add a button to show less // we have all tiles visible - add a button to show less
let showLessText = ( let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'> <span className='mx_RoomSublist2_showNButtonText'>
@ -393,18 +398,16 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// goes backwards and can become wildly incorrect (visibleTiles says 18 when there's // goes backwards and can become wildly incorrect (visibleTiles says 18 when there's
// only mathematically 7 possible). // only mathematically 7 possible).
const showMoreHeight = 32; // As defined by CSS
const resizeHandleHeight = 4; // As defined by CSS
// The padding is variable though, so figure out what we need padding for. // The padding is variable though, so figure out what we need padding for.
let padding = 0; let padding = 0;
if (showNButton) padding += showMoreHeight; if (showNButton) padding += SHOW_N_BUTTON_HEIGHT;
if (handles.length > 0) padding += resizeHandleHeight; padding += RESIZE_HANDLE_HEIGHT; // always append the handle height
const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding); const relativeTiles = layout.tilesWithPadding(tiles.length, padding);
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding); const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding);
const tilesWithoutPadding = Math.min(tiles.length, layout.visibleTiles); const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(tiles.length, tilesWithoutPadding, padding); const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
content = ( content = (
<ResizableBox <ResizableBox

View file

@ -113,7 +113,7 @@ export default class CrossSigningPanel extends React.PureComponent {
_bootstrapSecureSecretStorage = async (forceReset=false) => { _bootstrapSecureSecretStorage = async (forceReset=false) => {
this.setState({ error: null }); this.setState({ error: null });
try { try {
await accessSecretStorage(() => undefined, {forceReset}); await accessSecretStorage(() => undefined, forceReset);
} catch (e) { } catch (e) {
this.setState({ error: e }); this.setState({ error: e });
console.error("Error bootstrapping secret storage", e); console.error("Error bootstrapping secret storage", e);

View file

@ -66,7 +66,6 @@ export default class LabsUserSettingsTab extends React.Component {
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} /> <SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} /> <SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} /> <SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"keepSecretStoragePassphraseForSession"} level={SettingLevel.DEVICE} />
</div> </div>
</div> </div>
); );

View file

@ -480,7 +480,6 @@
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)", "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
"Show previews/thumbnails for images": "Show previews/thumbnails for images", "Show previews/thumbnails for images": "Show previews/thumbnails for images",
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms", "Enable message search in encrypted rooms": "Enable message search in encrypted rooms",
"Keep recovery passphrase in memory for this session": "Keep recovery passphrase in memory for this session",
"How fast should messages be downloaded.": "How fast should messages be downloaded.", "How fast should messages be downloaded.": "How fast should messages be downloaded.",
"Manually verify all remote sessions": "Manually verify all remote sessions", "Manually verify all remote sessions": "Manually verify all remote sessions",
"IRC display name width": "IRC display name width", "IRC display name width": "IRC display name width",
@ -2068,7 +2067,6 @@
"Account settings": "Account settings", "Account settings": "Account settings",
"Could not load user profile": "Could not load user profile", "Could not load user profile": "Could not load user profile",
"Verify this login": "Verify this login", "Verify this login": "Verify this login",
"Recovery Key": "Recovery Key",
"Session verified": "Session verified", "Session verified": "Session verified",
"Failed to send email": "Failed to send email", "Failed to send email": "Failed to send email",
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
@ -2122,16 +2120,10 @@
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.", "You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
"Registration Successful": "Registration Successful", "Registration Successful": "Registration Successful",
"Create your account": "Create your account", "Create your account": "Create your account",
"This isn't the recovery key for your account": "This isn't the recovery key for your account",
"This isn't a valid recovery key": "This isn't a valid recovery key",
"Looks good!": "Looks good!",
"Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
"Use Recovery Key": "Use Recovery Key",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
"This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:", "This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:",
"or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client", "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.", "Use Recovery Passphrase or Key": "Use Recovery Passphrase or Key",
"Enter your Recovery Key to continue.": "Enter your Recovery Key to continue.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.", "Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.",
@ -2175,43 +2167,47 @@
"Confirm encryption setup": "Confirm encryption setup", "Confirm encryption setup": "Confirm encryption setup",
"Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.",
"Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:",
"Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption",
"Restore": "Restore",
"You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
"Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:",
"Download": "Download",
"Copy": "Copy",
"Unable to query secret storage status": "Unable to query secret storage status",
"Retry": "Retry",
"Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.": "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.",
"Create a Recovery Key": "Create a Recovery Key",
"Upgrade your Recovery Key": "Upgrade your Recovery Key",
"Store your Recovery Key": "Store your Recovery Key",
"Unable to set up secret storage": "Unable to set up secret storage",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Enter a recovery passphrase": "Enter a recovery passphrase", "Enter a recovery passphrase": "Enter a recovery passphrase",
"Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.", "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
"Back up encrypted message keys": "Back up encrypted message keys",
"Set up with a recovery key": "Set up with a recovery key", "Set up with a recovery key": "Set up with a recovery key",
"That matches!": "That matches!", "That matches!": "That matches!",
"Use a different passphrase?": "Use a different passphrase?", "Use a different passphrase?": "Use a different passphrase?",
"That doesn't match.": "That doesn't match.", "That doesn't match.": "That doesn't match.",
"Go back to set it again.": "Go back to set it again.", "Go back to set it again.": "Go back to set it again.",
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.",
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...", "Confirm your recovery passphrase": "Confirm your recovery passphrase",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.",
"Your recovery key": "Your recovery key", "Your recovery key": "Your recovery key",
"Copy": "Copy",
"Download": "Download",
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "Your recovery key has been <b>copied to your clipboard</b>, paste it to:", "Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
"Your recovery key is in your <b>Downloads</b> folder.": "Your recovery key is in your <b>Downloads</b> folder.", "Your recovery key is in your <b>Downloads</b> folder.": "Your recovery key is in your <b>Downloads</b> folder.",
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe", "<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive", "<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage", "<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
"Unable to query secret storage status": "Unable to query secret storage status",
"Retry": "Retry",
"You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.",
"Upgrade your encryption": "Upgrade your encryption",
"Confirm recovery passphrase": "Confirm recovery passphrase",
"Make a copy of your recovery key": "Make a copy of your recovery key",
"You're done!": "You're done!",
"Unable to set up secret storage": "Unable to set up secret storage",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.",
"Set up Secure Message Recovery": "Set up Secure Message Recovery", "Set up Secure Message Recovery": "Set up Secure Message Recovery",
"Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase", "Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase",
"Confirm your recovery passphrase": "Confirm your recovery passphrase",
"Make a copy of your recovery key": "Make a copy of your recovery key",
"Starting backup...": "Starting backup...", "Starting backup...": "Starting backup...",
"Success!": "Success!", "Success!": "Success!",
"Create key backup": "Create key backup", "Create key backup": "Create key backup",

View file

@ -521,11 +521,6 @@ export const SETTINGS = {
displayName: _td("Enable message search in encrypted rooms"), displayName: _td("Enable message search in encrypted rooms"),
default: true, default: true,
}, },
"keepSecretStoragePassphraseForSession": {
supportedLevels: ['device', 'config'],
displayName: _td("Keep recovery passphrase in memory for this session"),
default: false,
},
"crawlerSleepTime": { "crawlerSleepTime": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("How fast should messages be downloaded."), displayName: _td("How fast should messages be downloaded."),

View file

@ -20,11 +20,10 @@ import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManage
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
export const PHASE_INTRO = 0; export const PHASE_INTRO = 0;
export const PHASE_RECOVERY_KEY = 1; export const PHASE_BUSY = 1;
export const PHASE_BUSY = 2; export const PHASE_DONE = 2; //final done stage, but still showing UX
export const PHASE_DONE = 3; //final done stage, but still showing UX export const PHASE_CONFIRM_SKIP = 3;
export const PHASE_CONFIRM_SKIP = 4; export const PHASE_FINISHED = 4; //UX can be closed
export const PHASE_FINISHED = 5; //UX can be closed
export class SetupEncryptionStore extends EventEmitter { export class SetupEncryptionStore extends EventEmitter {
static sharedInstance() { static sharedInstance() {
@ -37,19 +36,11 @@ export class SetupEncryptionStore extends EventEmitter {
return; return;
} }
this._started = true; this._started = true;
this.phase = PHASE_BUSY; this.phase = PHASE_INTRO;
this.verificationRequest = null; this.verificationRequest = null;
this.backupInfo = null; this.backupInfo = null;
// ID of the key that the secrets we want are encrypted with
this.keyId = null;
// Descriptor of the key that the secrets we want are encrypted with
this.keyInfo = null;
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
this.fetchKeyInfo();
} }
stop() { stop() {
@ -66,49 +57,7 @@ export class SetupEncryptionStore extends EventEmitter {
} }
} }
async fetchKeyInfo() {
const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false);
if (Object.keys(keys).length === 0) {
this.keyId = null;
this.keyInfo = null;
} else {
// If the secret is stored under more than one key, we just pick an arbitrary one
this.keyId = Object.keys(keys)[0];
this.keyInfo = keys[this.keyId];
}
this.phase = PHASE_INTRO;
this.emit("update");
}
async startKeyReset() {
try {
await accessSecretStorage(() => {}, {forceReset: true});
// If the keys are reset, the trust status event will fire and we'll change state
} catch (e) {
// dialog was cancelled - stay on the current screen
}
}
async useRecoveryKey() {
this.phase = PHASE_RECOVERY_KEY;
this.emit("update");
}
cancelUseRecoveryKey() {
this.phase = PHASE_INTRO;
this.emit("update");
}
async setupWithRecoveryKey(recoveryKey) {
this.startTrustCheck({[this.keyId]: recoveryKey});
}
async usePassPhrase() { async usePassPhrase() {
this.startTrustCheck();
}
async startTrustCheck(withKeys) {
this.phase = PHASE_BUSY; this.phase = PHASE_BUSY;
this.emit("update"); this.emit("update");
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -135,9 +84,6 @@ export class SetupEncryptionStore extends EventEmitter {
// to advance before this. // to advance before this.
await cli.restoreKeyBackupWithSecretStorage(backupInfo); await cli.restoreKeyBackupWithSecretStorage(backupInfo);
} }
}, {
withKeys,
passphraseOnly: true,
}).catch(reject); }).catch(reject);
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View file

@ -92,8 +92,12 @@ export class ListLayout {
return this.tilesToPixels(Math.min(maxTiles, n)) + padding; return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
} }
public tilesToPixelsWithPadding(n: number, padding: number): number { public tilesWithPadding(n: number, paddingPx: number): number {
return this.tilesToPixels(n) + padding; return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
}
public tilesToPixelsWithPadding(n: number, paddingPx: number): number {
return this.tilesToPixels(n) + paddingPx;
} }
public tilesToPixels(n: number): number { public tilesToPixels(n: number): number {

View file

@ -508,16 +508,14 @@ export class Algorithm extends EventEmitter {
return true; return true;
} }
if (cause === RoomUpdateCause.NewRoom) { // If the update is for a room change which might be the sticky room, prevent it. We
// TODO: Be smarter and insert rather than regen the planet. // need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though
await this.setKnownRooms([room, ...this.rooms]); // as the sticky room relies on this.
return true; if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
} if (this.stickyRoom === room) {
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
if (cause === RoomUpdateCause.RoomRemoved) { return false;
// TODO: Be smarter and splice rather than regen the planet. }
await this.setKnownRooms(this.rooms.filter(r => r !== room));
return true;
} }
let tags = this.roomIdsToTags[room.roomId]; let tags = this.roomIdsToTags[room.roomId];

View file

@ -87,7 +87,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm); super(tagId, initialSortingAlgorithm);
console.log("Constructed an ImportanceAlgorithm"); console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`);
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
@ -151,8 +151,36 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
} }
} }
private async handleSplice(room: Room, cause: RoomUpdateCause): Promise<boolean> {
if (cause === RoomUpdateCause.NewRoom) {
const category = this.getRoomCategory(room);
this.alterCategoryPositionBy(category, 1, this.indices);
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
} else if (cause === RoomUpdateCause.RoomRemoved) {
const roomIdx = this.getRoomIndex(room);
if (roomIdx === -1) return false; // no change
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
this.alterCategoryPositionBy(oldCategory, -1, this.indices);
this.cachedOrderedRooms.splice(roomIdx, 1); // remove the room
} else {
throw new Error(`Unhandled splice: ${cause}`);
}
}
private getRoomIndex(room: Room): number {
let roomIdx = this.cachedOrderedRooms.indexOf(room);
if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways.
console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`);
roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId);
}
return roomIdx;
}
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> { public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
// TODO: Handle NewRoom and RoomRemoved if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
return this.handleSplice(room, cause);
}
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
throw new Error(`Unsupported update cause: ${cause}`); throw new Error(`Unsupported update cause: ${cause}`);
} }
@ -162,11 +190,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
return; // Nothing to do here. return; // Nothing to do here.
} }
let roomIdx = this.cachedOrderedRooms.indexOf(room); const roomIdx = this.getRoomIndex(room);
if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways.
console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`);
roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId);
}
if (roomIdx === -1) { if (roomIdx === -1) {
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`); throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
} }
@ -188,12 +212,18 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
// room from the array. // room from the array.
} }
// The room received an update, so take out the slice and sort it. This should be relatively // Sort the category now that we've dumped the room in
// quick because the room is inserted at the top of the category, and most popular sorting await this.sortCategory(category);
// algorithms will deal with trying to keep the active room at the top/start of the category.
// For the few algorithms that will have to move the thing quite far (alphabetic with a Z room return true; // change made
// for example), the list should already be sorted well enough that it can rip through the }
// array and slot the changed room in quickly.
private async sortCategory(category: Category) {
// This should be relatively quick because the room is usually inserted at the top of the
// category, and most popular sorting algorithms will deal with trying to keep the active
// room at the top/start of the category. For the few algorithms that will have to move the
// thing quite far (alphabetic with a Z room for example), the list should already be sorted
// well enough that it can rip through the array and slot the changed room in quickly.
const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1]
? Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER
: this.indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; : this.indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]];
@ -202,8 +232,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort); const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort);
const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm); const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm);
this.cachedOrderedRooms.splice(startIdx, 0, ...sorted); this.cachedOrderedRooms.splice(startIdx, 0, ...sorted);
return true; // change made
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
@ -230,14 +258,29 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
// We also need to update subsequent categories as they'll all shift by nRooms, so we // We also need to update subsequent categories as they'll all shift by nRooms, so we
// loop over the order to achieve that. // loop over the order to achieve that.
for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) { this.alterCategoryPositionBy(fromCategory, -nRooms, indices);
const nextCategory = CATEGORY_ORDER[i]; this.alterCategoryPositionBy(toCategory, +nRooms, indices);
indices[nextCategory] -= nRooms; }
}
for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) { private alterCategoryPositionBy(category: Category, n: number, indices: ICategoryIndex) {
const nextCategory = CATEGORY_ORDER[i]; // Note: when we alter a category's index, we actually have to modify the ones following
indices[nextCategory] += nRooms; // the target and not the target itself.
// XXX: If this ever actually gets more than one room passed to it, it'll need more index
// handling. For instance, if 45 rooms are removed from the middle of a 50 room list, the
// index for the categories will be way off.
const nextOrderIndex = CATEGORY_ORDER.indexOf(category) + 1
if (n > 0) {
for (let i = nextOrderIndex; i < CATEGORY_ORDER.length; i++) {
const nextCategory = CATEGORY_ORDER[i];
indices[nextCategory] += Math.abs(n);
}
} else if (n < 0) {
for (let i = nextOrderIndex; i < CATEGORY_ORDER.length; i++) {
const nextCategory = CATEGORY_ORDER[i];
indices[nextCategory] -= Math.abs(n);
}
} }
// Do a quick check to see if we've completely broken the index // Do a quick check to see if we've completely broken the index

View file

@ -28,7 +28,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm); super(tagId, initialSortingAlgorithm);
console.log("Constructed a NaturalAlgorithm"); console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`);
} }
public async setRooms(rooms: Room[]): Promise<any> { public async setRooms(rooms: Room[]): Promise<any> {
@ -36,11 +36,19 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
} }
public async handleRoomUpdate(room, cause): Promise<boolean> { public async handleRoomUpdate(room, cause): Promise<boolean> {
// TODO: Handle NewRoom and RoomRemoved const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
if (!isSplice && !isInPlace) {
throw new Error(`Unsupported update cause: ${cause}`); throw new Error(`Unsupported update cause: ${cause}`);
} }
if (cause === RoomUpdateCause.NewRoom) {
this.cachedOrderedRooms.push(room);
} else if (cause === RoomUpdateCause.RoomRemoved) {
const idx = this.cachedOrderedRooms.indexOf(room);
if (idx >= 0) this.cachedOrderedRooms.splice(idx, 1);
}
// TODO: Optimize this to avoid useless operations // TODO: Optimize this to avoid useless operations
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);

View file

@ -67,6 +67,5 @@ export abstract class OrderingAlgorithm {
* @param cause The cause of the update. * @param cause The cause of the update.
* @returns True if the update requires the Algorithm to update the presentation layers. * @returns True if the update requires the Algorithm to update the presentation layers.
*/ */
// XXX: TODO: We assume this will only ever be a position update and NOT a NewRoom or RemoveRoom change!!
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean>; public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean>;
} }

View file

@ -79,7 +79,20 @@ module.exports = async function signup(session, username, password, homeserver)
const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit');
await acceptButton.click(); await acceptButton.click();
const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); //plow through cross-signing setup by entering arbitrary details
//TODO: It's probably important for the tests to know the passphrase
const xsigningPassphrase = 'a7eaXcjpa9!Yl7#V^h$B^%dovHUVX'; // https://xkcd.com/221/
let passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input');
await session.replaceInputText(passphraseField, xsigningPassphrase);
await session.delay(1000); // give it a second to analyze our passphrase for security
let xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary');
await xsignContButton.click();
//repeat passphrase entry
passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input');
await session.replaceInputText(passphraseField, xsigningPassphrase);
await session.delay(1000); // give it a second to analyze our passphrase for security
xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary');
await xsignContButton.click(); await xsignContButton.click();
//ignore the recovery key //ignore the recovery key
@ -88,11 +101,13 @@ module.exports = async function signup(session, username, password, homeserver)
await copyButton.click(); await copyButton.click();
//acknowledge that we copied the recovery key to a safe place //acknowledge that we copied the recovery key to a safe place
const copyContinueButton = await session.query( const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary');
'.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary',
);
await copyContinueButton.click(); await copyContinueButton.click();
//acknowledge that we're done cross-signing setup and our keys are safe
const doneOkButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary');
await doneOkButton.click();
//wait for registration to finish so the hash gets set //wait for registration to finish so the hash gets set
//onhashchange better? //onhashchange better?