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 { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
|
||||||
export enum UserTab {
|
export enum UserTab {
|
||||||
General = "USER_GENERAL_TAB",
|
General = "USER_GENERAL_TAB",
|
||||||
|
@ -47,8 +48,7 @@ export enum UserTab {
|
||||||
Help = "USER_HELP_TAB",
|
Help = "USER_HELP_TAB",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends IDialogProps {
|
||||||
onFinished: (success: boolean) => void;
|
|
||||||
initialTabId?: string;
|
initialTabId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,78 +17,81 @@ limitations under the License.
|
||||||
|
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import Spinner from '../elements/Spinner';
|
import Spinner from '../elements/Spinner';
|
||||||
import withValidation from '../elements/Validation';
|
import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import PassphraseField from "../auth/PassphraseField";
|
import PassphraseField from "../auth/PassphraseField";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import SetEmailDialog from "../dialogs/SetEmailDialog";
|
||||||
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
|
|
||||||
const FIELD_OLD_PASSWORD = 'field_old_password';
|
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||||
const FIELD_NEW_PASSWORD = 'field_new_password';
|
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||||
|
|
||||||
@replaceableComponent("views.settings.ChangePassword")
|
enum Phase {
|
||||||
export default class ChangePassword extends React.Component {
|
Edit = "edit",
|
||||||
static propTypes = {
|
Uploading = "uploading",
|
||||||
onFinished: PropTypes.func,
|
Error = "error",
|
||||||
onError: PropTypes.func,
|
}
|
||||||
onCheckPassword: PropTypes.func,
|
|
||||||
rowClassName: PropTypes.string,
|
interface IProps {
|
||||||
buttonClassName: PropTypes.string,
|
onFinished?: ({ didSetEmail: boolean }?) => void;
|
||||||
buttonKind: PropTypes.string,
|
onError?: (error: {error: string}) => void;
|
||||||
buttonLabel: PropTypes.string,
|
rowClassName?: string;
|
||||||
confirm: PropTypes.bool,
|
buttonClassName?: string;
|
||||||
|
buttonKind?: string;
|
||||||
|
buttonLabel?: string;
|
||||||
|
confirm?: boolean;
|
||||||
// Whether to autoFocus the new password input
|
// Whether to autoFocus the new password input
|
||||||
autoFocusNewPasswordInput: PropTypes.bool,
|
autoFocusNewPasswordInput?: boolean;
|
||||||
};
|
className?: string;
|
||||||
|
shouldAskForEmail?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
static Phases = {
|
interface IState {
|
||||||
Edit: "edit",
|
fieldValid: {};
|
||||||
Uploading: "uploading",
|
phase: Phase;
|
||||||
Error: "error",
|
oldPassword: string;
|
||||||
};
|
newPassword: string;
|
||||||
|
newPasswordConfirm: string;
|
||||||
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
@replaceableComponent("views.settings.ChangePassword")
|
||||||
|
export default class ChangePassword extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
onFinished() {},
|
onFinished() {},
|
||||||
onError() {},
|
onError() {},
|
||||||
onCheckPassword(oldPass, newPass, confirmPass) {
|
|
||||||
if (newPass !== confirmPass) {
|
|
||||||
return {
|
|
||||||
error: _t("New passwords don't match"),
|
|
||||||
};
|
|
||||||
} else if (!newPass || newPass.length === 0) {
|
|
||||||
return {
|
|
||||||
error: _t("Passwords can't be empty"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirm: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
confirm: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
fieldValid: {},
|
fieldValid: {},
|
||||||
phase: ChangePassword.Phases.Edit,
|
phase: Phase.Edit,
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
newPasswordConfirm: "",
|
newPasswordConfirm: "",
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
changePassword(oldPassword, newPassword) {
|
private onChangePassword(oldPassword: string, newPassword: string): void {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (!this.props.confirm) {
|
if (!this.props.confirm) {
|
||||||
this._changePassword(cli, oldPassword, newPassword);
|
this.changePassword(cli, oldPassword, newPassword);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
description:
|
description:
|
||||||
|
@ -109,20 +112,20 @@ export default class ChangePassword extends React.Component {
|
||||||
<button
|
<button
|
||||||
key="exportRoomKeys"
|
key="exportRoomKeys"
|
||||||
className="mx_Dialog_primary"
|
className="mx_Dialog_primary"
|
||||||
onClick={this._onExportE2eKeysClicked}
|
onClick={this.onExportE2eKeysClicked}
|
||||||
>
|
>
|
||||||
{ _t('Export E2E room keys') }
|
{ _t('Export E2E room keys') }
|
||||||
</button>,
|
</button>,
|
||||||
],
|
],
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this._changePassword(cli, oldPassword, newPassword);
|
this.changePassword(cli, oldPassword, newPassword);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_changePassword(cli, oldPassword, newPassword) {
|
private changePassword(cli: MatrixClient, oldPassword: string, newPassword: string): void {
|
||||||
const authDict = {
|
const authDict = {
|
||||||
type: 'm.login.password',
|
type: 'm.login.password',
|
||||||
identifier: {
|
identifier: {
|
||||||
|
@ -136,12 +139,12 @@ export default class ChangePassword extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: ChangePassword.Phases.Uploading,
|
phase: Phase.Uploading,
|
||||||
});
|
});
|
||||||
|
|
||||||
cli.setPassword(authDict, newPassword).then(() => {
|
cli.setPassword(authDict, newPassword).then(() => {
|
||||||
if (this.props.shouldAskForEmail) {
|
if (this.props.shouldAskForEmail) {
|
||||||
return this._optionallySetEmail().then((confirmed) => {
|
return this.optionallySetEmail().then((confirmed) => {
|
||||||
this.props.onFinished({
|
this.props.onFinished({
|
||||||
didSetEmail: confirmed,
|
didSetEmail: confirmed,
|
||||||
});
|
});
|
||||||
|
@ -153,7 +156,7 @@ export default class ChangePassword extends React.Component {
|
||||||
this.props.onError(err);
|
this.props.onError(err);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: ChangePassword.Phases.Edit,
|
phase: Phase.Edit,
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
newPasswordConfirm: "",
|
newPasswordConfirm: "",
|
||||||
|
@ -161,16 +164,27 @@ export default class ChangePassword extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_optionallySetEmail() {
|
private checkPassword(oldPass: string, newPass: string, confirmPass: string): {error: string} {
|
||||||
|
if (newPass !== confirmPass) {
|
||||||
|
return {
|
||||||
|
error: _t("New passwords don't match"),
|
||||||
|
};
|
||||||
|
} else if (!newPass || newPass.length === 0) {
|
||||||
|
return {
|
||||||
|
error: _t("Passwords can't be empty"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private optionallySetEmail(): Promise<boolean> {
|
||||||
// Ask for an email otherwise the user has no way to reset their password
|
// Ask for an email otherwise the user has no way to reset their password
|
||||||
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
|
||||||
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
||||||
title: _t('Do you want to set an email address?'),
|
title: _t('Do you want to set an email address?'),
|
||||||
});
|
});
|
||||||
return modal.finished.then(([confirmed]) => confirmed);
|
return modal.finished.then(([confirmed]) => confirmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onExportE2eKeysClicked = () => {
|
private onExportE2eKeysClicked = (): void => {
|
||||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||||
{
|
{
|
||||||
|
@ -179,7 +193,7 @@ export default class ChangePassword extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
markFieldValid(fieldID, valid) {
|
private markFieldValid(fieldID: string, valid: boolean): void {
|
||||||
const { fieldValid } = this.state;
|
const { fieldValid } = this.state;
|
||||||
fieldValid[fieldID] = valid;
|
fieldValid[fieldID] = valid;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -187,19 +201,19 @@ export default class ChangePassword extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeOldPassword = (ev) => {
|
private onChangeOldPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
oldPassword: ev.target.value,
|
oldPassword: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onOldPasswordValidate = async fieldState => {
|
private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||||
const result = await this.validateOldPasswordRules(fieldState);
|
const result = await this.validateOldPasswordRules(fieldState);
|
||||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validateOldPasswordRules = withValidation({
|
private validateOldPasswordRules = withValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -209,29 +223,29 @@ export default class ChangePassword extends React.Component {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
onChangeNewPassword = (ev) => {
|
private onChangeNewPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPassword: ev.target.value,
|
newPassword: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewPasswordValidate = result => {
|
private onNewPasswordValidate = (result: IValidationResult): void => {
|
||||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeNewPasswordConfirm = (ev) => {
|
private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPasswordConfirm: ev.target.value,
|
newPasswordConfirm: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewPasswordConfirmValidate = async fieldState => {
|
private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validatePasswordConfirmRules = withValidation({
|
private validatePasswordConfirmRules = withValidation<this>({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -248,7 +262,7 @@ export default class ChangePassword extends React.Component {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
onClickChange = async (ev) => {
|
private onClickChange = async (ev: React.MouseEvent | React.FormEvent): Promise<void> => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||||
|
@ -260,20 +274,20 @@ export default class ChangePassword extends React.Component {
|
||||||
const oldPassword = this.state.oldPassword;
|
const oldPassword = this.state.oldPassword;
|
||||||
const newPassword = this.state.newPassword;
|
const newPassword = this.state.newPassword;
|
||||||
const confirmPassword = this.state.newPasswordConfirm;
|
const confirmPassword = this.state.newPasswordConfirm;
|
||||||
const err = this.props.onCheckPassword(
|
const err = this.checkPassword(
|
||||||
oldPassword, newPassword, confirmPassword,
|
oldPassword, newPassword, confirmPassword,
|
||||||
);
|
);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.props.onError(err);
|
this.props.onError(err);
|
||||||
} else {
|
} else {
|
||||||
this.changePassword(oldPassword, newPassword);
|
this.onChangePassword(oldPassword, newPassword);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async verifyFieldsBeforeSubmit() {
|
private async verifyFieldsBeforeSubmit(): Promise<boolean> {
|
||||||
// Blur the active element if any, so we first run its blur validation,
|
// Blur the active element if any, so we first run its blur validation,
|
||||||
// which is less strict than the pass we're about to do below for all fields.
|
// which is less strict than the pass we're about to do below for all fields.
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.blur();
|
activeElement.blur();
|
||||||
}
|
}
|
||||||
|
@ -300,7 +314,7 @@ export default class ChangePassword extends React.Component {
|
||||||
|
|
||||||
// Validation and state updates are async, so we need to wait for them to complete
|
// Validation and state updates are async, so we need to wait for them to complete
|
||||||
// first. Queue a `setState` callback and wait for it to resolve.
|
// first. Queue a `setState` callback and wait for it to resolve.
|
||||||
await new Promise(resolve => this.setState({}, resolve));
|
await new Promise<void>((resolve) => this.setState({}, resolve));
|
||||||
|
|
||||||
if (this.allFieldsValid()) {
|
if (this.allFieldsValid()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -319,7 +333,7 @@ export default class ChangePassword extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allFieldsValid() {
|
private allFieldsValid(): boolean {
|
||||||
const keys = Object.keys(this.state.fieldValid);
|
const keys = Object.keys(this.state.fieldValid);
|
||||||
for (let i = 0; i < keys.length; ++i) {
|
for (let i = 0; i < keys.length; ++i) {
|
||||||
if (!this.state.fieldValid[keys[i]]) {
|
if (!this.state.fieldValid[keys[i]]) {
|
||||||
|
@ -329,7 +343,7 @@ export default class ChangePassword extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
findFirstInvalidField(fieldIDs) {
|
private findFirstInvalidField(fieldIDs: string[]): Field {
|
||||||
for (const fieldID of fieldIDs) {
|
for (const fieldID of fieldIDs) {
|
||||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||||
return this[fieldID];
|
return this[fieldID];
|
||||||
|
@ -338,12 +352,12 @@ export default class ChangePassword extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const rowClassName = this.props.rowClassName;
|
const rowClassName = this.props.rowClassName;
|
||||||
const buttonClassName = this.props.buttonClassName;
|
const buttonClassName = this.props.buttonClassName;
|
||||||
|
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
case ChangePassword.Phases.Edit:
|
case Phase.Edit:
|
||||||
return (
|
return (
|
||||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||||
<div className={rowClassName}>
|
<div className={rowClassName}>
|
||||||
|
@ -385,7 +399,7 @@ export default class ChangePassword extends React.Component {
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
case ChangePassword.Phases.Uploading:
|
case Phase.Uploading:
|
||||||
return (
|
return (
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<Spinner />
|
<Spinner />
|
|
@ -27,15 +27,31 @@ import QuestionDialog from '../dialogs/QuestionDialog';
|
||||||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||||
import { accessSecretStorage } from '../../../SecurityManager';
|
import { accessSecretStorage } from '../../../SecurityManager';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||||
|
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
loading: boolean;
|
||||||
|
error: null;
|
||||||
|
backupKeyStored: boolean;
|
||||||
|
backupKeyCached: boolean;
|
||||||
|
backupKeyWellFormed: boolean;
|
||||||
|
secretStorageKeyInAccount: boolean;
|
||||||
|
secretStorageReady: boolean;
|
||||||
|
backupInfo: IKeyBackupInfo;
|
||||||
|
backupSigStatus: TrustInfo;
|
||||||
|
sessionsRemaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
@replaceableComponent("views.settings.SecureBackupPanel")
|
@replaceableComponent("views.settings.SecureBackupPanel")
|
||||||
export default class SecureBackupPanel extends React.PureComponent {
|
export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
constructor(props) {
|
private unmounted = false;
|
||||||
|
|
||||||
|
constructor(props: {}) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._unmounted = false;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -50,42 +66,42 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this._checkKeyBackupStatus();
|
this.checkKeyBackupStatus();
|
||||||
|
|
||||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||||
MatrixClientPeg.get().on(
|
MatrixClientPeg.get().on(
|
||||||
'crypto.keyBackupSessionsRemaining',
|
'crypto.keyBackupSessionsRemaining',
|
||||||
this._onKeyBackupSessionsRemaining,
|
this.onKeyBackupSessionsRemaining,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
|
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||||
MatrixClientPeg.get().removeListener(
|
MatrixClientPeg.get().removeListener(
|
||||||
'crypto.keyBackupSessionsRemaining',
|
'crypto.keyBackupSessionsRemaining',
|
||||||
this._onKeyBackupSessionsRemaining,
|
this.onKeyBackupSessionsRemaining,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyBackupSessionsRemaining = (sessionsRemaining) => {
|
private onKeyBackupSessionsRemaining = (sessionsRemaining: number): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
sessionsRemaining,
|
sessionsRemaining,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_onKeyBackupStatus = () => {
|
private onKeyBackupStatus = (): void => {
|
||||||
// This just loads the current backup status rather than forcing
|
// This just loads the current backup status rather than forcing
|
||||||
// a re-check otherwise we risk causing infinite loops
|
// a re-check otherwise we risk causing infinite loops
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
}
|
};
|
||||||
|
|
||||||
async _checkKeyBackupStatus() {
|
private async checkKeyBackupStatus(): Promise<void> {
|
||||||
this._getUpdatedDiagnostics();
|
this.getUpdatedDiagnostics();
|
||||||
try {
|
try {
|
||||||
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
|
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -96,7 +112,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.log("Unable to fetch check backup status", e);
|
logger.log("Unable to fetch check backup status", e);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: e,
|
error: e,
|
||||||
|
@ -106,13 +122,13 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadBackupStatus() {
|
private async loadBackupStatus(): Promise<void> {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
this._getUpdatedDiagnostics();
|
this.getUpdatedDiagnostics();
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -121,7 +137,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.log("Unable to fetch key backup status", e);
|
logger.log("Unable to fetch key backup status", e);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: e,
|
error: e,
|
||||||
|
@ -131,7 +147,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getUpdatedDiagnostics() {
|
private async getUpdatedDiagnostics(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const secretStorage = cli.crypto.secretStorage;
|
const secretStorage = cli.crypto.secretStorage;
|
||||||
|
|
||||||
|
@ -142,7 +158,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||||
const secretStorageReady = await cli.isSecretStorageReady();
|
const secretStorageReady = await cli.isSecretStorageReady();
|
||||||
|
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
backupKeyStored,
|
backupKeyStored,
|
||||||
backupKeyCached,
|
backupKeyCached,
|
||||||
|
@ -152,18 +168,18 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_startNewBackup = () => {
|
private startNewBackup = (): void => {
|
||||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||||
{
|
{
|
||||||
onFinished: () => {
|
onFinished: () => {
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
},
|
},
|
||||||
}, null, /* priority = */ false, /* static = */ true,
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
_deleteBackup = () => {
|
private deleteBackup = (): void => {
|
||||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||||
title: _t('Delete Backup'),
|
title: _t('Delete Backup'),
|
||||||
description: _t(
|
description: _t(
|
||||||
|
@ -176,33 +192,33 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
if (!proceed) return;
|
if (!proceed) return;
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
|
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_restoreBackup = async () => {
|
private restoreBackup = async (): Promise<void> => {
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||||
/* priority = */ false, /* static = */ true,
|
/* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
_resetSecretStorage = async () => {
|
private resetSecretStorage = async (): Promise<void> => {
|
||||||
this.setState({ error: null });
|
this.setState({ error: null });
|
||||||
try {
|
try {
|
||||||
await accessSecretStorage(() => { }, /* forceReset = */ true);
|
await accessSecretStorage(async () => { }, /* forceReset = */ true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error resetting secret storage", e);
|
console.error("Error resetting secret storage", e);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({ error: e });
|
this.setState({ error: e });
|
||||||
}
|
}
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
@ -263,7 +279,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let backupSigStatuses = backupSigStatus.sigs.map((sig, i) => {
|
let backupSigStatuses: React.ReactNode = backupSigStatus.sigs.map((sig, i) => {
|
||||||
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
||||||
const validity = sub =>
|
const validity = sub =>
|
||||||
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
||||||
|
@ -371,14 +387,14 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
</>;
|
</>;
|
||||||
|
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
|
<AccessibleButton key="restore" kind="primary" onClick={this.restoreBackup}>
|
||||||
{ restoreButtonCaption }
|
{ restoreButtonCaption }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isSecureBackupRequired()) {
|
if (!isSecureBackupRequired()) {
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
|
<AccessibleButton key="delete" kind="danger" onClick={this.deleteBackup}>
|
||||||
{ _t("Delete Backup") }
|
{ _t("Delete Backup") }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
|
@ -392,7 +408,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
|
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
|
||||||
</>;
|
</>;
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
|
<AccessibleButton key="setup" kind="primary" onClick={this.startNewBackup}>
|
||||||
{ _t("Set up") }
|
{ _t("Set up") }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
|
@ -400,7 +416,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
|
|
||||||
if (secretStorageKeyInAccount) {
|
if (secretStorageKeyInAccount) {
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
|
<AccessibleButton key="reset" kind="danger" onClick={this.resetSecretStorage}>
|
||||||
{ _t("Reset") }
|
{ _t("Reset") }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
|
@ -16,16 +16,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import Field from "../../elements/Field";
|
import Field from "../../elements/Field";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
import * as Email from "../../../../email";
|
import * as Email from "../../../../email";
|
||||||
import AddThreepid from "../../../../AddThreepid";
|
import AddThreepid from "../../../../AddThreepid";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -39,42 +39,45 @@ places to communicate errors - these should be replaced with inline validation w
|
||||||
that is available.
|
that is available.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class ExistingEmailAddress extends React.Component {
|
interface IExistingEmailAddressProps {
|
||||||
static propTypes = {
|
email: IThreepid;
|
||||||
email: PropTypes.object.isRequired,
|
onRemoved: (emails: IThreepid) => void;
|
||||||
onRemoved: PropTypes.func.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
interface IExistingEmailAddressState {
|
||||||
super();
|
verifyRemove: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExistingEmailAddress extends React.Component<IExistingEmailAddressProps, IExistingEmailAddressState> {
|
||||||
|
constructor(props: IExistingEmailAddressProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
verifyRemove: false,
|
verifyRemove: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemove = (e) => {
|
private onRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: true });
|
this.setState({ verifyRemove: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDontRemove = (e) => {
|
private onDontRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: false });
|
this.setState({ verifyRemove: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onActuallyRemove = (e) => {
|
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
|
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
|
||||||
return this.props.onRemoved(this.props.email);
|
return this.props.onRemoved(this.props.email);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to remove contact information: " + err);
|
console.error("Unable to remove contact information: " + err);
|
||||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||||
title: _t("Unable to remove contact information"),
|
title: _t("Unable to remove contact information"),
|
||||||
|
@ -83,7 +86,7 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.verifyRemove) {
|
if (this.state.verifyRemove) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingEmailAddress">
|
<div className="mx_ExistingEmailAddress">
|
||||||
|
@ -91,14 +94,14 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
{ _t("Remove %(email)s?", { email: this.props.email.address } ) }
|
{ _t("Remove %(email)s?", { email: this.props.email.address } ) }
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onActuallyRemove}
|
onClick={this.onActuallyRemove}
|
||||||
kind="danger_sm"
|
kind="danger_sm"
|
||||||
className="mx_ExistingEmailAddress_confirmBtn"
|
className="mx_ExistingEmailAddress_confirmBtn"
|
||||||
>
|
>
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onDontRemove}
|
onClick={this.onDontRemove}
|
||||||
kind="link_sm"
|
kind="link_sm"
|
||||||
className="mx_ExistingEmailAddress_confirmBtn"
|
className="mx_ExistingEmailAddress_confirmBtn"
|
||||||
>
|
>
|
||||||
|
@ -111,7 +114,7 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingEmailAddress">
|
<div className="mx_ExistingEmailAddress">
|
||||||
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
|
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
|
||||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -119,14 +122,21 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.account.EmailAddresses")
|
interface IProps {
|
||||||
export default class EmailAddresses extends React.Component {
|
emails: IThreepid[];
|
||||||
static propTypes = {
|
onEmailsChange: (emails: Partial<IThreepid>[]) => void;
|
||||||
emails: PropTypes.array.isRequired,
|
}
|
||||||
onEmailsChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
interface IState {
|
||||||
|
verifying: boolean;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
newEmailAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.settings.account.EmailAddresses")
|
||||||
|
export default class EmailAddresses extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -137,24 +147,23 @@ export default class EmailAddresses extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemoved = (address) => {
|
private onRemoved = (address): void => {
|
||||||
const emails = this.props.emails.filter((e) => e !== address);
|
const emails = this.props.emails.filter((e) => e !== address);
|
||||||
this.props.onEmailsChange(emails);
|
this.props.onEmailsChange(emails);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onChangeNewEmailAddress = (e) => {
|
private onChangeNewEmailAddress = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newEmailAddress: e.target.value,
|
newEmailAddress: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAddClick = (e) => {
|
private onAddClick = (e: React.FormEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.newEmailAddress) return;
|
if (!this.state.newEmailAddress) return;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const email = this.state.newEmailAddress;
|
const email = this.state.newEmailAddress;
|
||||||
|
|
||||||
// TODO: Inline field validation
|
// TODO: Inline field validation
|
||||||
|
@ -181,7 +190,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onContinueClick = (e) => {
|
private onContinueClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -192,7 +201,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
const email = this.state.newEmailAddress;
|
const email = this.state.newEmailAddress;
|
||||||
const emails = [
|
const emails = [
|
||||||
...this.props.emails,
|
...this.props.emails,
|
||||||
{ address: email, medium: "email" },
|
{ address: email, medium: ThreepidMedium.Email },
|
||||||
];
|
];
|
||||||
this.props.onEmailsChange(emails);
|
this.props.onEmailsChange(emails);
|
||||||
newEmailAddress = "";
|
newEmailAddress = "";
|
||||||
|
@ -205,7 +214,6 @@ export default class EmailAddresses extends React.Component {
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||||
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
|
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
|
||||||
title: _t("Your email address hasn't been verified yet"),
|
title: _t("Your email address hasn't been verified yet"),
|
||||||
|
@ -222,13 +230,13 @@ export default class EmailAddresses extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const existingEmailElements = this.props.emails.map((e) => {
|
const existingEmailElements = this.props.emails.map((e) => {
|
||||||
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
|
return <ExistingEmailAddress email={e} onRemoved={this.onRemoved} key={e.address} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
let addButton = (
|
let addButton = (
|
||||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||||
{ _t("Add") }
|
{ _t("Add") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -237,7 +245,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
<div>
|
<div>
|
||||||
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
|
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onContinueClick}
|
onClick={this.onContinueClick}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.state.continueDisabled}
|
||||||
>
|
>
|
||||||
|
@ -251,7 +259,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
<div className="mx_EmailAddresses">
|
<div className="mx_EmailAddresses">
|
||||||
{ existingEmailElements }
|
{ existingEmailElements }
|
||||||
<form
|
<form
|
||||||
onSubmit={this._onAddClick}
|
onSubmit={this.onAddClick}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
noValidate={true}
|
noValidate={true}
|
||||||
className="mx_EmailAddresses_new"
|
className="mx_EmailAddresses_new"
|
||||||
|
@ -262,7 +270,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={this.state.verifying}
|
disabled={this.state.verifying}
|
||||||
value={this.state.newEmailAddress}
|
value={this.state.newEmailAddress}
|
||||||
onChange={this._onChangeNewEmailAddress}
|
onChange={this.onChangeNewEmailAddress}
|
||||||
/>
|
/>
|
||||||
{ addButton }
|
{ addButton }
|
||||||
</form>
|
</form>
|
|
@ -16,16 +16,17 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import Field from "../../elements/Field";
|
import Field from "../../elements/Field";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
import AddThreepid from "../../../../AddThreepid";
|
import AddThreepid from "../../../../AddThreepid";
|
||||||
import CountryDropdown from "../../auth/CountryDropdown";
|
import CountryDropdown from "../../auth/CountryDropdown";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import { PhoneNumberCountryDefinition } from "../../../../phonenumber";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -34,42 +35,45 @@ This is a copy/paste of EmailAddresses, mostly.
|
||||||
|
|
||||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||||
|
|
||||||
export class ExistingPhoneNumber extends React.Component {
|
interface IExistingPhoneNumberProps {
|
||||||
static propTypes = {
|
msisdn: IThreepid;
|
||||||
msisdn: PropTypes.object.isRequired,
|
onRemoved: (phoneNumber: IThreepid) => void;
|
||||||
onRemoved: PropTypes.func.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
interface IExistingPhoneNumberState {
|
||||||
super();
|
verifyRemove: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberProps, IExistingPhoneNumberState> {
|
||||||
|
constructor(props: IExistingPhoneNumberProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
verifyRemove: false,
|
verifyRemove: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemove = (e) => {
|
private onRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: true });
|
this.setState({ verifyRemove: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDontRemove = (e) => {
|
private onDontRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: false });
|
this.setState({ verifyRemove: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onActuallyRemove = (e) => {
|
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
|
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
|
||||||
return this.props.onRemoved(this.props.msisdn);
|
return this.props.onRemoved(this.props.msisdn);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to remove contact information: " + err);
|
console.error("Unable to remove contact information: " + err);
|
||||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||||
title: _t("Unable to remove contact information"),
|
title: _t("Unable to remove contact information"),
|
||||||
|
@ -78,7 +82,7 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.verifyRemove) {
|
if (this.state.verifyRemove) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingPhoneNumber">
|
<div className="mx_ExistingPhoneNumber">
|
||||||
|
@ -86,14 +90,14 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
|
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onActuallyRemove}
|
onClick={this.onActuallyRemove}
|
||||||
kind="danger_sm"
|
kind="danger_sm"
|
||||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||||
>
|
>
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onDontRemove}
|
onClick={this.onDontRemove}
|
||||||
kind="link_sm"
|
kind="link_sm"
|
||||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||||
>
|
>
|
||||||
|
@ -106,7 +110,7 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingPhoneNumber">
|
<div className="mx_ExistingPhoneNumber">
|
||||||
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
|
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
|
||||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,19 +118,30 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.account.PhoneNumbers")
|
interface IProps {
|
||||||
export default class PhoneNumbers extends React.Component {
|
msisdns: IThreepid[];
|
||||||
static propTypes = {
|
onMsisdnsChange: (phoneNumbers: Partial<IThreepid>[]) => void;
|
||||||
msisdns: PropTypes.array.isRequired,
|
}
|
||||||
onMsisdnsChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
interface IState {
|
||||||
|
verifying: boolean;
|
||||||
|
verifyError: string;
|
||||||
|
verifyMsisdn: string;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
phoneCountry: string;
|
||||||
|
newPhoneNumber: string;
|
||||||
|
newPhoneNumberCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.settings.account.PhoneNumbers")
|
||||||
|
export default class PhoneNumbers extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
verifying: false,
|
verifying: false,
|
||||||
verifyError: false,
|
verifyError: null,
|
||||||
verifyMsisdn: "",
|
verifyMsisdn: "",
|
||||||
addTask: null,
|
addTask: null,
|
||||||
continueDisabled: false,
|
continueDisabled: false,
|
||||||
|
@ -136,30 +151,29 @@ export default class PhoneNumbers extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemoved = (address) => {
|
private onRemoved = (address: IThreepid): void => {
|
||||||
const msisdns = this.props.msisdns.filter((e) => e !== address);
|
const msisdns = this.props.msisdns.filter((e) => e !== address);
|
||||||
this.props.onMsisdnsChange(msisdns);
|
this.props.onMsisdnsChange(msisdns);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onChangeNewPhoneNumber = (e) => {
|
private onChangeNewPhoneNumber = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPhoneNumber: e.target.value,
|
newPhoneNumber: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onChangeNewPhoneNumberCode = (e) => {
|
private onChangeNewPhoneNumberCode = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPhoneNumberCode: e.target.value,
|
newPhoneNumberCode: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAddClick = (e) => {
|
private onAddClick = (e: React.MouseEvent | React.FormEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.newPhoneNumber) return;
|
if (!this.state.newPhoneNumber) return;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const phoneNumber = this.state.newPhoneNumber;
|
const phoneNumber = this.state.newPhoneNumber;
|
||||||
const phoneCountry = this.state.phoneCountry;
|
const phoneCountry = this.state.phoneCountry;
|
||||||
|
|
||||||
|
@ -178,7 +192,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onContinueClick = (e) => {
|
private onContinueClick = (e: React.MouseEvent | React.FormEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -190,7 +204,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
const msisdns = [
|
const msisdns = [
|
||||||
...this.props.msisdns,
|
...this.props.msisdns,
|
||||||
{ address, medium: "msisdn" },
|
{ address, medium: ThreepidMedium.Phone },
|
||||||
];
|
];
|
||||||
this.props.onMsisdnsChange(msisdns);
|
this.props.onMsisdnsChange(msisdns);
|
||||||
newPhoneNumber = "";
|
newPhoneNumber = "";
|
||||||
|
@ -207,7 +221,6 @@ export default class PhoneNumbers extends React.Component {
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to verify phone number: " + err);
|
console.error("Unable to verify phone number: " + err);
|
||||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify phone number."),
|
title: _t("Unable to verify phone number."),
|
||||||
|
@ -219,17 +232,17 @@ export default class PhoneNumbers extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCountryChanged = (e) => {
|
private onCountryChanged = (country: PhoneNumberCountryDefinition): void => {
|
||||||
this.setState({ phoneCountry: e.iso2 });
|
this.setState({ phoneCountry: country.iso2 });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const existingPhoneElements = this.props.msisdns.map((p) => {
|
const existingPhoneElements = this.props.msisdns.map((p) => {
|
||||||
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />;
|
return <ExistingPhoneNumber msisdn={p} onRemoved={this.onRemoved} key={p.address} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
let addVerifySection = (
|
let addVerifySection = (
|
||||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||||
{ _t("Add") }
|
{ _t("Add") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -243,17 +256,17 @@ export default class PhoneNumbers extends React.Component {
|
||||||
<br />
|
<br />
|
||||||
{ this.state.verifyError }
|
{ this.state.verifyError }
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
|
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
label={_t("Verification code")}
|
label={_t("Verification code")}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.state.continueDisabled}
|
||||||
value={this.state.newPhoneNumberCode}
|
value={this.state.newPhoneNumberCode}
|
||||||
onChange={this._onChangeNewPhoneNumberCode}
|
onChange={this.onChangeNewPhoneNumberCode}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onContinueClick}
|
onClick={this.onContinueClick}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.state.continueDisabled}
|
||||||
>
|
>
|
||||||
|
@ -264,7 +277,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const phoneCountry = <CountryDropdown onOptionChange={this._onCountryChanged}
|
const phoneCountry = <CountryDropdown onOptionChange={this.onCountryChanged}
|
||||||
className="mx_PhoneNumbers_country"
|
className="mx_PhoneNumbers_country"
|
||||||
value={this.state.phoneCountry}
|
value={this.state.phoneCountry}
|
||||||
disabled={this.state.verifying}
|
disabled={this.state.verifying}
|
||||||
|
@ -275,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_PhoneNumbers">
|
<div className="mx_PhoneNumbers">
|
||||||
{ existingPhoneElements }
|
{ existingPhoneElements }
|
||||||
<form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
||||||
<div className="mx_PhoneNumbers_input">
|
<div className="mx_PhoneNumbers_input">
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -284,7 +297,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
disabled={this.state.verifying}
|
disabled={this.state.verifying}
|
||||||
prefixComponent={phoneCountry}
|
prefixComponent={phoneCountry}
|
||||||
value={this.state.newPhoneNumber}
|
value={this.state.newPhoneNumber}
|
||||||
onChange={this._onChangeNewPhoneNumber}
|
onChange={this.onChangeNewPhoneNumber}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
|
@ -16,14 +16,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import AddThreepid from '../../../../AddThreepid';
|
import AddThreepid from '../../../../AddThreepid';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -41,12 +42,19 @@ that is available.
|
||||||
TODO: Reduce all the copying between account vs. discovery components.
|
TODO: Reduce all the copying between account vs. discovery components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class EmailAddress extends React.Component {
|
interface IEmailAddressProps {
|
||||||
static propTypes = {
|
email: IThreepid;
|
||||||
email: PropTypes.object.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
interface IEmailAddressState {
|
||||||
|
verifying: boolean;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
bound: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddressState> {
|
||||||
|
constructor(props: IEmailAddressProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { bound } = props.email;
|
const { bound } = props.email;
|
||||||
|
@ -60,17 +68,17 @@ export class EmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
|
public UNSAFE_componentWillReceiveProps(nextProps: IEmailAddressProps): void {
|
||||||
const { bound } = nextProps.email;
|
const { bound } = nextProps.email;
|
||||||
this.setState({ bound });
|
this.setState({ bound });
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBinding({ bind, label, errorTitle }) {
|
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||||
if (!(await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind())) {
|
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.email;
|
const { medium, address } = this.props.email;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -103,8 +111,7 @@ export class EmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.email;
|
const { medium, address } = this.props.email;
|
||||||
|
|
||||||
const task = new AddThreepid();
|
const task = new AddThreepid();
|
||||||
|
@ -139,7 +146,7 @@ export class EmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRevokeClick = (e) => {
|
private onRevokeClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -147,9 +154,9 @@ export class EmailAddress extends React.Component {
|
||||||
label: "revoke",
|
label: "revoke",
|
||||||
errorTitle: _t("Unable to revoke sharing for email address"),
|
errorTitle: _t("Unable to revoke sharing for email address"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onShareClick = (e) => {
|
private onShareClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -157,9 +164,9 @@ export class EmailAddress extends React.Component {
|
||||||
label: "share",
|
label: "share",
|
||||||
errorTitle: _t("Unable to share email address"),
|
errorTitle: _t("Unable to share email address"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onContinueClick = async (e) => {
|
private onContinueClick = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -173,7 +180,6 @@ export class EmailAddress extends React.Component {
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||||
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
|
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
|
||||||
title: _t("Your email address hasn't been verified yet"),
|
title: _t("Your email address hasn't been verified yet"),
|
||||||
|
@ -188,10 +194,9 @@ export class EmailAddress extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const { address } = this.props.email;
|
const { address } = this.props.email;
|
||||||
const { verifying, bound } = this.state;
|
const { verifying, bound } = this.state;
|
||||||
|
|
||||||
|
@ -234,14 +239,13 @@ export class EmailAddress extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
interface IProps {
|
||||||
|
emails: IThreepid[];
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.discovery.EmailAddresses")
|
@replaceableComponent("views.settings.discovery.EmailAddresses")
|
||||||
export default class EmailAddresses extends React.Component {
|
export default class EmailAddresses extends React.Component<IProps> {
|
||||||
static propTypes = {
|
public render(): JSX.Element {
|
||||||
emails: PropTypes.array.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let content;
|
let content;
|
||||||
if (this.props.emails.length > 0) {
|
if (this.props.emails.length > 0) {
|
||||||
content = this.props.emails.map((e) => {
|
content = this.props.emails.map((e) => {
|
|
@ -16,14 +16,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import AddThreepid from '../../../../AddThreepid';
|
import AddThreepid from '../../../../AddThreepid';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import Field from "../../elements/Field";
|
||||||
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -32,12 +34,21 @@ This is a copy/paste of EmailAddresses, mostly.
|
||||||
|
|
||||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||||
|
|
||||||
export class PhoneNumber extends React.Component {
|
interface IPhoneNumberProps {
|
||||||
static propTypes = {
|
msisdn: IThreepid;
|
||||||
msisdn: PropTypes.object.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
interface IPhoneNumberState {
|
||||||
|
verifying: boolean;
|
||||||
|
verificationCode: string;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
bound: boolean;
|
||||||
|
verifyError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumberState> {
|
||||||
|
constructor(props: IPhoneNumberProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { bound } = props.msisdn;
|
const { bound } = props.msisdn;
|
||||||
|
@ -48,21 +59,22 @@ export class PhoneNumber extends React.Component {
|
||||||
addTask: null,
|
addTask: null,
|
||||||
continueDisabled: false,
|
continueDisabled: false,
|
||||||
bound,
|
bound,
|
||||||
|
verifyError: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
|
public UNSAFE_componentWillReceiveProps(nextProps: IPhoneNumberProps): void {
|
||||||
const { bound } = nextProps.msisdn;
|
const { bound } = nextProps.msisdn;
|
||||||
this.setState({ bound });
|
this.setState({ bound });
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBinding({ bind, label, errorTitle }) {
|
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||||
if (!(await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind())) {
|
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.msisdn;
|
const { medium, address } = this.props.msisdn;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -99,8 +111,7 @@ export class PhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.msisdn;
|
const { medium, address } = this.props.msisdn;
|
||||||
|
|
||||||
const task = new AddThreepid();
|
const task = new AddThreepid();
|
||||||
|
@ -139,7 +150,7 @@ export class PhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRevokeClick = (e) => {
|
private onRevokeClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -147,9 +158,9 @@ export class PhoneNumber extends React.Component {
|
||||||
label: "revoke",
|
label: "revoke",
|
||||||
errorTitle: _t("Unable to revoke sharing for phone number"),
|
errorTitle: _t("Unable to revoke sharing for phone number"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onShareClick = (e) => {
|
private onShareClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -157,15 +168,15 @@ export class PhoneNumber extends React.Component {
|
||||||
label: "share",
|
label: "share",
|
||||||
errorTitle: _t("Unable to share phone number"),
|
errorTitle: _t("Unable to share phone number"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onVerificationCodeChange = (e) => {
|
private onVerificationCodeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
verificationCode: e.target.value,
|
verificationCode: e.target.value,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onContinueClick = async (e) => {
|
private onContinueClick = async (e: React.MouseEvent | React.FormEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -183,7 +194,6 @@ export class PhoneNumber extends React.Component {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to verify phone number: " + err);
|
console.error("Unable to verify phone number: " + err);
|
||||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify phone number."),
|
title: _t("Unable to verify phone number."),
|
||||||
|
@ -193,11 +203,9 @@ export class PhoneNumber extends React.Component {
|
||||||
this.setState({ verifyError: _t("Incorrect verification code") });
|
this.setState({ verifyError: _t("Incorrect verification code") });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const Field = sdk.getComponent('elements.Field');
|
|
||||||
const { address } = this.props.msisdn;
|
const { address } = this.props.msisdn;
|
||||||
const { verifying, bound } = this.state;
|
const { verifying, bound } = this.state;
|
||||||
|
|
||||||
|
@ -247,13 +255,13 @@ export class PhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
interface IProps {
|
||||||
export default class PhoneNumbers extends React.Component {
|
msisdns: IThreepid[];
|
||||||
static propTypes = {
|
}
|
||||||
msisdns: PropTypes.array.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
||||||
|
export default class PhoneNumbers extends React.Component<IProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
let content;
|
let content;
|
||||||
if (this.props.msisdns.length > 0) {
|
if (this.props.msisdns.length > 0) {
|
||||||
content = this.props.msisdns.map((e) => {
|
content = this.props.msisdns.map((e) => {
|
|
@ -15,45 +15,46 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||||
import * as sdk from "../../../../..";
|
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
|
||||||
|
import RelatedGroupSettings from "../../../room_settings/RelatedGroupSettings";
|
||||||
|
import AliasSettings from "../../../room_settings/AliasSettings";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isRoomPublished: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
|
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
|
||||||
export default class GeneralRoomSettingsTab extends React.Component {
|
export default class GeneralRoomSettingsTab extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
public static contextType = MatrixClientContext;
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRoomPublished: false, // loaded async
|
isRoomPublished: false, // loaded async
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLeaveClick = () => {
|
private onLeaveClick = (): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'leave_room',
|
action: 'leave_room',
|
||||||
room_id: this.props.roomId,
|
room_id: this.props.roomId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
|
||||||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
|
||||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
|
||||||
|
|
||||||
const client = this.context;
|
const client = this.context;
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
||||||
|
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
||||||
<div className='mx_SettingsTab_section'>
|
<div className='mx_SettingsTab_section'>
|
||||||
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
|
<AccessibleButton kind='danger' onClick={this.onLeaveClick}>
|
||||||
{ _t('Leave room') }
|
{ _t('Leave room') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
|
@ -24,16 +23,21 @@ import SettingsStore from '../../../../../settings/SettingsStore';
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
currentSound: string;
|
||||||
|
uploadedFile: File;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
|
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
|
||||||
export default class NotificationsSettingsTab extends React.Component {
|
export default class NotificationsSettingsTab extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private soundUpload = createRef<HTMLInputElement>();
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
_soundUpload = createRef();
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentSound: "default",
|
currentSound: "default",
|
||||||
|
@ -42,7 +46,8 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
|
public UNSAFE_componentWillMount(): void {
|
||||||
const soundData = Notifier.getSoundForRoom(this.props.roomId);
|
const soundData = Notifier.getSoundForRoom(this.props.roomId);
|
||||||
if (!soundData) {
|
if (!soundData) {
|
||||||
return;
|
return;
|
||||||
|
@ -50,14 +55,14 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
this.setState({ currentSound: soundData.name || soundData.url });
|
this.setState({ currentSound: soundData.name || soundData.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _triggerUploader(e) {
|
private triggerUploader = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this._soundUpload.current.click();
|
this.soundUpload.current.click();
|
||||||
}
|
};
|
||||||
|
|
||||||
async _onSoundUploadChanged(e) {
|
private onSoundUploadChanged = (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||||
if (!e.target.files || !e.target.files.length) {
|
if (!e.target.files || !e.target.files.length) {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadedFile: null,
|
uploadedFile: null,
|
||||||
|
@ -69,23 +74,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadedFile: file,
|
uploadedFile: file,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
async _onClickSaveSound(e) {
|
private onClickSaveSound = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._saveSound();
|
await this.saveSound();
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.error(
|
console.error(
|
||||||
`Unable to save notification sound for ${this.props.roomId}`,
|
`Unable to save notification sound for ${this.props.roomId}`,
|
||||||
);
|
);
|
||||||
console.error(ex);
|
console.error(ex);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async _saveSound() {
|
private async saveSound(): Promise<void> {
|
||||||
if (!this.state.uploadedFile) {
|
if (!this.state.uploadedFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -122,7 +127,7 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearSound(e) {
|
private clearSound = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
SettingsStore.setValue(
|
SettingsStore.setValue(
|
||||||
|
@ -135,9 +140,9 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentSound: "default",
|
currentSound: "default",
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let currentUploadedFile = null;
|
let currentUploadedFile = null;
|
||||||
if (this.state.uploadedFile) {
|
if (this.state.uploadedFile) {
|
||||||
currentUploadedFile = (
|
currentUploadedFile = (
|
||||||
|
@ -154,23 +159,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
|
||||||
<div>
|
<div>
|
||||||
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
|
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
|
||||||
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
|
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this.clearSound} kind="primary">
|
||||||
{ _t("Reset") }
|
{ _t("Reset") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("Set a new custom sound") }</h3>
|
<h3>{ _t("Set a new custom sound") }</h3>
|
||||||
<form autoComplete="off" noValidate={true}>
|
<form autoComplete="off" noValidate={true}>
|
||||||
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
<input ref={this.soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this.onSoundUploadChanged} accept="audio/*" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{ currentUploadedFile }
|
{ currentUploadedFile }
|
||||||
|
|
||||||
<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
|
<AccessibleButton className="mx_NotificationSound_browse" onClick={this.triggerUploader} kind="primary">
|
||||||
{ _t("Browse") }
|
{ _t("Browse") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
||||||
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
|
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this.onClickSaveSound} kind="primary">
|
||||||
{ _t("Save") }
|
{ _t("Save") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<br />
|
<br />
|
|
@ -25,13 +25,11 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||||
import SpellCheckSettings from "../../SpellCheckSettings";
|
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import PlatformPeg from "../../../../../PlatformPeg";
|
import PlatformPeg from "../../../../../PlatformPeg";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import * as sdk from "../../../../..";
|
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import { Service, startTermsFlow } from "../../../../../Terms";
|
import { Policies, Service, startTermsFlow } from "../../../../../Terms";
|
||||||
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||||
import IdentityAuthClient from "../../../../../IdentityAuthClient";
|
import IdentityAuthClient from "../../../../../IdentityAuthClient";
|
||||||
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
|
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
|
||||||
|
@ -40,15 +38,50 @@ import Spinner from "../../../elements/Spinner";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
|
import ErrorDialog from "../../../dialogs/ErrorDialog";
|
||||||
|
import AccountPhoneNumbers from "../../account/PhoneNumbers";
|
||||||
|
import AccountEmailAddresses from "../../account/EmailAddresses";
|
||||||
|
import DiscoveryEmailAddresses from "../../discovery/EmailAddresses";
|
||||||
|
import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers";
|
||||||
|
import ChangePassword from "../../ChangePassword";
|
||||||
|
import InlineTermsAgreement from "../../../terms/InlineTermsAgreement";
|
||||||
|
import SetIdServer from "../../SetIdServer";
|
||||||
|
import SetIntegrationManager from "../../SetIntegrationManager";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
closeSettingsFn: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
language: string;
|
||||||
|
spellCheckLanguages: string[];
|
||||||
|
haveIdServer: boolean;
|
||||||
|
serverSupportsSeparateAddAndBind: boolean;
|
||||||
|
idServerHasUnsignedTerms: boolean;
|
||||||
|
requiredPolicyInfo: { // This object is passed along to a component for handling
|
||||||
|
hasTerms: boolean;
|
||||||
|
policiesAndServices: {
|
||||||
|
service: Service;
|
||||||
|
policies: Policies;
|
||||||
|
}[]; // From the startTermsFlow callback
|
||||||
|
agreedUrls: string[]; // From the startTermsFlow callback
|
||||||
|
resolve: (values: string[]) => void; // Promise resolve function for startTermsFlow callback
|
||||||
|
};
|
||||||
|
emails: IThreepid[];
|
||||||
|
msisdns: IThreepid[];
|
||||||
|
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
|
||||||
|
canChangePassword: boolean;
|
||||||
|
idServerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
|
@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
|
||||||
export default class GeneralUserSettingsTab extends React.Component {
|
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private readonly dispatcherRef: string;
|
||||||
closeSettingsFn: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
constructor(props: IProps) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
language: languageHandler.getCurrentLanguage(),
|
language: languageHandler.getCurrentLanguage(),
|
||||||
|
@ -58,20 +91,23 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
idServerHasUnsignedTerms: false,
|
idServerHasUnsignedTerms: false,
|
||||||
requiredPolicyInfo: { // This object is passed along to a component for handling
|
requiredPolicyInfo: { // This object is passed along to a component for handling
|
||||||
hasTerms: false,
|
hasTerms: false,
|
||||||
// policiesAndServices, // From the startTermsFlow callback
|
policiesAndServices: null, // From the startTermsFlow callback
|
||||||
// agreedUrls, // From the startTermsFlow callback
|
agreedUrls: null, // From the startTermsFlow callback
|
||||||
// resolve, // Promise resolve function for startTermsFlow callback
|
resolve: null, // Promise resolve function for startTermsFlow callback
|
||||||
},
|
},
|
||||||
emails: [],
|
emails: [],
|
||||||
msisdns: [],
|
msisdns: [],
|
||||||
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
||||||
|
canChangePassword: false,
|
||||||
|
idServerName: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move this to constructor
|
// TODO: [REACT-WARNING] Move this to constructor
|
||||||
async UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
|
public async UNSAFE_componentWillMount(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
|
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
|
||||||
|
@ -86,10 +122,10 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
|
|
||||||
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
|
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
|
||||||
|
|
||||||
this._getThreepidState();
|
this.getThreepidState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
public async componentDidMount(): Promise<void> {
|
||||||
const plaf = PlatformPeg.get();
|
const plaf = PlatformPeg.get();
|
||||||
if (plaf) {
|
if (plaf) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -98,30 +134,30 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction = (payload) => {
|
private onAction = (payload: ActionPayload): void => {
|
||||||
if (payload.action === 'id_server_changed') {
|
if (payload.action === 'id_server_changed') {
|
||||||
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
|
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
|
||||||
this._getThreepidState();
|
this.getThreepidState();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onEmailsChange = (emails) => {
|
private onEmailsChange = (emails: IThreepid[]): void => {
|
||||||
this.setState({ emails });
|
this.setState({ emails });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMsisdnsChange = (msisdns) => {
|
private onMsisdnsChange = (msisdns: IThreepid[]): void => {
|
||||||
this.setState({ msisdns });
|
this.setState({ msisdns });
|
||||||
};
|
};
|
||||||
|
|
||||||
async _getThreepidState() {
|
private async getThreepidState(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
// Check to see if terms need accepting
|
// Check to see if terms need accepting
|
||||||
this._checkTerms();
|
this.checkTerms();
|
||||||
|
|
||||||
// Need to get 3PIDs generally for Account section and possibly also for
|
// Need to get 3PIDs generally for Account section and possibly also for
|
||||||
// Discovery (assuming we have an IS and terms are agreed).
|
// Discovery (assuming we have an IS and terms are agreed).
|
||||||
|
@ -143,7 +179,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _checkTerms() {
|
private async checkTerms(): Promise<void> {
|
||||||
if (!this.state.haveIdServer) {
|
if (!this.state.haveIdServer) {
|
||||||
this.setState({ idServerHasUnsignedTerms: false });
|
this.setState({ idServerHasUnsignedTerms: false });
|
||||||
return;
|
return;
|
||||||
|
@ -176,6 +212,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
requiredPolicyInfo: {
|
requiredPolicyInfo: {
|
||||||
hasTerms: false,
|
hasTerms: false,
|
||||||
|
...this.state.requiredPolicyInfo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -187,19 +224,19 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLanguageChange = (newLanguage) => {
|
private onLanguageChange = (newLanguage: string): void => {
|
||||||
if (this.state.language === newLanguage) return;
|
if (this.state.language === newLanguage) return;
|
||||||
|
|
||||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
|
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
|
||||||
this.setState({ language: newLanguage });
|
this.setState({ language: newLanguage });
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
if (platform) {
|
if (platform) {
|
||||||
platform.setLanguage(newLanguage);
|
platform.setLanguage([newLanguage]);
|
||||||
platform.reload();
|
platform.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSpellCheckLanguagesChange = (languages) => {
|
private onSpellCheckLanguagesChange = (languages: string[]): void => {
|
||||||
this.setState({ spellCheckLanguages: languages });
|
this.setState({ spellCheckLanguages: languages });
|
||||||
|
|
||||||
const plaf = PlatformPeg.get();
|
const plaf = PlatformPeg.get();
|
||||||
|
@ -208,7 +245,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPasswordChangeError = (err) => {
|
private onPasswordChangeError = (err): void => {
|
||||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||||
let errMsg = err.error || err.message || "";
|
let errMsg = err.error || err.message || "";
|
||||||
if (err.httpStatus === 403) {
|
if (err.httpStatus === 403) {
|
||||||
|
@ -216,7 +253,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
} else if (!errMsg) {
|
} else if (!errMsg) {
|
||||||
errMsg += ` (HTTP status ${err.httpStatus})`;
|
errMsg += ` (HTTP status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to change password: " + errMsg);
|
console.error("Failed to change password: " + errMsg);
|
||||||
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
|
@ -224,9 +260,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPasswordChanged = () => {
|
private onPasswordChanged = (): void => {
|
||||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
|
||||||
title: _t("Success"),
|
title: _t("Success"),
|
||||||
description: _t(
|
description: _t(
|
||||||
|
@ -236,7 +271,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDeactivateClicked = () => {
|
private onDeactivateClicked = (): void => {
|
||||||
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {
|
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
if (success) this.props.closeSettingsFn();
|
if (success) this.props.closeSettingsFn();
|
||||||
|
@ -244,7 +279,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderProfileSection() {
|
private renderProfileSection(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<ProfileSettings />
|
<ProfileSettings />
|
||||||
|
@ -252,18 +287,14 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderAccountSection() {
|
private renderAccountSection(): JSX.Element {
|
||||||
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
|
||||||
const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses");
|
|
||||||
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
|
|
||||||
|
|
||||||
let passwordChangeForm = (
|
let passwordChangeForm = (
|
||||||
<ChangePassword
|
<ChangePassword
|
||||||
className="mx_GeneralUserSettingsTab_changePassword"
|
className="mx_GeneralUserSettingsTab_changePassword"
|
||||||
rowClassName=""
|
rowClassName=""
|
||||||
buttonKind="primary"
|
buttonKind="primary"
|
||||||
onError={this._onPasswordChangeError}
|
onError={this.onPasswordChangeError}
|
||||||
onFinished={this._onPasswordChanged} />
|
onFinished={this.onPasswordChanged} />
|
||||||
);
|
);
|
||||||
|
|
||||||
let threepidSection = null;
|
let threepidSection = null;
|
||||||
|
@ -278,15 +309,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
) {
|
) {
|
||||||
const emails = this.state.loading3pids
|
const emails = this.state.loading3pids
|
||||||
? <Spinner />
|
? <Spinner />
|
||||||
: <EmailAddresses
|
: <AccountEmailAddresses
|
||||||
emails={this.state.emails}
|
emails={this.state.emails}
|
||||||
onEmailsChange={this._onEmailsChange}
|
onEmailsChange={this.onEmailsChange}
|
||||||
/>;
|
/>;
|
||||||
const msisdns = this.state.loading3pids
|
const msisdns = this.state.loading3pids
|
||||||
? <Spinner />
|
? <Spinner />
|
||||||
: <PhoneNumbers
|
: <AccountPhoneNumbers
|
||||||
msisdns={this.state.msisdns}
|
msisdns={this.state.msisdns}
|
||||||
onMsisdnsChange={this._onMsisdnsChange}
|
onMsisdnsChange={this.onMsisdnsChange}
|
||||||
/>;
|
/>;
|
||||||
threepidSection = <div>
|
threepidSection = <div>
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
||||||
|
@ -318,37 +349,34 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderLanguageSection() {
|
private renderLanguageSection(): JSX.Element {
|
||||||
// TODO: Convert to new-styled Field
|
// TODO: Convert to new-styled Field
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
|
||||||
<LanguageDropdown
|
<LanguageDropdown
|
||||||
className="mx_GeneralUserSettingsTab_languageInput"
|
className="mx_GeneralUserSettingsTab_languageInput"
|
||||||
onOptionChange={this._onLanguageChange}
|
onOptionChange={this.onLanguageChange}
|
||||||
value={this.state.language}
|
value={this.state.language}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderSpellCheckSection() {
|
private renderSpellCheckSection(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
|
||||||
<SpellCheckSettings
|
<SpellCheckSettings
|
||||||
languages={this.state.spellCheckLanguages}
|
languages={this.state.spellCheckLanguages}
|
||||||
onLanguagesChange={this._onSpellCheckLanguagesChange}
|
onLanguagesChange={this.onSpellCheckLanguagesChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderDiscoverySection() {
|
private renderDiscoverySection(): JSX.Element {
|
||||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
|
||||||
|
|
||||||
if (this.state.requiredPolicyInfo.hasTerms) {
|
if (this.state.requiredPolicyInfo.hasTerms) {
|
||||||
const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
|
|
||||||
const intro = <span className="mx_SettingsTab_subsectionText">
|
const intro = <span className="mx_SettingsTab_subsectionText">
|
||||||
{ _t(
|
{ _t(
|
||||||
"Agree to the identity server (%(serverName)s) Terms of Service to " +
|
"Agree to the identity server (%(serverName)s) Terms of Service to " +
|
||||||
|
@ -370,11 +398,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses");
|
const emails = this.state.loading3pids ? <Spinner /> : <DiscoveryEmailAddresses emails={this.state.emails} />;
|
||||||
const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers");
|
const msisdns = this.state.loading3pids ? <Spinner /> : <DiscoveryPhoneNumbers msisdns={this.state.msisdns} />;
|
||||||
|
|
||||||
const emails = this.state.loading3pids ? <Spinner /> : <EmailAddresses emails={this.state.emails} />;
|
|
||||||
const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
|
|
||||||
|
|
||||||
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
|
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
||||||
|
@ -388,12 +413,12 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
{ threepidSection }
|
{ threepidSection }
|
||||||
{ /* has its own heading as it includes the current identity server */ }
|
{ /* has its own heading as it includes the current identity server */ }
|
||||||
<SetIdServer />
|
<SetIdServer missingTerms={false} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderManagementSection() {
|
private renderManagementSection(): JSX.Element {
|
||||||
// TODO: Improve warning text for account deactivation
|
// TODO: Improve warning text for account deactivation
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
|
@ -401,18 +426,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
<span className="mx_SettingsTab_subsectionText">
|
<span className="mx_SettingsTab_subsectionText">
|
||||||
{ _t("Deactivating your account is a permanent action - be careful!") }
|
{ _t("Deactivating your account is a permanent action - be careful!") }
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
|
<AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
|
||||||
{ _t("Deactivate Account") }
|
{ _t("Deactivate Account") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderIntegrationManagerSection() {
|
private renderIntegrationManagerSection(): JSX.Element {
|
||||||
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
||||||
|
|
||||||
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
{ /* has its own heading as it includes the current integration manager */ }
|
{ /* has its own heading as it includes the current integration manager */ }
|
||||||
|
@ -421,7 +444,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const plaf = PlatformPeg.get();
|
const plaf = PlatformPeg.get();
|
||||||
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
||||||
|
|
||||||
|
@ -439,7 +462,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
if (SettingsStore.getValue(UIFeature.Deactivate)) {
|
if (SettingsStore.getValue(UIFeature.Deactivate)) {
|
||||||
accountManagementSection = <>
|
accountManagementSection = <>
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
|
||||||
{ this._renderManagementSection() }
|
{ this.renderManagementSection() }
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,19 +470,19 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
|
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||||
discoverySection = <>
|
discoverySection = <>
|
||||||
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
|
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
|
||||||
{ this._renderDiscoverySection() }
|
{ this.renderDiscoverySection() }
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab">
|
<div className="mx_SettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||||
{ this._renderProfileSection() }
|
{ this.renderProfileSection() }
|
||||||
{ this._renderAccountSection() }
|
{ this.renderAccountSection() }
|
||||||
{ this._renderLanguageSection() }
|
{ this.renderLanguageSection() }
|
||||||
{ supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null }
|
{ supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null }
|
||||||
{ discoverySection }
|
{ discoverySection }
|
||||||
{ this._renderIntegrationManagerSection() /* Has its own title */ }
|
{ this.renderIntegrationManagerSection() /* Has its own title */ }
|
||||||
{ accountManagementSection }
|
{ accountManagementSection }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
|
@ -26,28 +25,32 @@ import BetaCard from "../../../beta/BetaCard";
|
||||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||||
|
|
||||||
export class LabsSettingToggle extends React.Component {
|
interface ILabsSettingToggleProps {
|
||||||
static propTypes = {
|
featureId: string;
|
||||||
featureId: PropTypes.string.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
_onChange = async (checked) => {
|
export class LabsSettingToggle extends React.Component<ILabsSettingToggleProps> {
|
||||||
|
private onChange = async (checked: boolean): Promise<void> => {
|
||||||
await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
|
await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const label = SettingsStore.getDisplayName(this.props.featureId);
|
const label = SettingsStore.getDisplayName(this.props.featureId);
|
||||||
const value = SettingsStore.getValue(this.props.featureId);
|
const value = SettingsStore.getValue(this.props.featureId);
|
||||||
const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
|
const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
|
||||||
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} disabled={!canChange} />;
|
return <LabelledToggleSwitch value={value} label={label} onChange={this.onChange} disabled={!canChange} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
showHiddenReadReceipts: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.LabsUserSettingsTab")
|
@replaceableComponent("views.settings.tabs.user.LabsUserSettingsTab")
|
||||||
export default class LabsUserSettingsTab extends React.Component {
|
export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||||
constructor() {
|
constructor(props: {}) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
||||||
this.setState({ showHiddenReadReceipts });
|
this.setState({ showHiddenReadReceipts });
|
||||||
|
@ -58,7 +61,7 @@ export default class LabsUserSettingsTab extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const features = SettingsStore.getFeatureSettingNames();
|
const features = SettingsStore.getFeatureSettingNames();
|
||||||
const [labs, betas] = features.reduce((arr, f) => {
|
const [labs, betas] = features.reduce((arr, f) => {
|
||||||
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
|
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
|
@ -26,34 +25,40 @@ import * as FormattingUtils from "../../../../../utils/FormattingUtils";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import Analytics from "../../../../../Analytics";
|
import Analytics from "../../../../../Analytics";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import * as sdk from "../../../../..";
|
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import { privateShouldBeEncrypted } from "../../../../../createRoom";
|
import { privateShouldBeEncrypted } from "../../../../../createRoom";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import SecureBackupPanel from "../../SecureBackupPanel";
|
import SecureBackupPanel from "../../SecureBackupPanel";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||||
import CountlyAnalytics from "../../../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../../../CountlyAnalytics";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||||
|
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import DevicesPanel from "../../DevicesPanel";
|
||||||
|
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||||
|
import CrossSigningPanel from "../../CrossSigningPanel";
|
||||||
|
import EventIndexPanel from "../../EventIndexPanel";
|
||||||
|
import InlineSpinner from "../../../elements/InlineSpinner";
|
||||||
|
|
||||||
export class IgnoredUser extends React.Component {
|
interface IIgnoredUserProps {
|
||||||
static propTypes = {
|
userId: string;
|
||||||
userId: PropTypes.string.isRequired,
|
onUnignored: (userId: string) => void;
|
||||||
onUnignored: PropTypes.func.isRequired,
|
inProgress: boolean;
|
||||||
inProgress: PropTypes.bool.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
_onUnignoreClicked = (e) => {
|
export class IgnoredUser extends React.Component<IIgnoredUserProps> {
|
||||||
|
private onUnignoreClicked = (): void => {
|
||||||
this.props.onUnignored(this.props.userId);
|
this.props.onUnignored(this.props.userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
|
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
|
||||||
return (
|
return (
|
||||||
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
|
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
|
||||||
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}>
|
<AccessibleButton onClick={this.onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}>
|
||||||
{ _t('Unignore') }
|
{ _t('Unignore') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<span id={id}>{ this.props.userId }</span>
|
<span id={id}>{ this.props.userId }</span>
|
||||||
|
@ -62,17 +67,26 @@ export class IgnoredUser extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
|
interface IProps {
|
||||||
export default class SecurityUserSettingsTab extends React.Component {
|
closeSettingsFn: () => void;
|
||||||
static propTypes = {
|
}
|
||||||
closeSettingsFn: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
interface IState {
|
||||||
super();
|
ignoredUserIds: string[];
|
||||||
|
waitingUnignored: string[];
|
||||||
|
managingInvites: boolean;
|
||||||
|
invitedRoomAmt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
|
||||||
|
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
// Get number of rooms we're invited to
|
// Get number of rooms we're invited to
|
||||||
const invitedRooms = this._getInvitedRooms();
|
const invitedRooms = this.getInvitedRooms();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
|
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
|
||||||
|
@ -80,59 +94,57 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
managingInvites: false,
|
managingInvites: false,
|
||||||
invitedRoomAmt: invitedRooms.length,
|
invitedRoomAmt: invitedRooms.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._onAction = this._onAction.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction({ action }) {
|
private onAction = ({ action }: ActionPayload)=> {
|
||||||
if (action === "ignore_state_changed") {
|
if (action === "ignore_state_changed") {
|
||||||
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
|
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
|
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
|
||||||
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
|
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentWillUnmount(): void {
|
||||||
this.dispatcherRef = dis.register(this._onAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateBlacklistDevicesFlag = (checked) => {
|
private updateBlacklistDevicesFlag = (checked): void => {
|
||||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateAnalytics = (checked) => {
|
private updateAnalytics = (checked: boolean): void => {
|
||||||
checked ? Analytics.enable() : Analytics.disable();
|
checked ? Analytics.enable() : Analytics.disable();
|
||||||
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
|
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
|
||||||
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
|
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
|
||||||
};
|
};
|
||||||
|
|
||||||
_onExportE2eKeysClicked = () => {
|
private onExportE2eKeysClicked = (): void => {
|
||||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||||
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||||
{ matrixClient: MatrixClientPeg.get() },
|
{ matrixClient: MatrixClientPeg.get() },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onImportE2eKeysClicked = () => {
|
private onImportE2eKeysClicked = (): void => {
|
||||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||||
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
|
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
|
||||||
{ matrixClient: MatrixClientPeg.get() },
|
{ matrixClient: MatrixClientPeg.get() },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onGoToUserProfileClick = () => {
|
private onGoToUserProfileClick = (): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_user_info',
|
action: 'view_user_info',
|
||||||
userId: MatrixClientPeg.get().getUserId(),
|
userId: MatrixClientPeg.get().getUserId(),
|
||||||
});
|
});
|
||||||
this.props.closeSettingsFn();
|
this.props.closeSettingsFn();
|
||||||
}
|
};
|
||||||
|
|
||||||
_onUserUnignored = async (userId) => {
|
private onUserUnignored = async (userId: string): Promise<void> => {
|
||||||
const { ignoredUserIds, waitingUnignored } = this.state;
|
const { ignoredUserIds, waitingUnignored } = this.state;
|
||||||
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
|
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
|
||||||
|
|
||||||
|
@ -144,24 +156,23 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_getInvitedRooms = () => {
|
private getInvitedRooms = (): Room[] => {
|
||||||
return MatrixClientPeg.get().getRooms().filter((r) => {
|
return MatrixClientPeg.get().getRooms().filter((r) => {
|
||||||
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
|
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_manageInvites = async (accept) => {
|
private manageInvites = async (accept: boolean): Promise<void> => {
|
||||||
this.setState({
|
this.setState({
|
||||||
managingInvites: true,
|
managingInvites: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compile array of invitation room ids
|
// Compile array of invitation room ids
|
||||||
const invitedRoomIds = this._getInvitedRooms().map((room) => {
|
const invitedRoomIds = this.getInvitedRooms().map((room) => {
|
||||||
return room.roomId;
|
return room.roomId;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Execute all acceptances/rejections sequentially
|
// Execute all acceptances/rejections sequentially
|
||||||
const self = this;
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli);
|
const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli);
|
||||||
for (let i = 0; i < invitedRoomIds.length; i++) {
|
for (let i = 0; i < invitedRoomIds.length; i++) {
|
||||||
|
@ -170,7 +181,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
// Accept/reject invite
|
// Accept/reject invite
|
||||||
await action(roomId).then(() => {
|
await action(roomId).then(() => {
|
||||||
// No error, update invited rooms button
|
// No error, update invited rooms button
|
||||||
this.setState({ invitedRoomAmt: self.state.invitedRoomAmt - 1 });
|
this.setState({ invitedRoomAmt: this.state.invitedRoomAmt - 1 });
|
||||||
}, async (e) => {
|
}, async (e) => {
|
||||||
// Action failure
|
// Action failure
|
||||||
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||||
|
@ -192,17 +203,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAcceptAllInvitesClicked = (ev) => {
|
private onAcceptAllInvitesClicked = (): void => {
|
||||||
this._manageInvites(true);
|
this.manageInvites(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRejectAllInvitesClicked = (ev) => {
|
private onRejectAllInvitesClicked = (): void => {
|
||||||
this._manageInvites(false);
|
this.manageInvites(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderCurrentDeviceInfo() {
|
private renderCurrentDeviceInfo(): JSX.Element {
|
||||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const deviceId = client.deviceId;
|
const deviceId = client.deviceId;
|
||||||
let identityKey = client.getDeviceEd25519Key();
|
let identityKey = client.getDeviceEd25519Key();
|
||||||
|
@ -216,10 +225,10 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
if (client.isCryptoEnabled()) {
|
if (client.isCryptoEnabled()) {
|
||||||
importExportButtons = (
|
importExportButtons = (
|
||||||
<div className='mx_SecurityUserSettingsTab_importExportButtons'>
|
<div className='mx_SecurityUserSettingsTab_importExportButtons'>
|
||||||
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
|
<AccessibleButton kind='primary' onClick={this.onExportE2eKeysClicked}>
|
||||||
{ _t("Export E2E room keys") }
|
{ _t("Export E2E room keys") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
|
<AccessibleButton kind='primary' onClick={this.onImportE2eKeysClicked}>
|
||||||
{ _t("Import E2E room keys") }
|
{ _t("Import E2E room keys") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -231,7 +240,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
noSendUnverifiedSetting = <SettingsFlag
|
noSendUnverifiedSetting = <SettingsFlag
|
||||||
name='blacklistUnverifiedDevices'
|
name='blacklistUnverifiedDevices'
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
onChange={this._updateBlacklistDevicesFlag}
|
onChange={this.updateBlacklistDevicesFlag}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +263,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderIgnoredUsers() {
|
private renderIgnoredUsers(): JSX.Element {
|
||||||
const { waitingUnignored, ignoredUserIds } = this.state;
|
const { waitingUnignored, ignoredUserIds } = this.state;
|
||||||
|
|
||||||
const userIds = !ignoredUserIds?.length
|
const userIds = !ignoredUserIds?.length
|
||||||
|
@ -263,7 +272,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
return (
|
return (
|
||||||
<IgnoredUser
|
<IgnoredUser
|
||||||
userId={u}
|
userId={u}
|
||||||
onUnignored={this._onUserUnignored}
|
onUnignored={this.onUserUnignored}
|
||||||
key={u}
|
key={u}
|
||||||
inProgress={waitingUnignored.includes(u)}
|
inProgress={waitingUnignored.includes(u)}
|
||||||
/>
|
/>
|
||||||
|
@ -280,15 +289,14 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderManageInvites() {
|
private renderManageInvites(): JSX.Element {
|
||||||
if (this.state.invitedRoomAmt === 0) {
|
if (this.state.invitedRoomAmt === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitedRooms = this._getInvitedRooms();
|
const invitedRooms = this.getInvitedRooms();
|
||||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
const onClickAccept = this.onAcceptAllInvitesClicked.bind(this, invitedRooms);
|
||||||
const onClickAccept = this._onAcceptAllInvitesClicked.bind(this, invitedRooms);
|
const onClickReject = this.onRejectAllInvitesClicked.bind(this, invitedRooms);
|
||||||
const onClickReject = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
|
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
|
||||||
|
@ -303,11 +311,8 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
|
|
||||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
|
||||||
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
|
|
||||||
|
|
||||||
const secureBackup = (
|
const secureBackup = (
|
||||||
<div className='mx_SettingsTab_section'>
|
<div className='mx_SettingsTab_section'>
|
||||||
|
@ -329,7 +334,6 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
// it's useful to have for testing the feature. If there's no interest
|
// it's useful to have for testing the feature. If there's no interest
|
||||||
// in having advanced details here once all flows are implemented, we
|
// in having advanced details here once all flows are implemented, we
|
||||||
// can remove this.
|
// can remove this.
|
||||||
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
|
|
||||||
const crossSigning = (
|
const crossSigning = (
|
||||||
<div className='mx_SettingsTab_section'>
|
<div className='mx_SettingsTab_section'>
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
|
||||||
|
@ -365,16 +369,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
{ _t("Learn more about how we use analytics.") }
|
{ _t("Learn more about how we use analytics.") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
|
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this.updateAnalytics} />
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
|
|
||||||
let advancedSection;
|
let advancedSection;
|
||||||
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
|
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
|
||||||
const ignoreUsersPanel = this._renderIgnoredUsers();
|
const ignoreUsersPanel = this.renderIgnoredUsers();
|
||||||
const invitesPanel = this._renderManageInvites();
|
const invitesPanel = this.renderManageInvites();
|
||||||
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
|
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
|
||||||
// only show the section if there's something to show
|
// only show the section if there's something to show
|
||||||
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
|
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
|
||||||
|
@ -399,7 +402,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
"Manage the names of and sign out of your sessions below or " +
|
"Manage the names of and sign out of your sessions below or " +
|
||||||
"<a>verify them in your User Profile</a>.", {},
|
"<a>verify them in your User Profile</a>.", {},
|
||||||
{
|
{
|
||||||
a: sub => <AccessibleButton kind="link" onClick={this._onGoToUserProfileClick}>
|
a: sub => <AccessibleButton kind="link" onClick={this.onGoToUserProfileClick}>
|
||||||
{ sub }
|
{ sub }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
},
|
},
|
||||||
|
@ -415,7 +418,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
{ secureBackup }
|
{ secureBackup }
|
||||||
{ eventIndex }
|
{ eventIndex }
|
||||||
{ crossSigning }
|
{ crossSigning }
|
||||||
{ this._renderCurrentDeviceInfo() }
|
{ this.renderCurrentDeviceInfo() }
|
||||||
</div>
|
</div>
|
||||||
{ privacySection }
|
{ privacySection }
|
||||||
{ advancedSection }
|
{ advancedSection }
|
|
@ -25,7 +25,7 @@ interface IProps {
|
||||||
policiesAndServicePairs: any[];
|
policiesAndServicePairs: any[];
|
||||||
onFinished: (string) => void;
|
onFinished: (string) => void;
|
||||||
agreedUrls: string[]; // array of URLs the user has accepted
|
agreedUrls: string[]; // array of URLs the user has accepted
|
||||||
introElement: Node;
|
introElement: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -1086,11 +1086,11 @@
|
||||||
"Failed to upload profile picture!": "Failed to upload profile picture!",
|
"Failed to upload profile picture!": "Failed to upload profile picture!",
|
||||||
"Upload new:": "Upload new:",
|
"Upload new:": "Upload new:",
|
||||||
"No display name": "No display name",
|
"No display name": "No display name",
|
||||||
"New passwords don't match": "New passwords don't match",
|
|
||||||
"Passwords can't be empty": "Passwords can't be empty",
|
|
||||||
"Warning!": "Warning!",
|
"Warning!": "Warning!",
|
||||||
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
|
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
|
||||||
"Export E2E room keys": "Export E2E room keys",
|
"Export E2E room keys": "Export E2E room keys",
|
||||||
|
"New passwords don't match": "New passwords don't match",
|
||||||
|
"Passwords can't be empty": "Passwords can't be empty",
|
||||||
"Do you want to set an email address?": "Do you want to set an email address?",
|
"Do you want to set an email address?": "Do you want to set an email address?",
|
||||||
"Confirm password": "Confirm password",
|
"Confirm password": "Confirm password",
|
||||||
"Passwords don't match": "Passwords don't match",
|
"Passwords don't match": "Passwords don't match",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue