Merge pull request #6843 from SimonBrandner/task/settings-ts

Convert `/src/components/views/settings/` to TS
This commit is contained in:
Travis Ralston 2021-09-22 00:16:59 -06:00 committed by GitHub
commit f02d6e8240
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 557 additions and 459 deletions

View file

@ -33,6 +33,7 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
import { UIFeature } from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
export enum UserTab { export enum UserTab {
General = "USER_GENERAL_TAB", General = "USER_GENERAL_TAB",
@ -47,8 +48,7 @@ export enum UserTab {
Help = "USER_HELP_TAB", Help = "USER_HELP_TAB",
} }
interface IProps { interface IProps extends IDialogProps {
onFinished: (success: boolean) => void;
initialTabId?: string; initialTabId?: string;
} }

View file

@ -17,78 +17,81 @@ limitations under the License.
import Field from "../elements/Field"; import Field from "../elements/Field";
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner'; import Spinner from '../elements/Spinner';
import withValidation from '../elements/Validation'; import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from "../../../index";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import PassphraseField from "../auth/PassphraseField"; import PassphraseField from "../auth/PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm'; import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
import { MatrixClient } from "matrix-js-sdk/src/client";
import SetEmailDialog from "../dialogs/SetEmailDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
const FIELD_OLD_PASSWORD = 'field_old_password'; const FIELD_OLD_PASSWORD = 'field_old_password';
const FIELD_NEW_PASSWORD = 'field_new_password'; const FIELD_NEW_PASSWORD = 'field_new_password';
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm'; const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
enum Phase {
Edit = "edit",
Uploading = "uploading",
Error = "error",
}
interface IProps {
onFinished?: ({ didSetEmail: boolean }?) => void;
onError?: (error: {error: string}) => void;
rowClassName?: string;
buttonClassName?: string;
buttonKind?: string;
buttonLabel?: string;
confirm?: boolean;
// Whether to autoFocus the new password input
autoFocusNewPasswordInput?: boolean;
className?: string;
shouldAskForEmail?: boolean;
}
interface IState {
fieldValid: {};
phase: Phase;
oldPassword: string;
newPassword: string;
newPasswordConfirm: string;
}
@replaceableComponent("views.settings.ChangePassword") @replaceableComponent("views.settings.ChangePassword")
export default class ChangePassword extends React.Component { export default class ChangePassword extends React.Component<IProps, IState> {
static propTypes = { public static defaultProps: Partial<IProps> = {
onFinished: PropTypes.func,
onError: PropTypes.func,
onCheckPassword: PropTypes.func,
rowClassName: PropTypes.string,
buttonClassName: PropTypes.string,
buttonKind: PropTypes.string,
buttonLabel: PropTypes.string,
confirm: PropTypes.bool,
// Whether to autoFocus the new password input
autoFocusNewPasswordInput: PropTypes.bool,
};
static Phases = {
Edit: "edit",
Uploading: "uploading",
Error: "error",
};
static defaultProps = {
onFinished() {}, onFinished() {},
onError() {}, onError() {},
onCheckPassword(oldPass, newPass, confirmPass) {
if (newPass !== confirmPass) {
return {
error: _t("New passwords don't match"),
};
} else if (!newPass || newPass.length === 0) {
return {
error: _t("Passwords can't be empty"),
};
}
},
confirm: true,
}
state = { confirm: true,
fieldValid: {},
phase: ChangePassword.Phases.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
}; };
changePassword(oldPassword, newPassword) { constructor(props: IProps) {
super(props);
this.state = {
fieldValid: {},
phase: Phase.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
};
}
private onChangePassword(oldPassword: string, newPassword: string): void {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!this.props.confirm) { if (!this.props.confirm) {
this._changePassword(cli, oldPassword, newPassword); this.changePassword(cli, oldPassword, newPassword);
return; return;
} }
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Change Password', '', QuestionDialog, { Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
title: _t("Warning!"), title: _t("Warning!"),
description: description:
@ -109,20 +112,20 @@ export default class ChangePassword extends React.Component {
<button <button
key="exportRoomKeys" key="exportRoomKeys"
className="mx_Dialog_primary" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked} onClick={this.onExportE2eKeysClicked}
> >
{ _t('Export E2E room keys') } { _t('Export E2E room keys') }
</button>, </button>,
], ],
onFinished: (confirmed) => { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
this._changePassword(cli, oldPassword, newPassword); this.changePassword(cli, oldPassword, newPassword);
} }
}, },
}); });
} }
_changePassword(cli, oldPassword, newPassword) { private changePassword(cli: MatrixClient, oldPassword: string, newPassword: string): void {
const authDict = { const authDict = {
type: 'm.login.password', type: 'm.login.password',
identifier: { identifier: {
@ -136,12 +139,12 @@ export default class ChangePassword extends React.Component {
}; };
this.setState({ this.setState({
phase: ChangePassword.Phases.Uploading, phase: Phase.Uploading,
}); });
cli.setPassword(authDict, newPassword).then(() => { cli.setPassword(authDict, newPassword).then(() => {
if (this.props.shouldAskForEmail) { if (this.props.shouldAskForEmail) {
return this._optionallySetEmail().then((confirmed) => { return this.optionallySetEmail().then((confirmed) => {
this.props.onFinished({ this.props.onFinished({
didSetEmail: confirmed, didSetEmail: confirmed,
}); });
@ -153,7 +156,7 @@ export default class ChangePassword extends React.Component {
this.props.onError(err); this.props.onError(err);
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
phase: ChangePassword.Phases.Edit, phase: Phase.Edit,
oldPassword: "", oldPassword: "",
newPassword: "", newPassword: "",
newPasswordConfirm: "", newPasswordConfirm: "",
@ -161,16 +164,27 @@ export default class ChangePassword extends React.Component {
}); });
} }
_optionallySetEmail() { private checkPassword(oldPass: string, newPass: string, confirmPass: string): {error: string} {
if (newPass !== confirmPass) {
return {
error: _t("New passwords don't match"),
};
} else if (!newPass || newPass.length === 0) {
return {
error: _t("Passwords can't be empty"),
};
}
}
private optionallySetEmail(): Promise<boolean> {
// Ask for an email otherwise the user has no way to reset their password // Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
title: _t('Do you want to set an email address?'), title: _t('Do you want to set an email address?'),
}); });
return modal.finished.then(([confirmed]) => confirmed); return modal.finished.then(([confirmed]) => confirmed);
} }
_onExportE2eKeysClicked = () => { private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{ {
@ -179,7 +193,7 @@ export default class ChangePassword extends React.Component {
); );
}; };
markFieldValid(fieldID, valid) { private markFieldValid(fieldID: string, valid: boolean): void {
const { fieldValid } = this.state; const { fieldValid } = this.state;
fieldValid[fieldID] = valid; fieldValid[fieldID] = valid;
this.setState({ this.setState({
@ -187,19 +201,19 @@ export default class ChangePassword extends React.Component {
}); });
} }
onChangeOldPassword = (ev) => { private onChangeOldPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
oldPassword: ev.target.value, oldPassword: ev.target.value,
}); });
}; };
onOldPasswordValidate = async fieldState => { private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.validateOldPasswordRules(fieldState); const result = await this.validateOldPasswordRules(fieldState);
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid); this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
return result; return result;
}; };
validateOldPasswordRules = withValidation({ private validateOldPasswordRules = withValidation({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -209,29 +223,29 @@ export default class ChangePassword extends React.Component {
], ],
}); });
onChangeNewPassword = (ev) => { private onChangeNewPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
newPassword: ev.target.value, newPassword: ev.target.value,
}); });
}; };
onNewPasswordValidate = result => { private onNewPasswordValidate = (result: IValidationResult): void => {
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid); this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
}; };
onChangeNewPasswordConfirm = (ev) => { private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
newPasswordConfirm: ev.target.value, newPasswordConfirm: ev.target.value,
}); });
}; };
onNewPasswordConfirmValidate = async fieldState => { private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.validatePasswordConfirmRules(fieldState); const result = await this.validatePasswordConfirmRules(fieldState);
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid); this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
return result; return result;
}; };
validatePasswordConfirmRules = withValidation({ private validatePasswordConfirmRules = withValidation<this>({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -248,7 +262,7 @@ export default class ChangePassword extends React.Component {
], ],
}); });
onClickChange = async (ev) => { private onClickChange = async (ev: React.MouseEvent | React.FormEvent): Promise<void> => {
ev.preventDefault(); ev.preventDefault();
const allFieldsValid = await this.verifyFieldsBeforeSubmit(); const allFieldsValid = await this.verifyFieldsBeforeSubmit();
@ -260,20 +274,20 @@ export default class ChangePassword extends React.Component {
const oldPassword = this.state.oldPassword; const oldPassword = this.state.oldPassword;
const newPassword = this.state.newPassword; const newPassword = this.state.newPassword;
const confirmPassword = this.state.newPasswordConfirm; const confirmPassword = this.state.newPasswordConfirm;
const err = this.props.onCheckPassword( const err = this.checkPassword(
oldPassword, newPassword, confirmPassword, oldPassword, newPassword, confirmPassword,
); );
if (err) { if (err) {
this.props.onError(err); this.props.onError(err);
} else { } else {
this.changePassword(oldPassword, newPassword); this.onChangePassword(oldPassword, newPassword);
} }
}; };
async verifyFieldsBeforeSubmit() { private async verifyFieldsBeforeSubmit(): Promise<boolean> {
// Blur the active element if any, so we first run its blur validation, // Blur the active element if any, so we first run its blur validation,
// which is less strict than the pass we're about to do below for all fields. // which is less strict than the pass we're about to do below for all fields.
const activeElement = document.activeElement; const activeElement = document.activeElement as HTMLElement;
if (activeElement) { if (activeElement) {
activeElement.blur(); activeElement.blur();
} }
@ -300,7 +314,7 @@ export default class ChangePassword extends React.Component {
// Validation and state updates are async, so we need to wait for them to complete // Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve. // first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve)); await new Promise<void>((resolve) => this.setState({}, resolve));
if (this.allFieldsValid()) { if (this.allFieldsValid()) {
return true; return true;
@ -319,7 +333,7 @@ export default class ChangePassword extends React.Component {
return false; return false;
} }
allFieldsValid() { private allFieldsValid(): boolean {
const keys = Object.keys(this.state.fieldValid); const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
if (!this.state.fieldValid[keys[i]]) { if (!this.state.fieldValid[keys[i]]) {
@ -329,7 +343,7 @@ export default class ChangePassword extends React.Component {
return true; return true;
} }
findFirstInvalidField(fieldIDs) { private findFirstInvalidField(fieldIDs: string[]): Field {
for (const fieldID of fieldIDs) { for (const fieldID of fieldIDs) {
if (!this.state.fieldValid[fieldID] && this[fieldID]) { if (!this.state.fieldValid[fieldID] && this[fieldID]) {
return this[fieldID]; return this[fieldID];
@ -338,12 +352,12 @@ export default class ChangePassword extends React.Component {
return null; return null;
} }
render() { public render(): JSX.Element {
const rowClassName = this.props.rowClassName; const rowClassName = this.props.rowClassName;
const buttonClassName = this.props.buttonClassName; const buttonClassName = this.props.buttonClassName;
switch (this.state.phase) { switch (this.state.phase) {
case ChangePassword.Phases.Edit: case Phase.Edit:
return ( return (
<form className={this.props.className} onSubmit={this.onClickChange}> <form className={this.props.className} onSubmit={this.onClickChange}>
<div className={rowClassName}> <div className={rowClassName}>
@ -385,7 +399,7 @@ export default class ChangePassword extends React.Component {
</AccessibleButton> </AccessibleButton>
</form> </form>
); );
case ChangePassword.Phases.Uploading: case Phase.Uploading:
return ( return (
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<Spinner /> <Spinner />

View file

@ -27,15 +27,31 @@ import QuestionDialog from '../dialogs/QuestionDialog';
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog'; import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
import { accessSecretStorage } from '../../../SecurityManager'; import { accessSecretStorage } from '../../../SecurityManager';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
interface IState {
loading: boolean;
error: null;
backupKeyStored: boolean;
backupKeyCached: boolean;
backupKeyWellFormed: boolean;
secretStorageKeyInAccount: boolean;
secretStorageReady: boolean;
backupInfo: IKeyBackupInfo;
backupSigStatus: TrustInfo;
sessionsRemaining: number;
}
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@replaceableComponent("views.settings.SecureBackupPanel") @replaceableComponent("views.settings.SecureBackupPanel")
export default class SecureBackupPanel extends React.PureComponent { export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
constructor(props) { private unmounted = false;
constructor(props: {}) {
super(props); super(props);
this._unmounted = false;
this.state = { this.state = {
loading: true, loading: true,
error: null, error: null,
@ -50,42 +66,42 @@ export default class SecureBackupPanel extends React.PureComponent {
}; };
} }
componentDidMount() { public componentDidMount(): void {
this._checkKeyBackupStatus(); this.checkKeyBackupStatus();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus); MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatus);
MatrixClientPeg.get().on( MatrixClientPeg.get().on(
'crypto.keyBackupSessionsRemaining', 'crypto.keyBackupSessionsRemaining',
this._onKeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining,
); );
} }
componentWillUnmount() { public componentWillUnmount(): void {
this._unmounted = true; this.unmounted = true;
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus); MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatus);
MatrixClientPeg.get().removeListener( MatrixClientPeg.get().removeListener(
'crypto.keyBackupSessionsRemaining', 'crypto.keyBackupSessionsRemaining',
this._onKeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining,
); );
} }
} }
_onKeyBackupSessionsRemaining = (sessionsRemaining) => { private onKeyBackupSessionsRemaining = (sessionsRemaining: number): void => {
this.setState({ this.setState({
sessionsRemaining, sessionsRemaining,
}); });
} };
_onKeyBackupStatus = () => { private onKeyBackupStatus = (): void => {
// This just loads the current backup status rather than forcing // This just loads the current backup status rather than forcing
// a re-check otherwise we risk causing infinite loops // a re-check otherwise we risk causing infinite loops
this._loadBackupStatus(); this.loadBackupStatus();
} };
async _checkKeyBackupStatus() { private async checkKeyBackupStatus(): Promise<void> {
this._getUpdatedDiagnostics(); this.getUpdatedDiagnostics();
try { try {
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup(); const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
this.setState({ this.setState({
@ -96,7 +112,7 @@ export default class SecureBackupPanel extends React.PureComponent {
}); });
} catch (e) { } catch (e) {
logger.log("Unable to fetch check backup status", e); logger.log("Unable to fetch check backup status", e);
if (this._unmounted) return; if (this.unmounted) return;
this.setState({ this.setState({
loading: false, loading: false,
error: e, error: e,
@ -106,13 +122,13 @@ export default class SecureBackupPanel extends React.PureComponent {
} }
} }
async _loadBackupStatus() { private async loadBackupStatus(): Promise<void> {
this.setState({ loading: true }); this.setState({ loading: true });
this._getUpdatedDiagnostics(); this.getUpdatedDiagnostics();
try { try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
if (this._unmounted) return; if (this.unmounted) return;
this.setState({ this.setState({
loading: false, loading: false,
error: null, error: null,
@ -121,7 +137,7 @@ export default class SecureBackupPanel extends React.PureComponent {
}); });
} catch (e) { } catch (e) {
logger.log("Unable to fetch key backup status", e); logger.log("Unable to fetch key backup status", e);
if (this._unmounted) return; if (this.unmounted) return;
this.setState({ this.setState({
loading: false, loading: false,
error: e, error: e,
@ -131,7 +147,7 @@ export default class SecureBackupPanel extends React.PureComponent {
} }
} }
async _getUpdatedDiagnostics() { private async getUpdatedDiagnostics(): Promise<void> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const secretStorage = cli.crypto.secretStorage; const secretStorage = cli.crypto.secretStorage;
@ -142,7 +158,7 @@ export default class SecureBackupPanel extends React.PureComponent {
const secretStorageKeyInAccount = await secretStorage.hasKey(); const secretStorageKeyInAccount = await secretStorage.hasKey();
const secretStorageReady = await cli.isSecretStorageReady(); const secretStorageReady = await cli.isSecretStorageReady();
if (this._unmounted) return; if (this.unmounted) return;
this.setState({ this.setState({
backupKeyStored, backupKeyStored,
backupKeyCached, backupKeyCached,
@ -152,18 +168,18 @@ export default class SecureBackupPanel extends React.PureComponent {
}); });
} }
_startNewBackup = () => { private startNewBackup = (): void => {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'), import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
{ {
onFinished: () => { onFinished: () => {
this._loadBackupStatus(); this.loadBackupStatus();
}, },
}, null, /* priority = */ false, /* static = */ true, }, null, /* priority = */ false, /* static = */ true,
); );
} };
_deleteBackup = () => { private deleteBackup = (): void => {
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
title: _t('Delete Backup'), title: _t('Delete Backup'),
description: _t( description: _t(
@ -176,33 +192,33 @@ export default class SecureBackupPanel extends React.PureComponent {
if (!proceed) return; if (!proceed) return;
this.setState({ loading: true }); this.setState({ loading: true });
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => { MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
this._loadBackupStatus(); this.loadBackupStatus();
}); });
}, },
}); });
} };
_restoreBackup = async () => { private restoreBackup = async (): Promise<void> => {
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null, 'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true, /* priority = */ false, /* static = */ true,
); );
} };
_resetSecretStorage = async () => { private resetSecretStorage = async (): Promise<void> => {
this.setState({ error: null }); this.setState({ error: null });
try { try {
await accessSecretStorage(() => { }, /* forceReset = */ true); await accessSecretStorage(async () => { }, /* forceReset = */ true);
} catch (e) { } catch (e) {
console.error("Error resetting secret storage", e); console.error("Error resetting secret storage", e);
if (this._unmounted) return; if (this.unmounted) return;
this.setState({ error: e }); this.setState({ error: e });
} }
if (this._unmounted) return; if (this.unmounted) return;
this._loadBackupStatus(); this.loadBackupStatus();
} };
render() { public render(): JSX.Element {
const { const {
loading, loading,
error, error,
@ -263,7 +279,7 @@ export default class SecureBackupPanel extends React.PureComponent {
</div>; </div>;
} }
let backupSigStatuses = backupSigStatus.sigs.map((sig, i) => { let backupSigStatuses: React.ReactNode = backupSigStatus.sigs.map((sig, i) => {
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null; const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
const validity = sub => const validity = sub =>
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}> <span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
@ -371,14 +387,14 @@ export default class SecureBackupPanel extends React.PureComponent {
</>; </>;
actions.push( actions.push(
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}> <AccessibleButton key="restore" kind="primary" onClick={this.restoreBackup}>
{ restoreButtonCaption } { restoreButtonCaption }
</AccessibleButton>, </AccessibleButton>,
); );
if (!isSecureBackupRequired()) { if (!isSecureBackupRequired()) {
actions.push( actions.push(
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}> <AccessibleButton key="delete" kind="danger" onClick={this.deleteBackup}>
{ _t("Delete Backup") } { _t("Delete Backup") }
</AccessibleButton>, </AccessibleButton>,
); );
@ -392,7 +408,7 @@ export default class SecureBackupPanel extends React.PureComponent {
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p> <p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
</>; </>;
actions.push( actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}> <AccessibleButton key="setup" kind="primary" onClick={this.startNewBackup}>
{ _t("Set up") } { _t("Set up") }
</AccessibleButton>, </AccessibleButton>,
); );
@ -400,7 +416,7 @@ export default class SecureBackupPanel extends React.PureComponent {
if (secretStorageKeyInAccount) { if (secretStorageKeyInAccount) {
actions.push( actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}> <AccessibleButton key="reset" kind="danger" onClick={this.resetSecretStorage}>
{ _t("Reset") } { _t("Reset") }
</AccessibleButton>, </AccessibleButton>,
); );

View file

@ -16,16 +16,16 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import Field from "../../elements/Field"; import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../elements/AccessibleButton";
import * as Email from "../../../../email"; import * as Email from "../../../../email";
import AddThreepid from "../../../../AddThreepid"; import AddThreepid from "../../../../AddThreepid";
import * as sdk from '../../../../index';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import ErrorDialog from "../../dialogs/ErrorDialog";
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
/* /*
TODO: Improve the UX for everything in here. TODO: Improve the UX for everything in here.
@ -39,42 +39,45 @@ places to communicate errors - these should be replaced with inline validation w
that is available. that is available.
*/ */
export class ExistingEmailAddress extends React.Component { interface IExistingEmailAddressProps {
static propTypes = { email: IThreepid;
email: PropTypes.object.isRequired, onRemoved: (emails: IThreepid) => void;
onRemoved: PropTypes.func.isRequired, }
};
constructor() { interface IExistingEmailAddressState {
super(); verifyRemove: boolean;
}
export class ExistingEmailAddress extends React.Component<IExistingEmailAddressProps, IExistingEmailAddressState> {
constructor(props: IExistingEmailAddressProps) {
super(props);
this.state = { this.state = {
verifyRemove: false, verifyRemove: false,
}; };
} }
_onRemove = (e) => { private onRemove = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setState({ verifyRemove: true }); this.setState({ verifyRemove: true });
}; };
_onDontRemove = (e) => { private onDontRemove = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setState({ verifyRemove: false }); this.setState({ verifyRemove: false });
}; };
_onActuallyRemove = (e) => { private onActuallyRemove = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => { MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
return this.props.onRemoved(this.props.email); return this.props.onRemoved(this.props.email);
}).catch((err) => { }).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err); console.error("Unable to remove contact information: " + err);
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, { Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"), title: _t("Unable to remove contact information"),
@ -83,7 +86,7 @@ export class ExistingEmailAddress extends React.Component {
}); });
}; };
render() { public render(): JSX.Element {
if (this.state.verifyRemove) { if (this.state.verifyRemove) {
return ( return (
<div className="mx_ExistingEmailAddress"> <div className="mx_ExistingEmailAddress">
@ -91,14 +94,14 @@ export class ExistingEmailAddress extends React.Component {
{ _t("Remove %(email)s?", { email: this.props.email.address } ) } { _t("Remove %(email)s?", { email: this.props.email.address } ) }
</span> </span>
<AccessibleButton <AccessibleButton
onClick={this._onActuallyRemove} onClick={this.onActuallyRemove}
kind="danger_sm" kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn" className="mx_ExistingEmailAddress_confirmBtn"
> >
{ _t("Remove") } { _t("Remove") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton <AccessibleButton
onClick={this._onDontRemove} onClick={this.onDontRemove}
kind="link_sm" kind="link_sm"
className="mx_ExistingEmailAddress_confirmBtn" className="mx_ExistingEmailAddress_confirmBtn"
> >
@ -111,7 +114,7 @@ export class ExistingEmailAddress extends React.Component {
return ( return (
<div className="mx_ExistingEmailAddress"> <div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span> <span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
<AccessibleButton onClick={this._onRemove} kind="danger_sm"> <AccessibleButton onClick={this.onRemove} kind="danger_sm">
{ _t("Remove") } { _t("Remove") }
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -119,14 +122,21 @@ export class ExistingEmailAddress extends React.Component {
} }
} }
@replaceableComponent("views.settings.account.EmailAddresses") interface IProps {
export default class EmailAddresses extends React.Component { emails: IThreepid[];
static propTypes = { onEmailsChange: (emails: Partial<IThreepid>[]) => void;
emails: PropTypes.array.isRequired, }
onEmailsChange: PropTypes.func.isRequired,
}
constructor(props) { interface IState {
verifying: boolean;
addTask: any; // FIXME: When AddThreepid is TSfied
continueDisabled: boolean;
newEmailAddress: string;
}
@replaceableComponent("views.settings.account.EmailAddresses")
export default class EmailAddresses extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
@ -137,24 +147,23 @@ export default class EmailAddresses extends React.Component {
}; };
} }
_onRemoved = (address) => { private onRemoved = (address): void => {
const emails = this.props.emails.filter((e) => e !== address); const emails = this.props.emails.filter((e) => e !== address);
this.props.onEmailsChange(emails); this.props.onEmailsChange(emails);
}; };
_onChangeNewEmailAddress = (e) => { private onChangeNewEmailAddress = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
newEmailAddress: e.target.value, newEmailAddress: e.target.value,
}); });
}; };
_onAddClick = (e) => { private onAddClick = (e: React.FormEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!this.state.newEmailAddress) return; if (!this.state.newEmailAddress) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const email = this.state.newEmailAddress; const email = this.state.newEmailAddress;
// TODO: Inline field validation // TODO: Inline field validation
@ -181,7 +190,7 @@ export default class EmailAddresses extends React.Component {
}); });
}; };
_onContinueClick = (e) => { private onContinueClick = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -192,7 +201,7 @@ export default class EmailAddresses extends React.Component {
const email = this.state.newEmailAddress; const email = this.state.newEmailAddress;
const emails = [ const emails = [
...this.props.emails, ...this.props.emails,
{ address: email, medium: "email" }, { address: email, medium: ThreepidMedium.Email },
]; ];
this.props.onEmailsChange(emails); this.props.onEmailsChange(emails);
newEmailAddress = ""; newEmailAddress = "";
@ -205,7 +214,6 @@ export default class EmailAddresses extends React.Component {
}); });
}).catch((err) => { }).catch((err) => {
this.setState({ continueDisabled: false }); this.setState({ continueDisabled: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
if (err.errcode === 'M_THREEPID_AUTH_FAILED') { if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, { Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
title: _t("Your email address hasn't been verified yet"), title: _t("Your email address hasn't been verified yet"),
@ -222,13 +230,13 @@ export default class EmailAddresses extends React.Component {
}); });
}; };
render() { public render(): JSX.Element {
const existingEmailElements = this.props.emails.map((e) => { const existingEmailElements = this.props.emails.map((e) => {
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />; return <ExistingEmailAddress email={e} onRemoved={this.onRemoved} key={e.address} />;
}); });
let addButton = ( let addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary"> <AccessibleButton onClick={this.onAddClick} kind="primary">
{ _t("Add") } { _t("Add") }
</AccessibleButton> </AccessibleButton>
); );
@ -237,7 +245,7 @@ export default class EmailAddresses extends React.Component {
<div> <div>
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div> <div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
<AccessibleButton <AccessibleButton
onClick={this._onContinueClick} onClick={this.onContinueClick}
kind="primary" kind="primary"
disabled={this.state.continueDisabled} disabled={this.state.continueDisabled}
> >
@ -251,7 +259,7 @@ export default class EmailAddresses extends React.Component {
<div className="mx_EmailAddresses"> <div className="mx_EmailAddresses">
{ existingEmailElements } { existingEmailElements }
<form <form
onSubmit={this._onAddClick} onSubmit={this.onAddClick}
autoComplete="off" autoComplete="off"
noValidate={true} noValidate={true}
className="mx_EmailAddresses_new" className="mx_EmailAddresses_new"
@ -262,7 +270,7 @@ export default class EmailAddresses extends React.Component {
autoComplete="off" autoComplete="off"
disabled={this.state.verifying} disabled={this.state.verifying}
value={this.state.newEmailAddress} value={this.state.newEmailAddress}
onChange={this._onChangeNewEmailAddress} onChange={this.onChangeNewEmailAddress}
/> />
{ addButton } { addButton }
</form> </form>

View file

@ -16,16 +16,17 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import Field from "../../elements/Field"; import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../elements/AccessibleButton";
import AddThreepid from "../../../../AddThreepid"; import AddThreepid from "../../../../AddThreepid";
import CountryDropdown from "../../auth/CountryDropdown"; import CountryDropdown from "../../auth/CountryDropdown";
import * as sdk from '../../../../index';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
import ErrorDialog from "../../dialogs/ErrorDialog";
import { PhoneNumberCountryDefinition } from "../../../../phonenumber";
/* /*
TODO: Improve the UX for everything in here. TODO: Improve the UX for everything in here.
@ -34,42 +35,45 @@ This is a copy/paste of EmailAddresses, mostly.
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic // TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
export class ExistingPhoneNumber extends React.Component { interface IExistingPhoneNumberProps {
static propTypes = { msisdn: IThreepid;
msisdn: PropTypes.object.isRequired, onRemoved: (phoneNumber: IThreepid) => void;
onRemoved: PropTypes.func.isRequired, }
};
constructor() { interface IExistingPhoneNumberState {
super(); verifyRemove: boolean;
}
export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberProps, IExistingPhoneNumberState> {
constructor(props: IExistingPhoneNumberProps) {
super(props);
this.state = { this.state = {
verifyRemove: false, verifyRemove: false,
}; };
} }
_onRemove = (e) => { private onRemove = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setState({ verifyRemove: true }); this.setState({ verifyRemove: true });
}; };
_onDontRemove = (e) => { private onDontRemove = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setState({ verifyRemove: false }); this.setState({ verifyRemove: false });
}; };
_onActuallyRemove = (e) => { private onActuallyRemove = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => { MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
return this.props.onRemoved(this.props.msisdn); return this.props.onRemoved(this.props.msisdn);
}).catch((err) => { }).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err); console.error("Unable to remove contact information: " + err);
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, { Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"), title: _t("Unable to remove contact information"),
@ -78,7 +82,7 @@ export class ExistingPhoneNumber extends React.Component {
}); });
}; };
render() { public render(): JSX.Element {
if (this.state.verifyRemove) { if (this.state.verifyRemove) {
return ( return (
<div className="mx_ExistingPhoneNumber"> <div className="mx_ExistingPhoneNumber">
@ -86,14 +90,14 @@ export class ExistingPhoneNumber extends React.Component {
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) } { _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
</span> </span>
<AccessibleButton <AccessibleButton
onClick={this._onActuallyRemove} onClick={this.onActuallyRemove}
kind="danger_sm" kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn" className="mx_ExistingPhoneNumber_confirmBtn"
> >
{ _t("Remove") } { _t("Remove") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton <AccessibleButton
onClick={this._onDontRemove} onClick={this.onDontRemove}
kind="link_sm" kind="link_sm"
className="mx_ExistingPhoneNumber_confirmBtn" className="mx_ExistingPhoneNumber_confirmBtn"
> >
@ -106,7 +110,7 @@ export class ExistingPhoneNumber extends React.Component {
return ( return (
<div className="mx_ExistingPhoneNumber"> <div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span> <span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
<AccessibleButton onClick={this._onRemove} kind="danger_sm"> <AccessibleButton onClick={this.onRemove} kind="danger_sm">
{ _t("Remove") } { _t("Remove") }
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -114,19 +118,30 @@ export class ExistingPhoneNumber extends React.Component {
} }
} }
@replaceableComponent("views.settings.account.PhoneNumbers") interface IProps {
export default class PhoneNumbers extends React.Component { msisdns: IThreepid[];
static propTypes = { onMsisdnsChange: (phoneNumbers: Partial<IThreepid>[]) => void;
msisdns: PropTypes.array.isRequired, }
onMsisdnsChange: PropTypes.func.isRequired,
}
constructor(props) { interface IState {
verifying: boolean;
verifyError: string;
verifyMsisdn: string;
addTask: any; // FIXME: When AddThreepid is TSfied
continueDisabled: boolean;
phoneCountry: string;
newPhoneNumber: string;
newPhoneNumberCode: string;
}
@replaceableComponent("views.settings.account.PhoneNumbers")
export default class PhoneNumbers extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
verifying: false, verifying: false,
verifyError: false, verifyError: null,
verifyMsisdn: "", verifyMsisdn: "",
addTask: null, addTask: null,
continueDisabled: false, continueDisabled: false,
@ -136,30 +151,29 @@ export default class PhoneNumbers extends React.Component {
}; };
} }
_onRemoved = (address) => { private onRemoved = (address: IThreepid): void => {
const msisdns = this.props.msisdns.filter((e) => e !== address); const msisdns = this.props.msisdns.filter((e) => e !== address);
this.props.onMsisdnsChange(msisdns); this.props.onMsisdnsChange(msisdns);
}; };
_onChangeNewPhoneNumber = (e) => { private onChangeNewPhoneNumber = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
newPhoneNumber: e.target.value, newPhoneNumber: e.target.value,
}); });
}; };
_onChangeNewPhoneNumberCode = (e) => { private onChangeNewPhoneNumberCode = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
newPhoneNumberCode: e.target.value, newPhoneNumberCode: e.target.value,
}); });
}; };
_onAddClick = (e) => { private onAddClick = (e: React.MouseEvent | React.FormEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!this.state.newPhoneNumber) return; if (!this.state.newPhoneNumber) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const phoneNumber = this.state.newPhoneNumber; const phoneNumber = this.state.newPhoneNumber;
const phoneCountry = this.state.phoneCountry; const phoneCountry = this.state.phoneCountry;
@ -178,7 +192,7 @@ export default class PhoneNumbers extends React.Component {
}); });
}; };
_onContinueClick = (e) => { private onContinueClick = (e: React.MouseEvent | React.FormEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -190,7 +204,7 @@ export default class PhoneNumbers extends React.Component {
if (finished) { if (finished) {
const msisdns = [ const msisdns = [
...this.props.msisdns, ...this.props.msisdns,
{ address, medium: "msisdn" }, { address, medium: ThreepidMedium.Phone },
]; ];
this.props.onMsisdnsChange(msisdns); this.props.onMsisdnsChange(msisdns);
newPhoneNumber = ""; newPhoneNumber = "";
@ -207,7 +221,6 @@ export default class PhoneNumbers extends React.Component {
}).catch((err) => { }).catch((err) => {
this.setState({ continueDisabled: false }); this.setState({ continueDisabled: false });
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify phone number: " + err); console.error("Unable to verify phone number: " + err);
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, { Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
title: _t("Unable to verify phone number."), title: _t("Unable to verify phone number."),
@ -219,17 +232,17 @@ export default class PhoneNumbers extends React.Component {
}); });
}; };
_onCountryChanged = (e) => { private onCountryChanged = (country: PhoneNumberCountryDefinition): void => {
this.setState({ phoneCountry: e.iso2 }); this.setState({ phoneCountry: country.iso2 });
}; };
render() { public render(): JSX.Element {
const existingPhoneElements = this.props.msisdns.map((p) => { const existingPhoneElements = this.props.msisdns.map((p) => {
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />; return <ExistingPhoneNumber msisdn={p} onRemoved={this.onRemoved} key={p.address} />;
}); });
let addVerifySection = ( let addVerifySection = (
<AccessibleButton onClick={this._onAddClick} kind="primary"> <AccessibleButton onClick={this.onAddClick} kind="primary">
{ _t("Add") } { _t("Add") }
</AccessibleButton> </AccessibleButton>
); );
@ -243,17 +256,17 @@ export default class PhoneNumbers extends React.Component {
<br /> <br />
{ this.state.verifyError } { this.state.verifyError }
</div> </div>
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}> <form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field <Field
type="text" type="text"
label={_t("Verification code")} label={_t("Verification code")}
autoComplete="off" autoComplete="off"
disabled={this.state.continueDisabled} disabled={this.state.continueDisabled}
value={this.state.newPhoneNumberCode} value={this.state.newPhoneNumberCode}
onChange={this._onChangeNewPhoneNumberCode} onChange={this.onChangeNewPhoneNumberCode}
/> />
<AccessibleButton <AccessibleButton
onClick={this._onContinueClick} onClick={this.onContinueClick}
kind="primary" kind="primary"
disabled={this.state.continueDisabled} disabled={this.state.continueDisabled}
> >
@ -264,7 +277,7 @@ export default class PhoneNumbers extends React.Component {
); );
} }
const phoneCountry = <CountryDropdown onOptionChange={this._onCountryChanged} const phoneCountry = <CountryDropdown onOptionChange={this.onCountryChanged}
className="mx_PhoneNumbers_country" className="mx_PhoneNumbers_country"
value={this.state.phoneCountry} value={this.state.phoneCountry}
disabled={this.state.verifying} disabled={this.state.verifying}
@ -275,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
return ( return (
<div className="mx_PhoneNumbers"> <div className="mx_PhoneNumbers">
{ existingPhoneElements } { existingPhoneElements }
<form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new"> <form onSubmit={this.onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
<div className="mx_PhoneNumbers_input"> <div className="mx_PhoneNumbers_input">
<Field <Field
type="text" type="text"
@ -284,7 +297,7 @@ export default class PhoneNumbers extends React.Component {
disabled={this.state.verifying} disabled={this.state.verifying}
prefixComponent={phoneCountry} prefixComponent={phoneCountry}
value={this.state.newPhoneNumber} value={this.state.newPhoneNumber}
onChange={this._onChangeNewPhoneNumber} onChange={this.onChangeNewPhoneNumber}
/> />
</div> </div>
</form> </form>

View file

@ -16,14 +16,15 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
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 Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid'; import AddThreepid from '../../../../AddThreepid';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
import ErrorDialog from "../../dialogs/ErrorDialog";
import AccessibleButton from "../../elements/AccessibleButton";
/* /*
TODO: Improve the UX for everything in here. TODO: Improve the UX for everything in here.
@ -41,12 +42,19 @@ that is available.
TODO: Reduce all the copying between account vs. discovery components. TODO: Reduce all the copying between account vs. discovery components.
*/ */
export class EmailAddress extends React.Component { interface IEmailAddressProps {
static propTypes = { email: IThreepid;
email: PropTypes.object.isRequired, }
};
constructor(props) { interface IEmailAddressState {
verifying: boolean;
addTask: any; // FIXME: When AddThreepid is TSfied
continueDisabled: boolean;
bound: boolean;
}
export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddressState> {
constructor(props: IEmailAddressProps) {
super(props); super(props);
const { bound } = props.email; const { bound } = props.email;
@ -60,17 +68,17 @@ export class EmailAddress extends React.Component {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
public UNSAFE_componentWillReceiveProps(nextProps: IEmailAddressProps): void {
const { bound } = nextProps.email; const { bound } = nextProps.email;
this.setState({ bound }); this.setState({ bound });
} }
async changeBinding({ bind, label, errorTitle }) { private async changeBinding({ bind, label, errorTitle }): Promise<void> {
if (!(await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind())) { if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
return this.changeBindingTangledAddBind({ bind, label, errorTitle }); return this.changeBindingTangledAddBind({ bind, label, errorTitle });
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.email; const { medium, address } = this.props.email;
try { try {
@ -103,8 +111,7 @@ export class EmailAddress extends React.Component {
} }
} }
async changeBindingTangledAddBind({ bind, label, errorTitle }) { private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.email; const { medium, address } = this.props.email;
const task = new AddThreepid(); const task = new AddThreepid();
@ -139,7 +146,7 @@ export class EmailAddress extends React.Component {
} }
} }
onRevokeClick = (e) => { private onRevokeClick = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.changeBinding({ this.changeBinding({
@ -147,9 +154,9 @@ export class EmailAddress extends React.Component {
label: "revoke", label: "revoke",
errorTitle: _t("Unable to revoke sharing for email address"), errorTitle: _t("Unable to revoke sharing for email address"),
}); });
} };
onShareClick = (e) => { private onShareClick = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.changeBinding({ this.changeBinding({
@ -157,9 +164,9 @@ export class EmailAddress extends React.Component {
label: "share", label: "share",
errorTitle: _t("Unable to share email address"), errorTitle: _t("Unable to share email address"),
}); });
} };
onContinueClick = async (e) => { private onContinueClick = async (e: React.MouseEvent): Promise<void> => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -173,7 +180,6 @@ export class EmailAddress extends React.Component {
}); });
} catch (err) { } catch (err) {
this.setState({ continueDisabled: false }); this.setState({ continueDisabled: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
if (err.errcode === 'M_THREEPID_AUTH_FAILED') { if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, { Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
title: _t("Your email address hasn't been verified yet"), title: _t("Your email address hasn't been verified yet"),
@ -188,10 +194,9 @@ export class EmailAddress extends React.Component {
}); });
} }
} }
} };
render() { public render(): JSX.Element {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const { address } = this.props.email; const { address } = this.props.email;
const { verifying, bound } = this.state; const { verifying, bound } = this.state;
@ -234,14 +239,13 @@ export class EmailAddress extends React.Component {
); );
} }
} }
interface IProps {
emails: IThreepid[];
}
@replaceableComponent("views.settings.discovery.EmailAddresses") @replaceableComponent("views.settings.discovery.EmailAddresses")
export default class EmailAddresses extends React.Component { export default class EmailAddresses extends React.Component<IProps> {
static propTypes = { public render(): JSX.Element {
emails: PropTypes.array.isRequired,
}
render() {
let content; let content;
if (this.props.emails.length > 0) { if (this.props.emails.length > 0) {
content = this.props.emails.map((e) => { content = this.props.emails.map((e) => {

View file

@ -16,14 +16,16 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
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 Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid'; import AddThreepid from '../../../../AddThreepid';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
import ErrorDialog from "../../dialogs/ErrorDialog";
import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
/* /*
TODO: Improve the UX for everything in here. TODO: Improve the UX for everything in here.
@ -32,12 +34,21 @@ This is a copy/paste of EmailAddresses, mostly.
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic // TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
export class PhoneNumber extends React.Component { interface IPhoneNumberProps {
static propTypes = { msisdn: IThreepid;
msisdn: PropTypes.object.isRequired, }
};
constructor(props) { interface IPhoneNumberState {
verifying: boolean;
verificationCode: string;
addTask: any; // FIXME: When AddThreepid is TSfied
continueDisabled: boolean;
bound: boolean;
verifyError: string;
}
export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumberState> {
constructor(props: IPhoneNumberProps) {
super(props); super(props);
const { bound } = props.msisdn; const { bound } = props.msisdn;
@ -48,21 +59,22 @@ export class PhoneNumber extends React.Component {
addTask: null, addTask: null,
continueDisabled: false, continueDisabled: false,
bound, bound,
verifyError: null,
}; };
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
public UNSAFE_componentWillReceiveProps(nextProps: IPhoneNumberProps): void {
const { bound } = nextProps.msisdn; const { bound } = nextProps.msisdn;
this.setState({ bound }); this.setState({ bound });
} }
async changeBinding({ bind, label, errorTitle }) { private async changeBinding({ bind, label, errorTitle }): Promise<void> {
if (!(await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind())) { if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
return this.changeBindingTangledAddBind({ bind, label, errorTitle }); return this.changeBindingTangledAddBind({ bind, label, errorTitle });
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.msisdn; const { medium, address } = this.props.msisdn;
try { try {
@ -99,8 +111,7 @@ export class PhoneNumber extends React.Component {
} }
} }
async changeBindingTangledAddBind({ bind, label, errorTitle }) { private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.msisdn; const { medium, address } = this.props.msisdn;
const task = new AddThreepid(); const task = new AddThreepid();
@ -139,7 +150,7 @@ export class PhoneNumber extends React.Component {
} }
} }
onRevokeClick = (e) => { private onRevokeClick = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.changeBinding({ this.changeBinding({
@ -147,9 +158,9 @@ export class PhoneNumber extends React.Component {
label: "revoke", label: "revoke",
errorTitle: _t("Unable to revoke sharing for phone number"), errorTitle: _t("Unable to revoke sharing for phone number"),
}); });
} };
onShareClick = (e) => { private onShareClick = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.changeBinding({ this.changeBinding({
@ -157,15 +168,15 @@ export class PhoneNumber extends React.Component {
label: "share", label: "share",
errorTitle: _t("Unable to share phone number"), errorTitle: _t("Unable to share phone number"),
}); });
} };
onVerificationCodeChange = (e) => { private onVerificationCodeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
verificationCode: e.target.value, verificationCode: e.target.value,
}); });
} };
onContinueClick = async (e) => { private onContinueClick = async (e: React.MouseEvent | React.FormEvent): Promise<void> => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -183,7 +194,6 @@ export class PhoneNumber extends React.Component {
} catch (err) { } catch (err) {
this.setState({ continueDisabled: false }); this.setState({ continueDisabled: false });
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify phone number: " + err); console.error("Unable to verify phone number: " + err);
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, { Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
title: _t("Unable to verify phone number."), title: _t("Unable to verify phone number."),
@ -193,11 +203,9 @@ export class PhoneNumber extends React.Component {
this.setState({ verifyError: _t("Incorrect verification code") }); this.setState({ verifyError: _t("Incorrect verification code") });
} }
} }
} };
render() { public render(): JSX.Element {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Field = sdk.getComponent('elements.Field');
const { address } = this.props.msisdn; const { address } = this.props.msisdn;
const { verifying, bound } = this.state; const { verifying, bound } = this.state;
@ -247,13 +255,13 @@ export class PhoneNumber extends React.Component {
} }
} }
@replaceableComponent("views.settings.discovery.PhoneNumbers") interface IProps {
export default class PhoneNumbers extends React.Component { msisdns: IThreepid[];
static propTypes = { }
msisdns: PropTypes.array.isRequired,
}
render() { @replaceableComponent("views.settings.discovery.PhoneNumbers")
export default class PhoneNumbers extends React.Component<IProps> {
public render(): JSX.Element {
let content; let content;
if (this.props.msisdns.length > 0) { if (this.props.msisdns.length > 0) {
content = this.props.msisdns.map((e) => { content = this.props.msisdns.map((e) => {

View file

@ -15,45 +15,46 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings"; import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature"; import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
import RelatedGroupSettings from "../../../room_settings/RelatedGroupSettings";
import AliasSettings from "../../../room_settings/AliasSettings";
interface IProps {
roomId: string;
}
interface IState {
isRoomPublished: boolean;
}
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab") @replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
export default class GeneralRoomSettingsTab extends React.Component { export default class GeneralRoomSettingsTab extends React.Component<IProps, IState> {
static propTypes = { public static contextType = MatrixClientContext;
roomId: PropTypes.string.isRequired,
};
static contextType = MatrixClientContext; constructor(props: IProps) {
super(props);
constructor() {
super();
this.state = { this.state = {
isRoomPublished: false, // loaded async isRoomPublished: false, // loaded async
}; };
} }
_onLeaveClick = () => { private onLeaveClick = (): void => {
dis.dispatch({ dis.dispatch({
action: 'leave_room', action: 'leave_room',
room_id: this.props.roomId, room_id: this.props.roomId,
}); });
}; };
render() { public render(): JSX.Element {
const AliasSettings = sdk.getComponent("room_settings.AliasSettings");
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
const client = this.context; const client = this.context;
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
@ -110,7 +111,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span> <span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
<AccessibleButton kind='danger' onClick={this._onLeaveClick}> <AccessibleButton kind='danger' onClick={this.onLeaveClick}>
{ _t('Leave room') } { _t('Leave room') }
</AccessibleButton> </AccessibleButton>
</div> </div>

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
@ -24,16 +23,21 @@ import SettingsStore from '../../../../../settings/SettingsStore';
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
interface IProps {
roomId: string;
}
interface IState {
currentSound: string;
uploadedFile: File;
}
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab") @replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
export default class NotificationsSettingsTab extends React.Component { export default class NotificationsSettingsTab extends React.Component<IProps, IState> {
static propTypes = { private soundUpload = createRef<HTMLInputElement>();
roomId: PropTypes.string.isRequired,
};
_soundUpload = createRef(); constructor(props: IProps) {
super(props);
constructor() {
super();
this.state = { this.state = {
currentSound: "default", currentSound: "default",
@ -42,7 +46,8 @@ export default class NotificationsSettingsTab extends React.Component {
} }
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
public UNSAFE_componentWillMount(): void {
const soundData = Notifier.getSoundForRoom(this.props.roomId); const soundData = Notifier.getSoundForRoom(this.props.roomId);
if (!soundData) { if (!soundData) {
return; return;
@ -50,14 +55,14 @@ export default class NotificationsSettingsTab extends React.Component {
this.setState({ currentSound: soundData.name || soundData.url }); this.setState({ currentSound: soundData.name || soundData.url });
} }
async _triggerUploader(e) { private triggerUploader = async (e: React.MouseEvent): Promise<void> => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this._soundUpload.current.click(); this.soundUpload.current.click();
} };
async _onSoundUploadChanged(e) { private onSoundUploadChanged = (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
if (!e.target.files || !e.target.files.length) { if (!e.target.files || !e.target.files.length) {
this.setState({ this.setState({
uploadedFile: null, uploadedFile: null,
@ -69,23 +74,23 @@ export default class NotificationsSettingsTab extends React.Component {
this.setState({ this.setState({
uploadedFile: file, uploadedFile: file,
}); });
} };
async _onClickSaveSound(e) { private onClickSaveSound = async (e: React.MouseEvent): Promise<void> => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
try { try {
await this._saveSound(); await this.saveSound();
} catch (ex) { } catch (ex) {
console.error( console.error(
`Unable to save notification sound for ${this.props.roomId}`, `Unable to save notification sound for ${this.props.roomId}`,
); );
console.error(ex); console.error(ex);
} }
} };
async _saveSound() { private async saveSound(): Promise<void> {
if (!this.state.uploadedFile) { if (!this.state.uploadedFile) {
return; return;
} }
@ -122,7 +127,7 @@ export default class NotificationsSettingsTab extends React.Component {
}); });
} }
_clearSound(e) { private clearSound = (e: React.MouseEvent): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
SettingsStore.setValue( SettingsStore.setValue(
@ -135,9 +140,9 @@ export default class NotificationsSettingsTab extends React.Component {
this.setState({ this.setState({
currentSound: "default", currentSound: "default",
}); });
} };
render() { public render(): JSX.Element {
let currentUploadedFile = null; let currentUploadedFile = null;
if (this.state.uploadedFile) { if (this.state.uploadedFile) {
currentUploadedFile = ( currentUploadedFile = (
@ -154,23 +159,23 @@ export default class NotificationsSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span> <span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
<div> <div>
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br /> <span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary"> <AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this.clearSound} kind="primary">
{ _t("Reset") } { _t("Reset") }
</AccessibleButton> </AccessibleButton>
</div> </div>
<div> <div>
<h3>{ _t("Set a new custom sound") }</h3> <h3>{ _t("Set a new custom sound") }</h3>
<form autoComplete="off" noValidate={true}> <form autoComplete="off" noValidate={true}>
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" /> <input ref={this.soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this.onSoundUploadChanged} accept="audio/*" />
</form> </form>
{ currentUploadedFile } { currentUploadedFile }
<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary"> <AccessibleButton className="mx_NotificationSound_browse" onClick={this.triggerUploader} kind="primary">
{ _t("Browse") } { _t("Browse") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary"> <AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this.onClickSaveSound} kind="primary">
{ _t("Save") } { _t("Save") }
</AccessibleButton> </AccessibleButton>
<br /> <br />

View file

@ -25,13 +25,11 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
import SpellCheckSettings from "../../SpellCheckSettings"; import SpellCheckSettings from "../../SpellCheckSettings";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types";
import PlatformPeg from "../../../../../PlatformPeg"; import PlatformPeg from "../../../../../PlatformPeg";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import { Service, startTermsFlow } from "../../../../../Terms"; import { Policies, Service, startTermsFlow } from "../../../../../Terms";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import IdentityAuthClient from "../../../../../IdentityAuthClient"; import IdentityAuthClient from "../../../../../IdentityAuthClient";
import { abbreviateUrl } from "../../../../../utils/UrlUtils"; import { abbreviateUrl } from "../../../../../utils/UrlUtils";
@ -40,15 +38,50 @@ import Spinner from "../../../elements/Spinner";
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature"; import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
import { ActionPayload } from "../../../../../dispatcher/payloads";
import ErrorDialog from "../../../dialogs/ErrorDialog";
import AccountPhoneNumbers from "../../account/PhoneNumbers";
import AccountEmailAddresses from "../../account/EmailAddresses";
import DiscoveryEmailAddresses from "../../discovery/EmailAddresses";
import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers";
import ChangePassword from "../../ChangePassword";
import InlineTermsAgreement from "../../../terms/InlineTermsAgreement";
import SetIdServer from "../../SetIdServer";
import SetIntegrationManager from "../../SetIntegrationManager";
interface IProps {
closeSettingsFn: () => void;
}
interface IState {
language: string;
spellCheckLanguages: string[];
haveIdServer: boolean;
serverSupportsSeparateAddAndBind: boolean;
idServerHasUnsignedTerms: boolean;
requiredPolicyInfo: { // This object is passed along to a component for handling
hasTerms: boolean;
policiesAndServices: {
service: Service;
policies: Policies;
}[]; // From the startTermsFlow callback
agreedUrls: string[]; // From the startTermsFlow callback
resolve: (values: string[]) => void; // Promise resolve function for startTermsFlow callback
};
emails: IThreepid[];
msisdns: IThreepid[];
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
canChangePassword: boolean;
idServerName: string;
}
@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab") @replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
export default class GeneralUserSettingsTab extends React.Component { export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
static propTypes = { private readonly dispatcherRef: string;
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() { constructor(props: IProps) {
super(); super(props);
this.state = { this.state = {
language: languageHandler.getCurrentLanguage(), language: languageHandler.getCurrentLanguage(),
@ -58,20 +91,23 @@ export default class GeneralUserSettingsTab extends React.Component {
idServerHasUnsignedTerms: false, idServerHasUnsignedTerms: false,
requiredPolicyInfo: { // This object is passed along to a component for handling requiredPolicyInfo: { // This object is passed along to a component for handling
hasTerms: false, hasTerms: false,
// policiesAndServices, // From the startTermsFlow callback policiesAndServices: null, // From the startTermsFlow callback
// agreedUrls, // From the startTermsFlow callback agreedUrls: null, // From the startTermsFlow callback
// resolve, // Promise resolve function for startTermsFlow callback resolve: null, // Promise resolve function for startTermsFlow callback
}, },
emails: [], emails: [],
msisdns: [], msisdns: [],
loading3pids: true, // whether or not the emails and msisdns have been loaded loading3pids: true, // whether or not the emails and msisdns have been loaded
canChangePassword: false,
idServerName: null,
}; };
this.dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
// TODO: [REACT-WARNING] Move this to constructor // TODO: [REACT-WARNING] Move this to constructor
async UNSAFE_componentWillMount() { // eslint-disable-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
public async UNSAFE_componentWillMount(): Promise<void> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind(); const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
@ -86,10 +122,10 @@ export default class GeneralUserSettingsTab extends React.Component {
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword }); this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
this._getThreepidState(); this.getThreepidState();
} }
async componentDidMount() { public async componentDidMount(): Promise<void> {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
if (plaf) { if (plaf) {
this.setState({ this.setState({
@ -98,30 +134,30 @@ export default class GeneralUserSettingsTab extends React.Component {
} }
} }
componentWillUnmount() { public componentWillUnmount(): void {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_onAction = (payload) => { private onAction = (payload: ActionPayload): void => {
if (payload.action === 'id_server_changed') { if (payload.action === 'id_server_changed') {
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) }); this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
this._getThreepidState(); this.getThreepidState();
} }
}; };
_onEmailsChange = (emails) => { private onEmailsChange = (emails: IThreepid[]): void => {
this.setState({ emails }); this.setState({ emails });
}; };
_onMsisdnsChange = (msisdns) => { private onMsisdnsChange = (msisdns: IThreepid[]): void => {
this.setState({ msisdns }); this.setState({ msisdns });
}; };
async _getThreepidState() { private async getThreepidState(): Promise<void> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
// Check to see if terms need accepting // Check to see if terms need accepting
this._checkTerms(); this.checkTerms();
// Need to get 3PIDs generally for Account section and possibly also for // Need to get 3PIDs generally for Account section and possibly also for
// Discovery (assuming we have an IS and terms are agreed). // Discovery (assuming we have an IS and terms are agreed).
@ -143,7 +179,7 @@ export default class GeneralUserSettingsTab extends React.Component {
}); });
} }
async _checkTerms() { private async checkTerms(): Promise<void> {
if (!this.state.haveIdServer) { if (!this.state.haveIdServer) {
this.setState({ idServerHasUnsignedTerms: false }); this.setState({ idServerHasUnsignedTerms: false });
return; return;
@ -176,6 +212,7 @@ export default class GeneralUserSettingsTab extends React.Component {
this.setState({ this.setState({
requiredPolicyInfo: { requiredPolicyInfo: {
hasTerms: false, hasTerms: false,
...this.state.requiredPolicyInfo,
}, },
}); });
} catch (e) { } catch (e) {
@ -187,19 +224,19 @@ export default class GeneralUserSettingsTab extends React.Component {
} }
} }
_onLanguageChange = (newLanguage) => { private onLanguageChange = (newLanguage: string): void => {
if (this.state.language === newLanguage) return; if (this.state.language === newLanguage) return;
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage); SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
this.setState({ language: newLanguage }); this.setState({ language: newLanguage });
const platform = PlatformPeg.get(); const platform = PlatformPeg.get();
if (platform) { if (platform) {
platform.setLanguage(newLanguage); platform.setLanguage([newLanguage]);
platform.reload(); platform.reload();
} }
}; };
_onSpellCheckLanguagesChange = (languages) => { private onSpellCheckLanguagesChange = (languages: string[]): void => {
this.setState({ spellCheckLanguages: languages }); this.setState({ spellCheckLanguages: languages });
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
@ -208,7 +245,7 @@ export default class GeneralUserSettingsTab extends React.Component {
} }
}; };
_onPasswordChangeError = (err) => { private onPasswordChangeError = (err): void => {
// TODO: Figure out a design that doesn't involve replacing the current dialog // TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || err.message || ""; let errMsg = err.error || err.message || "";
if (err.httpStatus === 403) { if (err.httpStatus === 403) {
@ -216,7 +253,6 @@ export default class GeneralUserSettingsTab extends React.Component {
} else if (!errMsg) { } else if (!errMsg) {
errMsg += ` (HTTP status ${err.httpStatus})`; errMsg += ` (HTTP status ${err.httpStatus})`;
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg); console.error("Failed to change password: " + errMsg);
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, { Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
title: _t("Error"), title: _t("Error"),
@ -224,9 +260,8 @@ export default class GeneralUserSettingsTab extends React.Component {
}); });
}; };
_onPasswordChanged = () => { private onPasswordChanged = (): void => {
// TODO: Figure out a design that doesn't involve replacing the current dialog // TODO: Figure out a design that doesn't involve replacing the current dialog
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Password changed', '', ErrorDialog, { Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
title: _t("Success"), title: _t("Success"),
description: _t( description: _t(
@ -236,7 +271,7 @@ export default class GeneralUserSettingsTab extends React.Component {
}); });
}; };
_onDeactivateClicked = () => { private onDeactivateClicked = (): void => {
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, { Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {
onFinished: (success) => { onFinished: (success) => {
if (success) this.props.closeSettingsFn(); if (success) this.props.closeSettingsFn();
@ -244,7 +279,7 @@ export default class GeneralUserSettingsTab extends React.Component {
}); });
}; };
_renderProfileSection() { private renderProfileSection(): JSX.Element {
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<ProfileSettings /> <ProfileSettings />
@ -252,18 +287,14 @@ export default class GeneralUserSettingsTab extends React.Component {
); );
} }
_renderAccountSection() { private renderAccountSection(): JSX.Element {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses");
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
let passwordChangeForm = ( let passwordChangeForm = (
<ChangePassword <ChangePassword
className="mx_GeneralUserSettingsTab_changePassword" className="mx_GeneralUserSettingsTab_changePassword"
rowClassName="" rowClassName=""
buttonKind="primary" buttonKind="primary"
onError={this._onPasswordChangeError} onError={this.onPasswordChangeError}
onFinished={this._onPasswordChanged} /> onFinished={this.onPasswordChanged} />
); );
let threepidSection = null; let threepidSection = null;
@ -278,15 +309,15 @@ export default class GeneralUserSettingsTab extends React.Component {
) { ) {
const emails = this.state.loading3pids const emails = this.state.loading3pids
? <Spinner /> ? <Spinner />
: <EmailAddresses : <AccountEmailAddresses
emails={this.state.emails} emails={this.state.emails}
onEmailsChange={this._onEmailsChange} onEmailsChange={this.onEmailsChange}
/>; />;
const msisdns = this.state.loading3pids const msisdns = this.state.loading3pids
? <Spinner /> ? <Spinner />
: <PhoneNumbers : <AccountPhoneNumbers
msisdns={this.state.msisdns} msisdns={this.state.msisdns}
onMsisdnsChange={this._onMsisdnsChange} onMsisdnsChange={this.onMsisdnsChange}
/>; />;
threepidSection = <div> threepidSection = <div>
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span> <span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
@ -318,37 +349,34 @@ export default class GeneralUserSettingsTab extends React.Component {
); );
} }
_renderLanguageSection() { private renderLanguageSection(): JSX.Element {
// TODO: Convert to new-styled Field // TODO: Convert to new-styled Field
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span> <span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
<LanguageDropdown <LanguageDropdown
className="mx_GeneralUserSettingsTab_languageInput" className="mx_GeneralUserSettingsTab_languageInput"
onOptionChange={this._onLanguageChange} onOptionChange={this.onLanguageChange}
value={this.state.language} value={this.state.language}
/> />
</div> </div>
); );
} }
_renderSpellCheckSection() { private renderSpellCheckSection(): JSX.Element {
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span> <span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
<SpellCheckSettings <SpellCheckSettings
languages={this.state.spellCheckLanguages} languages={this.state.spellCheckLanguages}
onLanguagesChange={this._onSpellCheckLanguagesChange} onLanguagesChange={this.onSpellCheckLanguagesChange}
/> />
</div> </div>
); );
} }
_renderDiscoverySection() { private renderDiscoverySection(): JSX.Element {
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
if (this.state.requiredPolicyInfo.hasTerms) { if (this.state.requiredPolicyInfo.hasTerms) {
const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
const intro = <span className="mx_SettingsTab_subsectionText"> const intro = <span className="mx_SettingsTab_subsectionText">
{ _t( { _t(
"Agree to the identity server (%(serverName)s) Terms of Service to " + "Agree to the identity server (%(serverName)s) Terms of Service to " +
@ -370,11 +398,8 @@ export default class GeneralUserSettingsTab extends React.Component {
); );
} }
const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses"); const emails = this.state.loading3pids ? <Spinner /> : <DiscoveryEmailAddresses emails={this.state.emails} />;
const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); const msisdns = this.state.loading3pids ? <Spinner /> : <DiscoveryPhoneNumbers msisdns={this.state.msisdns} />;
const emails = this.state.loading3pids ? <Spinner /> : <EmailAddresses emails={this.state.emails} />;
const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'> const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span> <span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
@ -388,12 +413,12 @@ export default class GeneralUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
{ threepidSection } { threepidSection }
{ /* has its own heading as it includes the current identity server */ } { /* has its own heading as it includes the current identity server */ }
<SetIdServer /> <SetIdServer missingTerms={false} />
</div> </div>
); );
} }
_renderManagementSection() { private renderManagementSection(): JSX.Element {
// TODO: Improve warning text for account deactivation // TODO: Improve warning text for account deactivation
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
@ -401,18 +426,16 @@ export default class GeneralUserSettingsTab extends React.Component {
<span className="mx_SettingsTab_subsectionText"> <span className="mx_SettingsTab_subsectionText">
{ _t("Deactivating your account is a permanent action - be careful!") } { _t("Deactivating your account is a permanent action - be careful!") }
</span> </span>
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger"> <AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
{ _t("Deactivate Account") } { _t("Deactivate Account") }
</AccessibleButton> </AccessibleButton>
</div> </div>
); );
} }
_renderIntegrationManagerSection() { private renderIntegrationManagerSection(): JSX.Element {
if (!SettingsStore.getValue(UIFeature.Widgets)) return null; if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
{ /* has its own heading as it includes the current integration manager */ } { /* has its own heading as it includes the current integration manager */ }
@ -421,7 +444,7 @@ export default class GeneralUserSettingsTab extends React.Component {
); );
} }
render() { public render(): JSX.Element {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck(); const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
@ -439,7 +462,7 @@ export default class GeneralUserSettingsTab extends React.Component {
if (SettingsStore.getValue(UIFeature.Deactivate)) { if (SettingsStore.getValue(UIFeature.Deactivate)) {
accountManagementSection = <> accountManagementSection = <>
<div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div> <div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
{ this._renderManagementSection() } { this.renderManagementSection() }
</>; </>;
} }
@ -447,19 +470,19 @@ export default class GeneralUserSettingsTab extends React.Component {
if (SettingsStore.getValue(UIFeature.IdentityServer)) { if (SettingsStore.getValue(UIFeature.IdentityServer)) {
discoverySection = <> discoverySection = <>
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div> <div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
{ this._renderDiscoverySection() } { this.renderDiscoverySection() }
</>; </>;
} }
return ( return (
<div className="mx_SettingsTab"> <div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{ _t("General") }</div> <div className="mx_SettingsTab_heading">{ _t("General") }</div>
{ this._renderProfileSection() } { this.renderProfileSection() }
{ this._renderAccountSection() } { this.renderAccountSection() }
{ this._renderLanguageSection() } { this.renderLanguageSection() }
{ supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null } { supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null }
{ discoverySection } { discoverySection }
{ this._renderIntegrationManagerSection() /* Has its own title */ } { this.renderIntegrationManagerSection() /* Has its own title */ }
{ accountManagementSection } { accountManagementSection }
</div> </div>
); );

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import PropTypes from "prop-types";
import SettingsStore from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
@ -26,28 +25,32 @@ import BetaCard from "../../../beta/BetaCard";
import SettingsFlag from '../../../elements/SettingsFlag'; import SettingsFlag from '../../../elements/SettingsFlag';
import { MatrixClientPeg } from '../../../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
export class LabsSettingToggle extends React.Component { interface ILabsSettingToggleProps {
static propTypes = { featureId: string;
featureId: PropTypes.string.isRequired, }
};
_onChange = async (checked) => { export class LabsSettingToggle extends React.Component<ILabsSettingToggleProps> {
private onChange = async (checked: boolean): Promise<void> => {
await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked); await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
this.forceUpdate(); this.forceUpdate();
}; };
render() { public render(): JSX.Element {
const label = SettingsStore.getDisplayName(this.props.featureId); const label = SettingsStore.getDisplayName(this.props.featureId);
const value = SettingsStore.getValue(this.props.featureId); const value = SettingsStore.getValue(this.props.featureId);
const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE); const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} disabled={!canChange} />; return <LabelledToggleSwitch value={value} label={label} onChange={this.onChange} disabled={!canChange} />;
} }
} }
interface IState {
showHiddenReadReceipts: boolean;
}
@replaceableComponent("views.settings.tabs.user.LabsUserSettingsTab") @replaceableComponent("views.settings.tabs.user.LabsUserSettingsTab")
export default class LabsUserSettingsTab extends React.Component { export default class LabsUserSettingsTab extends React.Component<{}, IState> {
constructor() { constructor(props: {}) {
super(); super(props);
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => { MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
this.setState({ showHiddenReadReceipts }); this.setState({ showHiddenReadReceipts });
@ -58,7 +61,7 @@ export default class LabsUserSettingsTab extends React.Component {
}; };
} }
render() { public render(): JSX.Element {
const features = SettingsStore.getFeatureSettingNames(); const features = SettingsStore.getFeatureSettingNames();
const [labs, betas] = features.reduce((arr, f) => { const [labs, betas] = features.reduce((arr, f) => {
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f); arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);

View file

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
@ -26,34 +25,40 @@ import * as FormattingUtils from "../../../../../utils/FormattingUtils";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import Analytics from "../../../../../Analytics"; import Analytics from "../../../../../Analytics";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
import * as sdk from "../../../../..";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import { privateShouldBeEncrypted } from "../../../../../createRoom"; import { privateShouldBeEncrypted } from "../../../../../createRoom";
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
import SecureBackupPanel from "../../SecureBackupPanel"; import SecureBackupPanel from "../../SecureBackupPanel";
import SettingsStore from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature"; import { UIFeature } from "../../../../../settings/UIFeature";
import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
import CountlyAnalytics from "../../../../../CountlyAnalytics"; import CountlyAnalytics from "../../../../../CountlyAnalytics";
import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
import { ActionPayload } from "../../../../../dispatcher/payloads";
import { Room } from "matrix-js-sdk/src/models/room";
import DevicesPanel from "../../DevicesPanel";
import SettingsFlag from "../../../elements/SettingsFlag";
import CrossSigningPanel from "../../CrossSigningPanel";
import EventIndexPanel from "../../EventIndexPanel";
import InlineSpinner from "../../../elements/InlineSpinner";
export class IgnoredUser extends React.Component { interface IIgnoredUserProps {
static propTypes = { userId: string;
userId: PropTypes.string.isRequired, onUnignored: (userId: string) => void;
onUnignored: PropTypes.func.isRequired, inProgress: boolean;
inProgress: PropTypes.bool.isRequired, }
};
_onUnignoreClicked = (e) => { export class IgnoredUser extends React.Component<IIgnoredUserProps> {
private onUnignoreClicked = (): void => {
this.props.onUnignored(this.props.userId); this.props.onUnignored(this.props.userId);
}; };
render() { public render(): JSX.Element {
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`; const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
return ( return (
<div className='mx_SecurityUserSettingsTab_ignoredUser'> <div className='mx_SecurityUserSettingsTab_ignoredUser'>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}> <AccessibleButton onClick={this.onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}>
{ _t('Unignore') } { _t('Unignore') }
</AccessibleButton> </AccessibleButton>
<span id={id}>{ this.props.userId }</span> <span id={id}>{ this.props.userId }</span>
@ -62,17 +67,26 @@ export class IgnoredUser extends React.Component {
} }
} }
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab") interface IProps {
export default class SecurityUserSettingsTab extends React.Component { closeSettingsFn: () => void;
static propTypes = { }
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() { interface IState {
super(); ignoredUserIds: string[];
waitingUnignored: string[];
managingInvites: boolean;
invitedRoomAmt: number;
}
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
private dispatcherRef: string;
constructor(props: IProps) {
super(props);
// Get number of rooms we're invited to // Get number of rooms we're invited to
const invitedRooms = this._getInvitedRooms(); const invitedRooms = this.getInvitedRooms();
this.state = { this.state = {
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(), ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
@ -80,59 +94,57 @@ export default class SecurityUserSettingsTab extends React.Component {
managingInvites: false, managingInvites: false,
invitedRoomAmt: invitedRooms.length, invitedRoomAmt: invitedRooms.length,
}; };
this._onAction = this._onAction.bind(this);
} }
_onAction({ action }) { private onAction = ({ action }: ActionPayload)=> {
if (action === "ignore_state_changed") { if (action === "ignore_state_changed") {
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers(); const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e)); const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored }); this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
} }
};
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
} }
componentDidMount() { public componentWillUnmount(): void {
this.dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_updateBlacklistDevicesFlag = (checked) => { private updateBlacklistDevicesFlag = (checked): void => {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
}; };
_updateAnalytics = (checked) => { private updateAnalytics = (checked: boolean): void => {
checked ? Analytics.enable() : Analytics.disable(); checked ? Analytics.enable() : Analytics.disable();
CountlyAnalytics.instance.enable(/* anonymous = */ !checked); CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId()); PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
}; };
_onExportE2eKeysClicked = () => { private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', '', Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{ matrixClient: MatrixClientPeg.get() }, { matrixClient: MatrixClientPeg.get() },
); );
}; };
_onImportE2eKeysClicked = () => { private onImportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Import E2E Keys', '', Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'), import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
{ matrixClient: MatrixClientPeg.get() }, { matrixClient: MatrixClientPeg.get() },
); );
}; };
_onGoToUserProfileClick = () => { private onGoToUserProfileClick = (): void => {
dis.dispatch({ dis.dispatch({
action: 'view_user_info', action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(), userId: MatrixClientPeg.get().getUserId(),
}); });
this.props.closeSettingsFn(); this.props.closeSettingsFn();
} };
_onUserUnignored = async (userId) => { private onUserUnignored = async (userId: string): Promise<void> => {
const { ignoredUserIds, waitingUnignored } = this.state; const { ignoredUserIds, waitingUnignored } = this.state;
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e)); const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
@ -144,24 +156,23 @@ export default class SecurityUserSettingsTab extends React.Component {
} }
}; };
_getInvitedRooms = () => { private getInvitedRooms = (): Room[] => {
return MatrixClientPeg.get().getRooms().filter((r) => { return MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite"); return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
}); });
}; };
_manageInvites = async (accept) => { private manageInvites = async (accept: boolean): Promise<void> => {
this.setState({ this.setState({
managingInvites: true, managingInvites: true,
}); });
// Compile array of invitation room ids // Compile array of invitation room ids
const invitedRoomIds = this._getInvitedRooms().map((room) => { const invitedRoomIds = this.getInvitedRooms().map((room) => {
return room.roomId; return room.roomId;
}); });
// Execute all acceptances/rejections sequentially // Execute all acceptances/rejections sequentially
const self = this;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli); const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli);
for (let i = 0; i < invitedRoomIds.length; i++) { for (let i = 0; i < invitedRoomIds.length; i++) {
@ -170,7 +181,7 @@ export default class SecurityUserSettingsTab extends React.Component {
// Accept/reject invite // Accept/reject invite
await action(roomId).then(() => { await action(roomId).then(() => {
// No error, update invited rooms button // No error, update invited rooms button
this.setState({ invitedRoomAmt: self.state.invitedRoomAmt - 1 }); this.setState({ invitedRoomAmt: this.state.invitedRoomAmt - 1 });
}, async (e) => { }, async (e) => {
// Action failure // Action failure
if (e.errcode === "M_LIMIT_EXCEEDED") { if (e.errcode === "M_LIMIT_EXCEEDED") {
@ -192,17 +203,15 @@ export default class SecurityUserSettingsTab extends React.Component {
}); });
}; };
_onAcceptAllInvitesClicked = (ev) => { private onAcceptAllInvitesClicked = (): void => {
this._manageInvites(true); this.manageInvites(true);
}; };
_onRejectAllInvitesClicked = (ev) => { private onRejectAllInvitesClicked = (): void => {
this._manageInvites(false); this.manageInvites(false);
}; };
_renderCurrentDeviceInfo() { private renderCurrentDeviceInfo(): JSX.Element {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const deviceId = client.deviceId; const deviceId = client.deviceId;
let identityKey = client.getDeviceEd25519Key(); let identityKey = client.getDeviceEd25519Key();
@ -216,10 +225,10 @@ export default class SecurityUserSettingsTab extends React.Component {
if (client.isCryptoEnabled()) { if (client.isCryptoEnabled()) {
importExportButtons = ( importExportButtons = (
<div className='mx_SecurityUserSettingsTab_importExportButtons'> <div className='mx_SecurityUserSettingsTab_importExportButtons'>
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}> <AccessibleButton kind='primary' onClick={this.onExportE2eKeysClicked}>
{ _t("Export E2E room keys") } { _t("Export E2E room keys") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}> <AccessibleButton kind='primary' onClick={this.onImportE2eKeysClicked}>
{ _t("Import E2E room keys") } { _t("Import E2E room keys") }
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -231,7 +240,7 @@ export default class SecurityUserSettingsTab extends React.Component {
noSendUnverifiedSetting = <SettingsFlag noSendUnverifiedSetting = <SettingsFlag
name='blacklistUnverifiedDevices' name='blacklistUnverifiedDevices'
level={SettingLevel.DEVICE} level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag} onChange={this.updateBlacklistDevicesFlag}
/>; />;
} }
@ -254,7 +263,7 @@ export default class SecurityUserSettingsTab extends React.Component {
); );
} }
_renderIgnoredUsers() { private renderIgnoredUsers(): JSX.Element {
const { waitingUnignored, ignoredUserIds } = this.state; const { waitingUnignored, ignoredUserIds } = this.state;
const userIds = !ignoredUserIds?.length const userIds = !ignoredUserIds?.length
@ -263,7 +272,7 @@ export default class SecurityUserSettingsTab extends React.Component {
return ( return (
<IgnoredUser <IgnoredUser
userId={u} userId={u}
onUnignored={this._onUserUnignored} onUnignored={this.onUserUnignored}
key={u} key={u}
inProgress={waitingUnignored.includes(u)} inProgress={waitingUnignored.includes(u)}
/> />
@ -280,15 +289,14 @@ export default class SecurityUserSettingsTab extends React.Component {
); );
} }
_renderManageInvites() { private renderManageInvites(): JSX.Element {
if (this.state.invitedRoomAmt === 0) { if (this.state.invitedRoomAmt === 0) {
return null; return null;
} }
const invitedRooms = this._getInvitedRooms(); const invitedRooms = this.getInvitedRooms();
const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); const onClickAccept = this.onAcceptAllInvitesClicked.bind(this, invitedRooms);
const onClickAccept = this._onAcceptAllInvitesClicked.bind(this, invitedRooms); const onClickReject = this.onRejectAllInvitesClicked.bind(this, invitedRooms);
const onClickReject = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
return ( return (
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'> <div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
<span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span> <span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
@ -303,11 +311,8 @@ export default class SecurityUserSettingsTab extends React.Component {
); );
} }
render() { public render(): JSX.Element {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
const secureBackup = ( const secureBackup = (
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
@ -329,7 +334,6 @@ export default class SecurityUserSettingsTab extends React.Component {
// it's useful to have for testing the feature. If there's no interest // it's useful to have for testing the feature. If there's no interest
// in having advanced details here once all flows are implemented, we // in having advanced details here once all flows are implemented, we
// can remove this. // can remove this.
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
const crossSigning = ( const crossSigning = (
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span> <span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
@ -365,16 +369,15 @@ export default class SecurityUserSettingsTab extends React.Component {
{ _t("Learn more about how we use analytics.") } { _t("Learn more about how we use analytics.") }
</AccessibleButton> </AccessibleButton>
</div> </div>
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} /> <SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this.updateAnalytics} />
</div> </div>
</React.Fragment>; </React.Fragment>;
} }
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
let advancedSection; let advancedSection;
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
const ignoreUsersPanel = this._renderIgnoredUsers(); const ignoreUsersPanel = this.renderIgnoredUsers();
const invitesPanel = this._renderManageInvites(); const invitesPanel = this.renderManageInvites();
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null; const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
// only show the section if there's something to show // only show the section if there's something to show
if (ignoreUsersPanel || invitesPanel || e2ePanel) { if (ignoreUsersPanel || invitesPanel || e2ePanel) {
@ -399,7 +402,7 @@ export default class SecurityUserSettingsTab extends React.Component {
"Manage the names of and sign out of your sessions below or " + "Manage the names of and sign out of your sessions below or " +
"<a>verify them in your User Profile</a>.", {}, "<a>verify them in your User Profile</a>.", {},
{ {
a: sub => <AccessibleButton kind="link" onClick={this._onGoToUserProfileClick}> a: sub => <AccessibleButton kind="link" onClick={this.onGoToUserProfileClick}>
{ sub } { sub }
</AccessibleButton>, </AccessibleButton>,
}, },
@ -415,7 +418,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{ secureBackup } { secureBackup }
{ eventIndex } { eventIndex }
{ crossSigning } { crossSigning }
{ this._renderCurrentDeviceInfo() } { this.renderCurrentDeviceInfo() }
</div> </div>
{ privacySection } { privacySection }
{ advancedSection } { advancedSection }

View file

@ -25,7 +25,7 @@ interface IProps {
policiesAndServicePairs: any[]; policiesAndServicePairs: any[];
onFinished: (string) => void; onFinished: (string) => void;
agreedUrls: string[]; // array of URLs the user has accepted agreedUrls: string[]; // array of URLs the user has accepted
introElement: Node; introElement: React.ReactNode;
} }
interface IState { interface IState {

View file

@ -1086,11 +1086,11 @@
"Failed to upload profile picture!": "Failed to upload profile picture!", "Failed to upload profile picture!": "Failed to upload profile picture!",
"Upload new:": "Upload new:", "Upload new:": "Upload new:",
"No display name": "No display name", "No display name": "No display name",
"New passwords don't match": "New passwords don't match",
"Passwords can't be empty": "Passwords can't be empty",
"Warning!": "Warning!", "Warning!": "Warning!",
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
"Export E2E room keys": "Export E2E room keys", "Export E2E room keys": "Export E2E room keys",
"New passwords don't match": "New passwords don't match",
"Passwords can't be empty": "Passwords can't be empty",
"Do you want to set an email address?": "Do you want to set an email address?", "Do you want to set an email address?": "Do you want to set an email address?",
"Confirm password": "Confirm password", "Confirm password": "Confirm password",
"Passwords don't match": "Passwords don't match", "Passwords don't match": "Passwords don't match",