Merge pull request #6843 from SimonBrandner/task/settings-ts
Convert `/src/components/views/settings/` to TS
This commit is contained in:
commit
f02d6e8240
14 changed files with 557 additions and 459 deletions
|
@ -33,6 +33,7 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
|
|||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
export enum UserTab {
|
||||
General = "USER_GENERAL_TAB",
|
||||
|
@ -47,8 +48,7 @@ export enum UserTab {
|
|||
Help = "USER_HELP_TAB",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
interface IProps extends IDialogProps {
|
||||
initialTabId?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,78 +17,81 @@ limitations under the License.
|
|||
|
||||
import Field from "../elements/Field";
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import withValidation from '../elements/Validation';
|
||||
import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import PassphraseField from "../auth/PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
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_NEW_PASSWORD = 'field_new_password';
|
||||
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")
|
||||
export default class ChangePassword extends React.Component {
|
||||
static propTypes = {
|
||||
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 = {
|
||||
export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
onFinished() {},
|
||||
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 = {
|
||||
fieldValid: {},
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
confirm: true,
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
if (!this.props.confirm) {
|
||||
this._changePassword(cli, oldPassword, newPassword);
|
||||
this.changePassword(cli, oldPassword, newPassword);
|
||||
return;
|
||||
}
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
|
@ -109,20 +112,20 @@ export default class ChangePassword extends React.Component {
|
|||
<button
|
||||
key="exportRoomKeys"
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}
|
||||
onClick={this.onExportE2eKeysClicked}
|
||||
>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
],
|
||||
onFinished: (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 = {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
|
@ -136,12 +139,12 @@ export default class ChangePassword extends React.Component {
|
|||
};
|
||||
|
||||
this.setState({
|
||||
phase: ChangePassword.Phases.Uploading,
|
||||
phase: Phase.Uploading,
|
||||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this._optionallySetEmail().then((confirmed) => {
|
||||
return this.optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
didSetEmail: confirmed,
|
||||
});
|
||||
|
@ -153,7 +156,7 @@ export default class ChangePassword extends React.Component {
|
|||
this.props.onError(err);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
phase: Phase.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
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
|
||||
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
||||
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
||||
title: _t('Do you want to set an email address?'),
|
||||
});
|
||||
return modal.finished.then(([confirmed]) => confirmed);
|
||||
}
|
||||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||
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;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
|
@ -187,19 +201,19 @@ export default class ChangePassword extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onChangeOldPassword = (ev) => {
|
||||
private onChangeOldPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
oldPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onOldPasswordValidate = async fieldState => {
|
||||
private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validateOldPasswordRules(fieldState);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validateOldPasswordRules = withValidation({
|
||||
private validateOldPasswordRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -209,29 +223,29 @@ export default class ChangePassword extends React.Component {
|
|||
],
|
||||
});
|
||||
|
||||
onChangeNewPassword = (ev) => {
|
||||
private onChangeNewPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onNewPasswordValidate = result => {
|
||||
private onNewPasswordValidate = (result: IValidationResult): void => {
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||
};
|
||||
|
||||
onChangeNewPasswordConfirm = (ev) => {
|
||||
private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPasswordConfirm: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onNewPasswordConfirmValidate = async fieldState => {
|
||||
private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validatePasswordConfirmRules = withValidation({
|
||||
private validatePasswordConfirmRules = withValidation<this>({
|
||||
rules: [
|
||||
{
|
||||
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();
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
|
@ -260,20 +274,20 @@ export default class ChangePassword extends React.Component {
|
|||
const oldPassword = this.state.oldPassword;
|
||||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
const err = this.props.onCheckPassword(
|
||||
const err = this.checkPassword(
|
||||
oldPassword, newPassword, confirmPassword,
|
||||
);
|
||||
if (err) {
|
||||
this.props.onError(err);
|
||||
} 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,
|
||||
// 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) {
|
||||
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
|
||||
// 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()) {
|
||||
return true;
|
||||
|
@ -319,7 +333,7 @@ export default class ChangePassword extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
allFieldsValid() {
|
||||
private allFieldsValid(): boolean {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (!this.state.fieldValid[keys[i]]) {
|
||||
|
@ -329,7 +343,7 @@ export default class ChangePassword extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
findFirstInvalidField(fieldIDs) {
|
||||
private findFirstInvalidField(fieldIDs: string[]): Field {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
|
@ -338,12 +352,12 @@ export default class ChangePassword extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
|
||||
switch (this.state.phase) {
|
||||
case ChangePassword.Phases.Edit:
|
||||
case Phase.Edit:
|
||||
return (
|
||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
<div className={rowClassName}>
|
||||
|
@ -385,7 +399,7 @@ export default class ChangePassword extends React.Component {
|
|||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
case ChangePassword.Phases.Uploading:
|
||||
case Phase.Uploading:
|
||||
return (
|
||||
<div className="mx_Dialog_content">
|
||||
<Spinner />
|
|
@ -27,15 +27,31 @@ import QuestionDialog from '../dialogs/QuestionDialog';
|
|||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||
import { accessSecretStorage } from '../../../SecurityManager';
|
||||
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";
|
||||
|
||||
@replaceableComponent("views.settings.SecureBackupPanel")
|
||||
export default class SecureBackupPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
|
@ -50,42 +66,42 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._checkKeyBackupStatus();
|
||||
public componentDidMount(): void {
|
||||
this.checkKeyBackupStatus();
|
||||
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on(
|
||||
'crypto.keyBackupSessionsRemaining',
|
||||
this._onKeyBackupSessionsRemaining,
|
||||
this.onKeyBackupSessionsRemaining,
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener(
|
||||
'crypto.keyBackupSessionsRemaining',
|
||||
this._onKeyBackupSessionsRemaining,
|
||||
this.onKeyBackupSessionsRemaining,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyBackupSessionsRemaining = (sessionsRemaining) => {
|
||||
private onKeyBackupSessionsRemaining = (sessionsRemaining: number): void => {
|
||||
this.setState({
|
||||
sessionsRemaining,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyBackupStatus = () => {
|
||||
private onKeyBackupStatus = (): void => {
|
||||
// This just loads the current backup status rather than forcing
|
||||
// a re-check otherwise we risk causing infinite loops
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
this.loadBackupStatus();
|
||||
};
|
||||
|
||||
async _checkKeyBackupStatus() {
|
||||
this._getUpdatedDiagnostics();
|
||||
private async checkKeyBackupStatus(): Promise<void> {
|
||||
this.getUpdatedDiagnostics();
|
||||
try {
|
||||
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
|
||||
this.setState({
|
||||
|
@ -96,7 +112,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
});
|
||||
} catch (e) {
|
||||
logger.log("Unable to fetch check backup status", e);
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
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._getUpdatedDiagnostics();
|
||||
this.getUpdatedDiagnostics();
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: null,
|
||||
|
@ -121,7 +137,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
});
|
||||
} catch (e) {
|
||||
logger.log("Unable to fetch key backup status", e);
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
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 secretStorage = cli.crypto.secretStorage;
|
||||
|
||||
|
@ -142,7 +158,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const secretStorageReady = await cli.isSecretStorageReady();
|
||||
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
|
@ -152,18 +168,18 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
_startNewBackup = () => {
|
||||
private startNewBackup = (): void => {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_deleteBackup = () => {
|
||||
private deleteBackup = (): void => {
|
||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||
title: _t('Delete Backup'),
|
||||
description: _t(
|
||||
|
@ -176,33 +192,33 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
if (!proceed) return;
|
||||
this.setState({ loading: true });
|
||||
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
|
||||
this._loadBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_restoreBackup = async () => {
|
||||
private restoreBackup = async (): Promise<void> => {
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_resetSecretStorage = async () => {
|
||||
private resetSecretStorage = async (): Promise<void> => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage(() => { }, /* forceReset = */ true);
|
||||
await accessSecretStorage(async () => { }, /* forceReset = */ true);
|
||||
} catch (e) {
|
||||
console.error("Error resetting secret storage", e);
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({ error: e });
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
if (this.unmounted) return;
|
||||
this.loadBackupStatus();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
|
@ -263,7 +279,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
</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 validity = sub =>
|
||||
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
||||
|
@ -371,14 +387,14 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
</>;
|
||||
|
||||
actions.push(
|
||||
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
|
||||
<AccessibleButton key="restore" kind="primary" onClick={this.restoreBackup}>
|
||||
{ restoreButtonCaption }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
||||
if (!isSecureBackupRequired()) {
|
||||
actions.push(
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this.deleteBackup}>
|
||||
{ _t("Delete Backup") }
|
||||
</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>
|
||||
</>;
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this.startNewBackup}>
|
||||
{ _t("Set up") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
@ -400,7 +416,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
|
||||
if (secretStorageKeyInAccount) {
|
||||
actions.push(
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this.resetSecretStorage}>
|
||||
{ _t("Reset") }
|
||||
</AccessibleButton>,
|
||||
);
|
|
@ -16,16 +16,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import Field from "../../elements/Field";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import * as Email from "../../../../email";
|
||||
import AddThreepid from "../../../../AddThreepid";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
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.
|
||||
|
@ -39,42 +39,45 @@ places to communicate errors - these should be replaced with inline validation w
|
|||
that is available.
|
||||
*/
|
||||
|
||||
export class ExistingEmailAddress extends React.Component {
|
||||
static propTypes = {
|
||||
email: PropTypes.object.isRequired,
|
||||
onRemoved: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IExistingEmailAddressProps {
|
||||
email: IThreepid;
|
||||
onRemoved: (emails: IThreepid) => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IExistingEmailAddressState {
|
||||
verifyRemove: boolean;
|
||||
}
|
||||
|
||||
export class ExistingEmailAddress extends React.Component<IExistingEmailAddressProps, IExistingEmailAddressState> {
|
||||
constructor(props: IExistingEmailAddressProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: true });
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
private onDontRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
|
||||
return this.props.onRemoved(this.props.email);
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to remove contact information: " + err);
|
||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||
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) {
|
||||
return (
|
||||
<div className="mx_ExistingEmailAddress">
|
||||
|
@ -91,14 +94,14 @@ export class ExistingEmailAddress extends React.Component {
|
|||
{ _t("Remove %(email)s?", { email: this.props.email.address } ) }
|
||||
</span>
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
onClick={this.onActuallyRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
>
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
onClick={this.onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
>
|
||||
|
@ -111,7 +114,7 @@ export class ExistingEmailAddress extends React.Component {
|
|||
return (
|
||||
<div className="mx_ExistingEmailAddress">
|
||||
<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") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -119,14 +122,21 @@ export class ExistingEmailAddress extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.account.EmailAddresses")
|
||||
export default class EmailAddresses extends React.Component {
|
||||
static propTypes = {
|
||||
emails: PropTypes.array.isRequired,
|
||||
onEmailsChange: PropTypes.func.isRequired,
|
||||
}
|
||||
interface IProps {
|
||||
emails: IThreepid[];
|
||||
onEmailsChange: (emails: Partial<IThreepid>[]) => void;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
this.props.onEmailsChange(emails);
|
||||
};
|
||||
|
||||
_onChangeNewEmailAddress = (e) => {
|
||||
private onChangeNewEmailAddress = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newEmailAddress: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onAddClick = (e) => {
|
||||
private onAddClick = (e: React.FormEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.newEmailAddress) return;
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const email = this.state.newEmailAddress;
|
||||
|
||||
// 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.preventDefault();
|
||||
|
||||
|
@ -192,7 +201,7 @@ export default class EmailAddresses extends React.Component {
|
|||
const email = this.state.newEmailAddress;
|
||||
const emails = [
|
||||
...this.props.emails,
|
||||
{ address: email, medium: "email" },
|
||||
{ address: email, medium: ThreepidMedium.Email },
|
||||
];
|
||||
this.props.onEmailsChange(emails);
|
||||
newEmailAddress = "";
|
||||
|
@ -205,7 +214,6 @@ export default class EmailAddresses extends React.Component {
|
|||
});
|
||||
}).catch((err) => {
|
||||
this.setState({ continueDisabled: false });
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
|
||||
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) => {
|
||||
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
|
||||
return <ExistingEmailAddress email={e} onRemoved={this.onRemoved} key={e.address} />;
|
||||
});
|
||||
|
||||
let addButton = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -237,7 +245,7 @@ export default class EmailAddresses extends React.Component {
|
|||
<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
|
||||
onClick={this._onContinueClick}
|
||||
onClick={this.onContinueClick}
|
||||
kind="primary"
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
|
@ -251,7 +259,7 @@ export default class EmailAddresses extends React.Component {
|
|||
<div className="mx_EmailAddresses">
|
||||
{ existingEmailElements }
|
||||
<form
|
||||
onSubmit={this._onAddClick}
|
||||
onSubmit={this.onAddClick}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EmailAddresses_new"
|
||||
|
@ -262,7 +270,7 @@ export default class EmailAddresses extends React.Component {
|
|||
autoComplete="off"
|
||||
disabled={this.state.verifying}
|
||||
value={this.state.newEmailAddress}
|
||||
onChange={this._onChangeNewEmailAddress}
|
||||
onChange={this.onChangeNewEmailAddress}
|
||||
/>
|
||||
{ addButton }
|
||||
</form>
|
|
@ -16,16 +16,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import Field from "../../elements/Field";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import AddThreepid from "../../../../AddThreepid";
|
||||
import CountryDropdown from "../../auth/CountryDropdown";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
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.
|
||||
|
@ -34,42 +35,45 @@ This is a copy/paste of EmailAddresses, mostly.
|
|||
|
||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||
|
||||
export class ExistingPhoneNumber extends React.Component {
|
||||
static propTypes = {
|
||||
msisdn: PropTypes.object.isRequired,
|
||||
onRemoved: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IExistingPhoneNumberProps {
|
||||
msisdn: IThreepid;
|
||||
onRemoved: (phoneNumber: IThreepid) => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IExistingPhoneNumberState {
|
||||
verifyRemove: boolean;
|
||||
}
|
||||
|
||||
export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberProps, IExistingPhoneNumberState> {
|
||||
constructor(props: IExistingPhoneNumberProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: true });
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
private onDontRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
|
||||
return this.props.onRemoved(this.props.msisdn);
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to remove contact information: " + err);
|
||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||
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) {
|
||||
return (
|
||||
<div className="mx_ExistingPhoneNumber">
|
||||
|
@ -86,14 +90,14 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
|
||||
</span>
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
onClick={this.onActuallyRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||
>
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
onClick={this.onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||
>
|
||||
|
@ -106,7 +110,7 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
return (
|
||||
<div className="mx_ExistingPhoneNumber">
|
||||
<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") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -114,19 +118,30 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.account.PhoneNumbers")
|
||||
export default class PhoneNumbers extends React.Component {
|
||||
static propTypes = {
|
||||
msisdns: PropTypes.array.isRequired,
|
||||
onMsisdnsChange: PropTypes.func.isRequired,
|
||||
}
|
||||
interface IProps {
|
||||
msisdns: IThreepid[];
|
||||
onMsisdnsChange: (phoneNumbers: Partial<IThreepid>[]) => void;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
this.state = {
|
||||
verifying: false,
|
||||
verifyError: false,
|
||||
verifyError: null,
|
||||
verifyMsisdn: "",
|
||||
addTask: null,
|
||||
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);
|
||||
this.props.onMsisdnsChange(msisdns);
|
||||
};
|
||||
|
||||
_onChangeNewPhoneNumber = (e) => {
|
||||
private onChangeNewPhoneNumber = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPhoneNumber: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onChangeNewPhoneNumberCode = (e) => {
|
||||
private onChangeNewPhoneNumberCode = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPhoneNumberCode: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onAddClick = (e) => {
|
||||
private onAddClick = (e: React.MouseEvent | React.FormEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.newPhoneNumber) return;
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const phoneNumber = this.state.newPhoneNumber;
|
||||
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.preventDefault();
|
||||
|
||||
|
@ -190,7 +204,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
if (finished) {
|
||||
const msisdns = [
|
||||
...this.props.msisdns,
|
||||
{ address, medium: "msisdn" },
|
||||
{ address, medium: ThreepidMedium.Phone },
|
||||
];
|
||||
this.props.onMsisdnsChange(msisdns);
|
||||
newPhoneNumber = "";
|
||||
|
@ -207,7 +221,6 @@ export default class PhoneNumbers extends React.Component {
|
|||
}).catch((err) => {
|
||||
this.setState({ continueDisabled: false });
|
||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify phone number: " + err);
|
||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||
title: _t("Unable to verify phone number."),
|
||||
|
@ -219,17 +232,17 @@ export default class PhoneNumbers extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onCountryChanged = (e) => {
|
||||
this.setState({ phoneCountry: e.iso2 });
|
||||
private onCountryChanged = (country: PhoneNumberCountryDefinition): void => {
|
||||
this.setState({ phoneCountry: country.iso2 });
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
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 = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -243,17 +256,17 @@ export default class PhoneNumbers extends React.Component {
|
|||
<br />
|
||||
{ this.state.verifyError }
|
||||
</div>
|
||||
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
|
||||
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
|
||||
<Field
|
||||
type="text"
|
||||
label={_t("Verification code")}
|
||||
autoComplete="off"
|
||||
disabled={this.state.continueDisabled}
|
||||
value={this.state.newPhoneNumberCode}
|
||||
onChange={this._onChangeNewPhoneNumberCode}
|
||||
onChange={this.onChangeNewPhoneNumberCode}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this._onContinueClick}
|
||||
onClick={this.onContinueClick}
|
||||
kind="primary"
|
||||
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"
|
||||
value={this.state.phoneCountry}
|
||||
disabled={this.state.verifying}
|
||||
|
@ -275,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
return (
|
||||
<div className="mx_PhoneNumbers">
|
||||
{ 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">
|
||||
<Field
|
||||
type="text"
|
||||
|
@ -284,7 +297,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
disabled={this.state.verifying}
|
||||
prefixComponent={phoneCountry}
|
||||
value={this.state.newPhoneNumber}
|
||||
onChange={this._onChangeNewPhoneNumber}
|
||||
onChange={this.onChangeNewPhoneNumber}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
|
@ -16,14 +16,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
import AddThreepid from '../../../../AddThreepid';
|
||||
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.
|
||||
|
@ -41,12 +42,19 @@ that is available.
|
|||
TODO: Reduce all the copying between account vs. discovery components.
|
||||
*/
|
||||
|
||||
export class EmailAddress extends React.Component {
|
||||
static propTypes = {
|
||||
email: PropTypes.object.isRequired,
|
||||
};
|
||||
interface IEmailAddressProps {
|
||||
email: IThreepid;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const { bound } = props.email;
|
||||
|
@ -60,17 +68,17 @@ export class EmailAddress extends React.Component {
|
|||
}
|
||||
|
||||
// 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;
|
||||
this.setState({ bound });
|
||||
}
|
||||
|
||||
async changeBinding({ bind, label, errorTitle }) {
|
||||
if (!(await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind())) {
|
||||
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const { medium, address } = this.props.email;
|
||||
|
||||
try {
|
||||
|
@ -103,8 +111,7 @@ export class EmailAddress extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||
const { medium, address } = this.props.email;
|
||||
|
||||
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.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -147,9 +154,9 @@ export class EmailAddress extends React.Component {
|
|||
label: "revoke",
|
||||
errorTitle: _t("Unable to revoke sharing for email address"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onShareClick = (e) => {
|
||||
private onShareClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -157,9 +164,9 @@ export class EmailAddress extends React.Component {
|
|||
label: "share",
|
||||
errorTitle: _t("Unable to share email address"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onContinueClick = async (e) => {
|
||||
private onContinueClick = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -173,7 +180,6 @@ export class EmailAddress extends React.Component {
|
|||
});
|
||||
} catch (err) {
|
||||
this.setState({ continueDisabled: false });
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
|
||||
title: _t("Your email address hasn't been verified yet"),
|
||||
|
@ -188,10 +194,9 @@ export class EmailAddress extends React.Component {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
public render(): JSX.Element {
|
||||
const { address } = this.props.email;
|
||||
const { verifying, bound } = this.state;
|
||||
|
||||
|
@ -234,14 +239,13 @@ export class EmailAddress extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
interface IProps {
|
||||
emails: IThreepid[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.discovery.EmailAddresses")
|
||||
export default class EmailAddresses extends React.Component {
|
||||
static propTypes = {
|
||||
emails: PropTypes.array.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
export default class EmailAddresses extends React.Component<IProps> {
|
||||
public render(): JSX.Element {
|
||||
let content;
|
||||
if (this.props.emails.length > 0) {
|
||||
content = this.props.emails.map((e) => {
|
|
@ -16,14 +16,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
import AddThreepid from '../../../../AddThreepid';
|
||||
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.
|
||||
|
@ -32,12 +34,21 @@ This is a copy/paste of EmailAddresses, mostly.
|
|||
|
||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||
|
||||
export class PhoneNumber extends React.Component {
|
||||
static propTypes = {
|
||||
msisdn: PropTypes.object.isRequired,
|
||||
};
|
||||
interface IPhoneNumberProps {
|
||||
msisdn: IThreepid;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const { bound } = props.msisdn;
|
||||
|
@ -48,21 +59,22 @@ export class PhoneNumber extends React.Component {
|
|||
addTask: null,
|
||||
continueDisabled: false,
|
||||
bound,
|
||||
verifyError: null,
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
this.setState({ bound });
|
||||
}
|
||||
|
||||
async changeBinding({ bind, label, errorTitle }) {
|
||||
if (!(await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind())) {
|
||||
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const { medium, address } = this.props.msisdn;
|
||||
|
||||
try {
|
||||
|
@ -99,8 +111,7 @@ export class PhoneNumber extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||
const { medium, address } = this.props.msisdn;
|
||||
|
||||
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.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -147,9 +158,9 @@ export class PhoneNumber extends React.Component {
|
|||
label: "revoke",
|
||||
errorTitle: _t("Unable to revoke sharing for phone number"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onShareClick = (e) => {
|
||||
private onShareClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -157,15 +168,15 @@ export class PhoneNumber extends React.Component {
|
|||
label: "share",
|
||||
errorTitle: _t("Unable to share phone number"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onVerificationCodeChange = (e) => {
|
||||
private onVerificationCodeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
verificationCode: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onContinueClick = async (e) => {
|
||||
private onContinueClick = async (e: React.MouseEvent | React.FormEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -183,7 +194,6 @@ export class PhoneNumber extends React.Component {
|
|||
} catch (err) {
|
||||
this.setState({ continueDisabled: false });
|
||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify phone number: " + err);
|
||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||
title: _t("Unable to verify phone number."),
|
||||
|
@ -193,11 +203,9 @@ export class PhoneNumber extends React.Component {
|
|||
this.setState({ verifyError: _t("Incorrect verification code") });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
public render(): JSX.Element {
|
||||
const { address } = this.props.msisdn;
|
||||
const { verifying, bound } = this.state;
|
||||
|
||||
|
@ -247,13 +255,13 @@ export class PhoneNumber extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
||||
export default class PhoneNumbers extends React.Component {
|
||||
static propTypes = {
|
||||
msisdns: PropTypes.array.isRequired,
|
||||
}
|
||||
interface IProps {
|
||||
msisdns: IThreepid[];
|
||||
}
|
||||
|
||||
render() {
|
||||
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
||||
export default class PhoneNumbers extends React.Component<IProps> {
|
||||
public render(): JSX.Element {
|
||||
let content;
|
||||
if (this.props.msisdns.length > 0) {
|
||||
content = this.props.msisdns.map((e) => {
|
|
@ -15,45 +15,46 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
import * as sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
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")
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class GeneralRoomSettingsTab extends React.Component<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isRoomPublished: false, // loaded async
|
||||
};
|
||||
}
|
||||
|
||||
_onLeaveClick = () => {
|
||||
private onLeaveClick = (): void => {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
||||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
|
||||
public render(): JSX.Element {
|
||||
const client = this.context;
|
||||
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>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
|
||||
<AccessibleButton kind='danger' onClick={this.onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
</AccessibleButton>
|
||||
</div>
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
@ -24,16 +23,21 @@ import SettingsStore from '../../../../../settings/SettingsStore';
|
|||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
currentSound: string;
|
||||
uploadedFile: File;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
|
||||
export default class NotificationsSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class NotificationsSettingsTab extends React.Component<IProps, IState> {
|
||||
private soundUpload = createRef<HTMLInputElement>();
|
||||
|
||||
_soundUpload = createRef();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
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
|
||||
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);
|
||||
if (!soundData) {
|
||||
return;
|
||||
|
@ -50,14 +55,14 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
this.setState({ currentSound: soundData.name || soundData.url });
|
||||
}
|
||||
|
||||
async _triggerUploader(e) {
|
||||
private triggerUploader = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
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) {
|
||||
this.setState({
|
||||
uploadedFile: null,
|
||||
|
@ -69,23 +74,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
this.setState({
|
||||
uploadedFile: file,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async _onClickSaveSound(e) {
|
||||
private onClickSaveSound = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await this._saveSound();
|
||||
await this.saveSound();
|
||||
} catch (ex) {
|
||||
console.error(
|
||||
`Unable to save notification sound for ${this.props.roomId}`,
|
||||
);
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async _saveSound() {
|
||||
private async saveSound(): Promise<void> {
|
||||
if (!this.state.uploadedFile) {
|
||||
return;
|
||||
}
|
||||
|
@ -122,7 +127,7 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_clearSound(e) {
|
||||
private clearSound = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
SettingsStore.setValue(
|
||||
|
@ -135,9 +140,9 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
this.setState({
|
||||
currentSound: "default",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let currentUploadedFile = null;
|
||||
if (this.state.uploadedFile) {
|
||||
currentUploadedFile = (
|
||||
|
@ -154,23 +159,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
|
||||
<div>
|
||||
<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") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{ _t("Set a new custom sound") }</h3>
|
||||
<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>
|
||||
|
||||
{ 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") }
|
||||
</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") }
|
||||
</AccessibleButton>
|
||||
<br />
|
|
@ -25,13 +25,11 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
|
|||
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||
import PropTypes from "prop-types";
|
||||
import PlatformPeg from "../../../../../PlatformPeg";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../..";
|
||||
import Modal from "../../../../../Modal";
|
||||
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 IdentityAuthClient from "../../../../../IdentityAuthClient";
|
||||
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
|
||||
|
@ -40,15 +38,50 @@ import Spinner from "../../../elements/Spinner";
|
|||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
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")
|
||||
export default class GeneralUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
||||
private readonly dispatcherRef: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
language: languageHandler.getCurrentLanguage(),
|
||||
|
@ -58,20 +91,23 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
idServerHasUnsignedTerms: false,
|
||||
requiredPolicyInfo: { // This object is passed along to a component for handling
|
||||
hasTerms: false,
|
||||
// policiesAndServices, // From the startTermsFlow callback
|
||||
// agreedUrls, // From the startTermsFlow callback
|
||||
// resolve, // Promise resolve function for startTermsFlow callback
|
||||
policiesAndServices: null, // From the startTermsFlow callback
|
||||
agreedUrls: null, // From the startTermsFlow callback
|
||||
resolve: null, // Promise resolve function for startTermsFlow callback
|
||||
},
|
||||
emails: [],
|
||||
msisdns: [],
|
||||
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
|
||||
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 serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
|
||||
|
@ -86,10 +122,10 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
|
||||
|
||||
this._getThreepidState();
|
||||
this.getThreepidState();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
public async componentDidMount(): Promise<void> {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
this.setState({
|
||||
|
@ -98,30 +134,30 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.action === 'id_server_changed') {
|
||||
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
|
||||
this._getThreepidState();
|
||||
this.getThreepidState();
|
||||
}
|
||||
};
|
||||
|
||||
_onEmailsChange = (emails) => {
|
||||
private onEmailsChange = (emails: IThreepid[]): void => {
|
||||
this.setState({ emails });
|
||||
};
|
||||
|
||||
_onMsisdnsChange = (msisdns) => {
|
||||
private onMsisdnsChange = (msisdns: IThreepid[]): void => {
|
||||
this.setState({ msisdns });
|
||||
};
|
||||
|
||||
async _getThreepidState() {
|
||||
private async getThreepidState(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
// Check to see if terms need accepting
|
||||
this._checkTerms();
|
||||
this.checkTerms();
|
||||
|
||||
// Need to get 3PIDs generally for Account section and possibly also for
|
||||
// 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) {
|
||||
this.setState({ idServerHasUnsignedTerms: false });
|
||||
return;
|
||||
|
@ -176,6 +212,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
this.setState({
|
||||
requiredPolicyInfo: {
|
||||
hasTerms: false,
|
||||
...this.state.requiredPolicyInfo,
|
||||
},
|
||||
});
|
||||
} 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;
|
||||
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
|
||||
this.setState({ language: newLanguage });
|
||||
const platform = PlatformPeg.get();
|
||||
if (platform) {
|
||||
platform.setLanguage(newLanguage);
|
||||
platform.setLanguage([newLanguage]);
|
||||
platform.reload();
|
||||
}
|
||||
};
|
||||
|
||||
_onSpellCheckLanguagesChange = (languages) => {
|
||||
private onSpellCheckLanguagesChange = (languages: string[]): void => {
|
||||
this.setState({ spellCheckLanguages: languages });
|
||||
|
||||
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
|
||||
let errMsg = err.error || err.message || "";
|
||||
if (err.httpStatus === 403) {
|
||||
|
@ -216,7 +253,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
} else if (!errMsg) {
|
||||
errMsg += ` (HTTP status ${err.httpStatus})`;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change password: " + errMsg);
|
||||
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
|
||||
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
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
|
||||
title: _t("Success"),
|
||||
description: _t(
|
||||
|
@ -236,7 +271,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onDeactivateClicked = () => {
|
||||
private onDeactivateClicked = (): void => {
|
||||
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {
|
||||
onFinished: (success) => {
|
||||
if (success) this.props.closeSettingsFn();
|
||||
|
@ -244,7 +279,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_renderProfileSection() {
|
||||
private renderProfileSection(): JSX.Element {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<ProfileSettings />
|
||||
|
@ -252,18 +287,14 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderAccountSection() {
|
||||
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||
const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses");
|
||||
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
|
||||
|
||||
private renderAccountSection(): JSX.Element {
|
||||
let passwordChangeForm = (
|
||||
<ChangePassword
|
||||
className="mx_GeneralUserSettingsTab_changePassword"
|
||||
rowClassName=""
|
||||
buttonKind="primary"
|
||||
onError={this._onPasswordChangeError}
|
||||
onFinished={this._onPasswordChanged} />
|
||||
onError={this.onPasswordChangeError}
|
||||
onFinished={this.onPasswordChanged} />
|
||||
);
|
||||
|
||||
let threepidSection = null;
|
||||
|
@ -278,15 +309,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
) {
|
||||
const emails = this.state.loading3pids
|
||||
? <Spinner />
|
||||
: <EmailAddresses
|
||||
: <AccountEmailAddresses
|
||||
emails={this.state.emails}
|
||||
onEmailsChange={this._onEmailsChange}
|
||||
onEmailsChange={this.onEmailsChange}
|
||||
/>;
|
||||
const msisdns = this.state.loading3pids
|
||||
? <Spinner />
|
||||
: <PhoneNumbers
|
||||
: <AccountPhoneNumbers
|
||||
msisdns={this.state.msisdns}
|
||||
onMsisdnsChange={this._onMsisdnsChange}
|
||||
onMsisdnsChange={this.onMsisdnsChange}
|
||||
/>;
|
||||
threepidSection = <div>
|
||||
<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
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
|
||||
<LanguageDropdown
|
||||
className="mx_GeneralUserSettingsTab_languageInput"
|
||||
onOptionChange={this._onLanguageChange}
|
||||
onOptionChange={this.onLanguageChange}
|
||||
value={this.state.language}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSpellCheckSection() {
|
||||
private renderSpellCheckSection(): JSX.Element {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
|
||||
<SpellCheckSettings
|
||||
languages={this.state.spellCheckLanguages}
|
||||
onLanguagesChange={this._onSpellCheckLanguagesChange}
|
||||
onLanguagesChange={this.onSpellCheckLanguagesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderDiscoverySection() {
|
||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
||||
|
||||
private renderDiscoverySection(): JSX.Element {
|
||||
if (this.state.requiredPolicyInfo.hasTerms) {
|
||||
const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
|
||||
const intro = <span className="mx_SettingsTab_subsectionText">
|
||||
{ _t(
|
||||
"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 PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers");
|
||||
|
||||
const emails = this.state.loading3pids ? <Spinner /> : <EmailAddresses emails={this.state.emails} />;
|
||||
const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
|
||||
const emails = this.state.loading3pids ? <Spinner /> : <DiscoveryEmailAddresses emails={this.state.emails} />;
|
||||
const msisdns = this.state.loading3pids ? <Spinner /> : <DiscoveryPhoneNumbers msisdns={this.state.msisdns} />;
|
||||
|
||||
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
|
||||
<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">
|
||||
{ threepidSection }
|
||||
{ /* has its own heading as it includes the current identity server */ }
|
||||
<SetIdServer />
|
||||
<SetIdServer missingTerms={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderManagementSection() {
|
||||
private renderManagementSection(): JSX.Element {
|
||||
// TODO: Improve warning text for account deactivation
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
|
@ -401,18 +426,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
<span className="mx_SettingsTab_subsectionText">
|
||||
{ _t("Deactivating your account is a permanent action - be careful!") }
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
|
||||
<AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
|
||||
{ _t("Deactivate Account") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderIntegrationManagerSection() {
|
||||
private renderIntegrationManagerSection(): JSX.Element {
|
||||
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
||||
|
||||
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
{ /* 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 supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
||||
|
||||
|
@ -439,7 +462,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
if (SettingsStore.getValue(UIFeature.Deactivate)) {
|
||||
accountManagementSection = <>
|
||||
<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)) {
|
||||
discoverySection = <>
|
||||
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
|
||||
{ this._renderDiscoverySection() }
|
||||
{ this.renderDiscoverySection() }
|
||||
</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||
{ this._renderProfileSection() }
|
||||
{ this._renderAccountSection() }
|
||||
{ this._renderLanguageSection() }
|
||||
{ supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null }
|
||||
{ this.renderProfileSection() }
|
||||
{ this.renderAccountSection() }
|
||||
{ this.renderLanguageSection() }
|
||||
{ supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null }
|
||||
{ discoverySection }
|
||||
{ this._renderIntegrationManagerSection() /* Has its own title */ }
|
||||
{ this.renderIntegrationManagerSection() /* Has its own title */ }
|
||||
{ accountManagementSection }
|
||||
</div>
|
||||
);
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import PropTypes from "prop-types";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
|
@ -26,28 +25,32 @@ import BetaCard from "../../../beta/BetaCard";
|
|||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||
|
||||
export class LabsSettingToggle extends React.Component {
|
||||
static propTypes = {
|
||||
featureId: PropTypes.string.isRequired,
|
||||
};
|
||||
interface ILabsSettingToggleProps {
|
||||
featureId: string;
|
||||
}
|
||||
|
||||
_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);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const label = SettingsStore.getDisplayName(this.props.featureId);
|
||||
const value = SettingsStore.getValue(this.props.featureId);
|
||||
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")
|
||||
export default class LabsUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((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 [labs, betas] = features.reduce((arr, f) => {
|
||||
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
|
@ -26,34 +25,40 @@ import * as FormattingUtils from "../../../../../utils/FormattingUtils";
|
|||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Analytics from "../../../../../Analytics";
|
||||
import Modal from "../../../../../Modal";
|
||||
import * as sdk from "../../../../..";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { privateShouldBeEncrypted } from "../../../../../createRoom";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import SecureBackupPanel from "../../SecureBackupPanel";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||
import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||
import CountlyAnalytics from "../../../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
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 {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
onUnignored: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool.isRequired,
|
||||
};
|
||||
interface IIgnoredUserProps {
|
||||
userId: string;
|
||||
onUnignored: (userId: string) => void;
|
||||
inProgress: boolean;
|
||||
}
|
||||
|
||||
_onUnignoreClicked = (e) => {
|
||||
export class IgnoredUser extends React.Component<IIgnoredUserProps> {
|
||||
private onUnignoreClicked = (): void => {
|
||||
this.props.onUnignored(this.props.userId);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
|
||||
return (
|
||||
<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') }
|
||||
</AccessibleButton>
|
||||
<span id={id}>{ this.props.userId }</span>
|
||||
|
@ -62,17 +67,26 @@ export class IgnoredUser extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
|
||||
export default class SecurityUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
closeSettingsFn: () => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IState {
|
||||
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
|
||||
const invitedRooms = this._getInvitedRooms();
|
||||
const invitedRooms = this.getInvitedRooms();
|
||||
|
||||
this.state = {
|
||||
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
|
||||
|
@ -80,59 +94,57 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
managingInvites: false,
|
||||
invitedRoomAmt: invitedRooms.length,
|
||||
};
|
||||
|
||||
this._onAction = this._onAction.bind(this);
|
||||
}
|
||||
|
||||
_onAction({ action }) {
|
||||
private onAction = ({ action }: ActionPayload)=> {
|
||||
if (action === "ignore_state_changed") {
|
||||
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
|
||||
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_updateBlacklistDevicesFlag = (checked) => {
|
||||
private updateBlacklistDevicesFlag = (checked): void => {
|
||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
||||
};
|
||||
|
||||
_updateAnalytics = (checked) => {
|
||||
private updateAnalytics = (checked: boolean): void => {
|
||||
checked ? Analytics.enable() : Analytics.disable();
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
|
||||
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
|
||||
};
|
||||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
||||
_onImportE2eKeysClicked = () => {
|
||||
private onImportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
||||
_onGoToUserProfileClick = () => {
|
||||
private onGoToUserProfileClick = (): void => {
|
||||
dis.dispatch({
|
||||
action: 'view_user_info',
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
});
|
||||
this.props.closeSettingsFn();
|
||||
}
|
||||
};
|
||||
|
||||
_onUserUnignored = async (userId) => {
|
||||
private onUserUnignored = async (userId: string): Promise<void> => {
|
||||
const { ignoredUserIds, waitingUnignored } = this.state;
|
||||
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 r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
|
||||
});
|
||||
};
|
||||
|
||||
_manageInvites = async (accept) => {
|
||||
private manageInvites = async (accept: boolean): Promise<void> => {
|
||||
this.setState({
|
||||
managingInvites: true,
|
||||
});
|
||||
|
||||
// Compile array of invitation room ids
|
||||
const invitedRoomIds = this._getInvitedRooms().map((room) => {
|
||||
const invitedRoomIds = this.getInvitedRooms().map((room) => {
|
||||
return room.roomId;
|
||||
});
|
||||
|
||||
// Execute all acceptances/rejections sequentially
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli);
|
||||
for (let i = 0; i < invitedRoomIds.length; i++) {
|
||||
|
@ -170,7 +181,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
// Accept/reject invite
|
||||
await action(roomId).then(() => {
|
||||
// No error, update invited rooms button
|
||||
this.setState({ invitedRoomAmt: self.state.invitedRoomAmt - 1 });
|
||||
this.setState({ invitedRoomAmt: this.state.invitedRoomAmt - 1 });
|
||||
}, async (e) => {
|
||||
// Action failure
|
||||
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||
|
@ -192,17 +203,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onAcceptAllInvitesClicked = (ev) => {
|
||||
this._manageInvites(true);
|
||||
private onAcceptAllInvitesClicked = (): void => {
|
||||
this.manageInvites(true);
|
||||
};
|
||||
|
||||
_onRejectAllInvitesClicked = (ev) => {
|
||||
this._manageInvites(false);
|
||||
private onRejectAllInvitesClicked = (): void => {
|
||||
this.manageInvites(false);
|
||||
};
|
||||
|
||||
_renderCurrentDeviceInfo() {
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
|
||||
private renderCurrentDeviceInfo(): JSX.Element {
|
||||
const client = MatrixClientPeg.get();
|
||||
const deviceId = client.deviceId;
|
||||
let identityKey = client.getDeviceEd25519Key();
|
||||
|
@ -216,10 +225,10 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
if (client.isCryptoEnabled()) {
|
||||
importExportButtons = (
|
||||
<div className='mx_SecurityUserSettingsTab_importExportButtons'>
|
||||
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
|
||||
<AccessibleButton kind='primary' onClick={this.onExportE2eKeysClicked}>
|
||||
{ _t("Export E2E room keys") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
|
||||
<AccessibleButton kind='primary' onClick={this.onImportE2eKeysClicked}>
|
||||
{ _t("Import E2E room keys") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -231,7 +240,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
noSendUnverifiedSetting = <SettingsFlag
|
||||
name='blacklistUnverifiedDevices'
|
||||
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 userIds = !ignoredUserIds?.length
|
||||
|
@ -263,7 +272,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
return (
|
||||
<IgnoredUser
|
||||
userId={u}
|
||||
onUnignored={this._onUserUnignored}
|
||||
onUnignored={this.onUserUnignored}
|
||||
key={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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const invitedRooms = this._getInvitedRooms();
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
const onClickAccept = this._onAcceptAllInvitesClicked.bind(this, invitedRooms);
|
||||
const onClickReject = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
|
||||
const invitedRooms = this.getInvitedRooms();
|
||||
const onClickAccept = this.onAcceptAllInvitesClicked.bind(this, invitedRooms);
|
||||
const onClickReject = this.onRejectAllInvitesClicked.bind(this, invitedRooms);
|
||||
return (
|
||||
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
|
||||
<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 DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
|
||||
|
||||
const secureBackup = (
|
||||
<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
|
||||
// in having advanced details here once all flows are implemented, we
|
||||
// can remove this.
|
||||
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
|
||||
const crossSigning = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<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.") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
|
||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this.updateAnalytics} />
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
|
||||
let advancedSection;
|
||||
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
|
||||
const ignoreUsersPanel = this._renderIgnoredUsers();
|
||||
const invitesPanel = this._renderManageInvites();
|
||||
const ignoreUsersPanel = this.renderIgnoredUsers();
|
||||
const invitesPanel = this.renderManageInvites();
|
||||
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
|
||||
// only show the section if there's something to show
|
||||
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 " +
|
||||
"<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 }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
|
@ -415,7 +418,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
{ secureBackup }
|
||||
{ eventIndex }
|
||||
{ crossSigning }
|
||||
{ this._renderCurrentDeviceInfo() }
|
||||
{ this.renderCurrentDeviceInfo() }
|
||||
</div>
|
||||
{ privacySection }
|
||||
{ advancedSection }
|
|
@ -25,7 +25,7 @@ interface IProps {
|
|||
policiesAndServicePairs: any[];
|
||||
onFinished: (string) => void;
|
||||
agreedUrls: string[]; // array of URLs the user has accepted
|
||||
introElement: Node;
|
||||
introElement: React.ReactNode;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -1086,11 +1086,11 @@
|
|||
"Failed to upload profile picture!": "Failed to upload profile picture!",
|
||||
"Upload new:": "Upload new:",
|
||||
"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!",
|
||||
"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",
|
||||
"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?",
|
||||
"Confirm password": "Confirm password",
|
||||
"Passwords don't match": "Passwords don't match",
|
||||
|
|
Loading…
Reference in a new issue