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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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