Convert inputs on Export/Import Room Key dialogs to be real Fields (#9350)

* Convert inputs on Export/Import Room Key dialogs to be real Fields

Fixes https://github.com/vector-im/element-web/issues/18517

* Correctly label the second field

* Appease the linter
This commit is contained in:
Travis Ralston 2022-10-05 01:28:57 -04:00 committed by GitHub
parent 99488b84ec
commit 1032334b20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 51 deletions

View file

@ -49,3 +49,8 @@ export type KeysWithObjectShape<Input> = {
? (Input[P] extends Array<unknown> ? never : P) ? (Input[P] extends Array<unknown> ? never : P)
: never; : never;
}[keyof Input]; }[keyof Input];
export type KeysStartingWith<Input extends object, Str extends string> = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X
}[keyof Input];

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,7 +16,7 @@ limitations under the License.
*/ */
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import React, { createRef } from 'react'; import React from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -23,6 +24,8 @@ import { _t } from '../../../../languageHandler';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Field from "../../../../components/views/elements/Field";
import { KeysStartingWith } from "../../../../@types/common";
enum Phase { enum Phase {
Edit = "edit", Edit = "edit",
@ -36,12 +39,14 @@ interface IProps extends IDialogProps {
interface IState { interface IState {
phase: Phase; phase: Phase;
errStr: string; errStr: string;
passphrase1: string;
passphrase2: string;
} }
type AnyPassphrase = KeysStartingWith<IState, "passphrase">;
export default class ExportE2eKeysDialog extends React.Component<IProps, IState> { export default class ExportE2eKeysDialog extends React.Component<IProps, IState> {
private unmounted = false; private unmounted = false;
private passphrase1 = createRef<HTMLInputElement>();
private passphrase2 = createRef<HTMLInputElement>();
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -49,6 +54,8 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
this.state = { this.state = {
phase: Phase.Edit, phase: Phase.Edit,
errStr: null, errStr: null,
passphrase1: "",
passphrase2: "",
}; };
} }
@ -59,8 +66,8 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => { private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => {
ev.preventDefault(); ev.preventDefault();
const passphrase = this.passphrase1.current.value; const passphrase = this.state.passphrase1;
if (passphrase !== this.passphrase2.current.value) { if (passphrase !== this.state.passphrase2) {
this.setState({ errStr: _t('Passphrases must match') }); this.setState({ errStr: _t('Passphrases must match') });
return false; return false;
} }
@ -112,6 +119,12 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
return false; return false;
}; };
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>, phrase: AnyPassphrase) => {
this.setState({
[phrase]: ev.target.value,
} as Pick<IState, AnyPassphrase>);
};
public render(): JSX.Element { public render(): JSX.Element {
const disableForm = (this.state.phase === Phase.Exporting); const disableForm = (this.state.phase === Phase.Exporting);
@ -146,39 +159,28 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
</div> </div>
<div className='mx_E2eKeysDialog_inputTable'> <div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <Field
<label htmlFor='passphrase1'> label={_t("Enter passphrase")}
{ _t("Enter passphrase") } value={this.state.passphrase1}
</label> onChange={e => this.onPassphraseChange(e, "passphrase1")}
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this.passphrase1}
id='passphrase1'
autoFocus={true} autoFocus={true}
size={64} size={64}
type='password' type="password"
disabled={disableForm} disabled={disableForm}
/> />
</div> </div>
</div>
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <Field
<label htmlFor='passphrase2'> label={_t("Confirm passphrase")}
{ _t("Confirm passphrase") } value={this.state.passphrase2}
</label> onChange={e => this.onPassphraseChange(e, "passphrase2")}
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref={this.passphrase2}
id='passphrase2'
size={64} size={64}
type='password' type="password"
disabled={disableForm} disabled={disableForm}
/> />
</div> </div>
</div> </div>
</div> </div>
</div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input <input
className='mx_Dialog_primary' className='mx_Dialog_primary'

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -22,6 +23,7 @@ import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryptio
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Field from "../../../../components/views/elements/Field";
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> { function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -48,12 +50,12 @@ interface IState {
enableSubmit: boolean; enableSubmit: boolean;
phase: Phase; phase: Phase;
errStr: string; errStr: string;
passphrase: string;
} }
export default class ImportE2eKeysDialog extends React.Component<IProps, IState> { export default class ImportE2eKeysDialog extends React.Component<IProps, IState> {
private unmounted = false; private unmounted = false;
private file = createRef<HTMLInputElement>(); private file = createRef<HTMLInputElement>();
private passphrase = createRef<HTMLInputElement>();
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -62,6 +64,7 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
enableSubmit: false, enableSubmit: false,
phase: Phase.Edit, phase: Phase.Edit,
errStr: null, errStr: null,
passphrase: "",
}; };
} }
@ -69,16 +72,22 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
this.unmounted = true; this.unmounted = true;
} }
private onFormChange = (ev: React.FormEvent): void => { private onFormChange = (): void => {
const files = this.file.current.files || []; const files = this.file.current.files || [];
this.setState({ this.setState({
enableSubmit: (this.passphrase.current.value !== "" && files.length > 0), enableSubmit: (this.state.passphrase !== "" && files.length > 0),
}); });
}; };
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ passphrase: ev.target.value });
this.onFormChange(); // update general form state too
};
private onFormSubmit = (ev: React.FormEvent): boolean => { private onFormSubmit = (ev: React.FormEvent): boolean => {
ev.preventDefault(); ev.preventDefault();
this.startImport(this.file.current.files[0], this.passphrase.current.value); // noinspection JSIgnoredPromiseFromCall
this.startImport(this.file.current.files[0], this.state.passphrase);
return false; return false;
}; };
@ -161,20 +170,14 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
</div> </div>
</div> </div>
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <Field
<label htmlFor='passphrase'> label={_t("Enter passphrase")}
{ _t("Enter passphrase") } value={this.state.passphrase}
</label> onChange={this.onPassphraseChange}
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this.passphrase}
id='passphrase'
size={64} size={64}
type='password' type="password"
onChange={this.onFormChange} disabled={disableForm}
disabled={disableForm} /> />
</div>
</div> </div>
</div> </div>
</div> </div>