Merge pull request #6076 from matrix-org/jryans/convert-flow-to-ts-2

Convert some Flow typed files to TS (round 2)
This commit is contained in:
J. Ryan Stinnett 2021-05-26 10:54:09 +01:00 committed by GitHub
commit e3a9e4690b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 502 additions and 364 deletions

View file

@ -36,14 +36,18 @@ export class Service {
} }
} }
interface Policy { export interface LocalisedPolicy {
name: string;
url: string;
}
export interface Policy {
// @ts-ignore: No great way to express indexed types together with other keys // @ts-ignore: No great way to express indexed types together with other keys
version: string; version: string;
[lang: string]: { [lang: string]: LocalisedPolicy;
url: string;
};
} }
type Policies = {
export type Policies = {
[policy: string]: Policy, [policy: string]: Policy,
}; };

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016-2021 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {createRef} from 'react'; import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
import PropTypes from 'prop-types'; import classNames from 'classnames';
import classnames from 'classnames'; import { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms';
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -74,36 +73,72 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
* focus: set the input focus appropriately in the form. * focus: set the input focus appropriately in the form.
*/ */
enum AuthType {
Password = "m.login.password",
Recaptcha = "m.login.recaptcha",
Terms = "m.login.terms",
Email = "m.login.email.identity",
Msisdn = "m.login.msisdn",
Sso = "m.login.sso",
SsoUnstable = "org.matrix.login.sso",
}
/* eslint-disable camelcase */
interface IAuthDict {
type?: AuthType;
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
user?: string;
identifier?: any;
password?: string;
response?: string;
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds?: any;
threepidCreds?: any;
}
/* eslint-enable camelcase */
export const DEFAULT_PHASE = 0; export const DEFAULT_PHASE = 0;
@replaceableComponent("views.auth.PasswordAuthEntry") interface IAuthEntryProps {
export class PasswordAuthEntry extends React.Component { matrixClient: MatrixClient;
static LOGIN_TYPE = "m.login.password"; loginType: string;
authSessionId: string;
errorText?: string;
// Is the auth logic currently waiting for something to happen?
busy?: boolean;
onPhaseChange: (phase: number) => void;
submitAuthDict: (auth: IAuthDict) => void;
}
static propTypes = { interface IPasswordAuthEntryState {
matrixClient: PropTypes.object.isRequired, password: string;
submitAuthDict: PropTypes.func.isRequired, }
errorText: PropTypes.string,
// is the auth logic currently waiting for something to @replaceableComponent("views.auth.PasswordAuthEntry")
// happen? export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
busy: PropTypes.bool, static LOGIN_TYPE = AuthType.Password;
onPhaseChange: PropTypes.func.isRequired,
}; constructor(props) {
super(props);
this.state = {
password: "",
};
}
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
} }
state = { private onSubmit = (e: FormEvent) => {
password: "",
};
_onSubmit = e => {
e.preventDefault(); e.preventDefault();
if (this.props.busy) return; if (this.props.busy) return;
this.props.submitAuthDict({ this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE, type: AuthType.Password,
// TODO: Remove `user` once servers support proper UIA // TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/vector-im/element-web/issues/10312
user: this.props.matrixClient.credentials.userId, user: this.props.matrixClient.credentials.userId,
@ -115,7 +150,7 @@ export class PasswordAuthEntry extends React.Component {
}); });
}; };
_onPasswordFieldChange = ev => { private onPasswordFieldChange = (ev: ChangeEvent<HTMLInputElement>) => {
// enable the submit button iff the password is non-empty // enable the submit button iff the password is non-empty
this.setState({ this.setState({
password: ev.target.value, password: ev.target.value,
@ -123,7 +158,7 @@ export class PasswordAuthEntry extends React.Component {
}; };
render() { render() {
const passwordBoxClass = classnames({ const passwordBoxClass = classNames({
"error": this.props.errorText, "error": this.props.errorText,
}); });
@ -155,7 +190,7 @@ export class PasswordAuthEntry extends React.Component {
return ( return (
<div> <div>
<p>{ _t("Confirm your identity by entering your account password below.") }</p> <p>{ _t("Confirm your identity by entering your account password below.") }</p>
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection"> <form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
<Field <Field
className={passwordBoxClass} className={passwordBoxClass}
type="password" type="password"
@ -163,7 +198,7 @@ export class PasswordAuthEntry extends React.Component {
label={_t('Password')} label={_t('Password')}
autoFocus={true} autoFocus={true}
value={this.state.password} value={this.state.password}
onChange={this._onPasswordFieldChange} onChange={this.onPasswordFieldChange}
/> />
<div className="mx_button_row"> <div className="mx_button_row">
{ submitButtonOrSpinner } { submitButtonOrSpinner }
@ -175,26 +210,26 @@ export class PasswordAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.RecaptchaAuthEntry") /* eslint-disable camelcase */
export class RecaptchaAuthEntry extends React.Component { interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
static LOGIN_TYPE = "m.login.recaptcha"; stageParams?: {
public_key?: string;
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
}; };
}
/* eslint-enable camelcase */
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
static LOGIN_TYPE = AuthType.Recaptcha;
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
} }
_onCaptchaResponse = response => { private onCaptchaResponse = (response: string) => {
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit"); CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
this.props.submitAuthDict({ this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE, type: AuthType.Recaptcha,
response: response, response: response,
}); });
}; };
@ -230,7 +265,7 @@ export class RecaptchaAuthEntry extends React.Component {
return ( return (
<div> <div>
<CaptchaForm sitePublicKey={sitePublicKey} <CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse} onCaptchaResponse={this.onCaptchaResponse}
/> />
{ errorSection } { errorSection }
</div> </div>
@ -238,18 +273,28 @@ export class RecaptchaAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.TermsAuthEntry") interface ITermsAuthEntryProps extends IAuthEntryProps {
export class TermsAuthEntry extends React.Component { stageParams?: {
static LOGIN_TYPE = "m.login.terms"; policies?: Policies;
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
}; };
showContinue: boolean;
}
interface LocalisedPolicyWithId extends LocalisedPolicy {
id: string;
}
interface ITermsAuthEntryState {
policies: LocalisedPolicyWithId[];
toggledPolicies: {
[policy: string]: boolean;
};
errorText?: string;
}
@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
static LOGIN_TYPE = AuthType.Terms;
constructor(props) { constructor(props) {
super(props); super(props);
@ -294,8 +339,11 @@ export class TermsAuthEntry extends React.Component {
initToggles[policyId] = false; initToggles[policyId] = false;
langPolicy.id = policyId; pickedPolicies.push({
pickedPolicies.push(langPolicy); id: policyId,
name: langPolicy.name,
url: langPolicy.url,
});
} }
this.state = { this.state = {
@ -311,11 +359,11 @@ export class TermsAuthEntry extends React.Component {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
} }
tryContinue = () => { public tryContinue = () => {
this._trySubmit(); this.trySubmit();
}; };
_togglePolicy(policyId) { private togglePolicy(policyId: string) {
const newToggles = {}; const newToggles = {};
for (const policy of this.state.policies) { for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id]; let checked = this.state.toggledPolicies[policy.id];
@ -326,7 +374,7 @@ export class TermsAuthEntry extends React.Component {
this.setState({"toggledPolicies": newToggles}); this.setState({"toggledPolicies": newToggles});
} }
_trySubmit = () => { private trySubmit = () => {
let allChecked = true; let allChecked = true;
for (const policy of this.state.policies) { for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id]; const checked = this.state.toggledPolicies[policy.id];
@ -334,7 +382,7 @@ export class TermsAuthEntry extends React.Component {
} }
if (allChecked) { if (allChecked) {
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE}); this.props.submitAuthDict({type: AuthType.Terms});
CountlyAnalytics.instance.track("onboarding_terms_complete"); CountlyAnalytics.instance.track("onboarding_terms_complete");
} else { } else {
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")}); this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
@ -356,7 +404,7 @@ export class TermsAuthEntry extends React.Component {
checkboxes.push( checkboxes.push(
// XXX: replace with StyledCheckbox // XXX: replace with StyledCheckbox
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy"> <label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} /> <input type="checkbox" onChange={() => this.togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a> <a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
</label>, </label>,
); );
@ -375,7 +423,7 @@ export class TermsAuthEntry extends React.Component {
if (this.props.showContinue !== false) { if (this.props.showContinue !== false) {
// XXX: button classes // XXX: button classes
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton" submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>; onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
} }
return ( return (
@ -389,21 +437,18 @@ export class TermsAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.EmailIdentityAuthEntry") interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
export class EmailIdentityAuthEntry extends React.Component { inputs?: {
static LOGIN_TYPE = "m.login.email.identity"; emailAddress?: string;
static propTypes = {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
authSessionId: PropTypes.string.isRequired,
clientSecret: PropTypes.string.isRequired,
inputs: PropTypes.object.isRequired,
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
onPhaseChange: PropTypes.func.isRequired,
}; };
stageState?: {
emailSid: string;
};
}
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
static LOGIN_TYPE = AuthType.Email;
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
@ -427,7 +472,7 @@ export class EmailIdentityAuthEntry extends React.Component {
return ( return (
<div className="mx_InteractiveAuthEntryComponents_emailWrapper"> <div className="mx_InteractiveAuthEntryComponents_emailWrapper">
<p>{ _t("A confirmation email has been sent to %(emailAddress)s", <p>{ _t("A confirmation email has been sent to %(emailAddress)s",
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> }, { emailAddress: <b>{ this.props.inputs.emailAddress }</b> },
) } ) }
</p> </p>
<p>{ _t("Open the link in the email to continue registration.") }</p> <p>{ _t("Open the link in the email to continue registration.") }</p>
@ -437,37 +482,44 @@ export class EmailIdentityAuthEntry extends React.Component {
} }
} }
interface IMsisdnAuthEntryProps extends IAuthEntryProps {
inputs: {
phoneCountry: string;
phoneNumber: string;
};
clientSecret: string;
fail: (error: Error) => void;
}
interface IMsisdnAuthEntryState {
token: string;
requestingToken: boolean;
errorText: string;
}
@replaceableComponent("views.auth.MsisdnAuthEntry") @replaceableComponent("views.auth.MsisdnAuthEntry")
export class MsisdnAuthEntry extends React.Component { export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
static LOGIN_TYPE = "m.login.msisdn"; static LOGIN_TYPE = AuthType.Msisdn;
static propTypes = { private submitUrl: string;
inputs: PropTypes.shape({ private sid: string;
phoneCountry: PropTypes.string, private msisdn: string;
phoneNumber: PropTypes.string,
}),
fail: PropTypes.func,
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
onPhaseChange: PropTypes.func.isRequired,
};
state = { constructor(props) {
token: '', super(props);
requestingToken: false,
}; this.state = {
token: '',
requestingToken: false,
errorText: '',
};
}
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
this._submitUrl = null;
this._sid = null;
this._msisdn = null;
this._tokenBox = null;
this.setState({requestingToken: true}); this.setState({requestingToken: true});
this._requestMsisdnToken().catch((e) => { this.requestMsisdnToken().catch((e) => {
this.props.fail(e); this.props.fail(e);
}).finally(() => { }).finally(() => {
this.setState({requestingToken: false}); this.setState({requestingToken: false});
@ -477,26 +529,26 @@ export class MsisdnAuthEntry extends React.Component {
/* /*
* Requests a verification token by SMS. * Requests a verification token by SMS.
*/ */
_requestMsisdnToken() { private requestMsisdnToken(): Promise<void> {
return this.props.matrixClient.requestRegisterMsisdnToken( return this.props.matrixClient.requestRegisterMsisdnToken(
this.props.inputs.phoneCountry, this.props.inputs.phoneCountry,
this.props.inputs.phoneNumber, this.props.inputs.phoneNumber,
this.props.clientSecret, this.props.clientSecret,
1, // TODO: Multiple send attempts? 1, // TODO: Multiple send attempts?
).then((result) => { ).then((result) => {
this._submitUrl = result.submit_url; this.submitUrl = result.submit_url;
this._sid = result.sid; this.sid = result.sid;
this._msisdn = result.msisdn; this.msisdn = result.msisdn;
}); });
} }
_onTokenChange = e => { private onTokenChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
token: e.target.value, token: e.target.value,
}); });
}; };
_onFormSubmit = async e => { private onFormSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
if (this.state.token == '') return; if (this.state.token == '') return;
@ -506,20 +558,20 @@ export class MsisdnAuthEntry extends React.Component {
try { try {
let result; let result;
if (this._submitUrl) { if (this.submitUrl) {
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl( result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
this._submitUrl, this._sid, this.props.clientSecret, this.state.token, this.submitUrl, this.sid, this.props.clientSecret, this.state.token,
); );
} else { } else {
throw new Error("The registration with MSISDN flow is misconfigured"); throw new Error("The registration with MSISDN flow is misconfigured");
} }
if (result.success) { if (result.success) {
const creds = { const creds = {
sid: this._sid, sid: this.sid,
client_secret: this.props.clientSecret, client_secret: this.props.clientSecret,
}; };
this.props.submitAuthDict({ this.props.submitAuthDict({
type: MsisdnAuthEntry.LOGIN_TYPE, type: AuthType.Msisdn,
// TODO: Remove `threepid_creds` once servers support proper UIA // TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220 // See https://github.com/matrix-org/matrix-doc/issues/2220
@ -543,7 +595,7 @@ export class MsisdnAuthEntry extends React.Component {
return <Loader />; return <Loader />;
} else { } else {
const enableSubmit = Boolean(this.state.token); const enableSubmit = Boolean(this.state.token);
const submitClasses = classnames({ const submitClasses = classNames({
mx_InteractiveAuthEntryComponents_msisdnSubmit: true, mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
mx_GeneralButton: true, mx_GeneralButton: true,
}); });
@ -558,16 +610,16 @@ export class MsisdnAuthEntry extends React.Component {
return ( return (
<div> <div>
<p>{ _t("A text message has been sent to %(msisdn)s", <p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>{ this._msisdn }</i> }, { msisdn: <i>{ this.msisdn }</i> },
) } ) }
</p> </p>
<p>{ _t("Please enter the code it contains:") }</p> <p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}> <form onSubmit={this.onFormSubmit}>
<input type="text" <input type="text"
className="mx_InteractiveAuthEntryComponents_msisdnEntry" className="mx_InteractiveAuthEntryComponents_msisdnEntry"
value={this.state.token} value={this.state.token}
onChange={this._onTokenChange} onChange={this.onTokenChange}
aria-label={ _t("Code")} aria-label={ _t("Code")}
/> />
<br /> <br />
@ -584,40 +636,40 @@ export class MsisdnAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.SSOAuthEntry") interface ISSOAuthEntryProps extends IAuthEntryProps {
export class SSOAuthEntry extends React.Component { continueText?: string;
static propTypes = { continueKind?: string;
matrixClient: PropTypes.object.isRequired, onCancel?: () => void;
authSessionId: PropTypes.string.isRequired, }
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
continueText: PropTypes.string,
continueKind: PropTypes.string,
onCancel: PropTypes.func,
};
static LOGIN_TYPE = "m.login.sso"; interface ISSOAuthEntryState {
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso"; phase: number;
attemptFailed: boolean;
}
@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
static LOGIN_TYPE = AuthType.Sso;
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
static PHASE_PREAUTH = 1; // button to start SSO static PHASE_PREAUTH = 1; // button to start SSO
static PHASE_POSTAUTH = 2; // button to confirm SSO completed static PHASE_POSTAUTH = 2; // button to confirm SSO completed
_ssoUrl: string; private ssoUrl: string;
private popupWindow: Window;
constructor(props) { constructor(props) {
super(props); super(props);
// We actually send the user through fallback auth so we don't have to // We actually send the user through fallback auth so we don't have to
// deal with a redirect back to us, losing application context. // deal with a redirect back to us, losing application context.
this._ssoUrl = props.matrixClient.getFallbackAuthUrl( this.ssoUrl = props.matrixClient.getFallbackAuthUrl(
this.props.loginType, this.props.loginType,
this.props.authSessionId, this.props.authSessionId,
); );
this._popupWindow = null; this.popupWindow = null;
window.addEventListener("message", this._onReceiveMessage); window.addEventListener("message", this.onReceiveMessage);
this.state = { this.state = {
phase: SSOAuthEntry.PHASE_PREAUTH, phase: SSOAuthEntry.PHASE_PREAUTH,
@ -625,44 +677,44 @@ export class SSOAuthEntry extends React.Component {
}; };
} }
componentDidMount(): void { componentDidMount() {
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH); this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage); window.removeEventListener("message", this.onReceiveMessage);
if (this._popupWindow) { if (this.popupWindow) {
this._popupWindow.close(); this.popupWindow.close();
this._popupWindow = null; this.popupWindow = null;
} }
} }
attemptFailed = () => { public attemptFailed = () => {
this.setState({ this.setState({
attemptFailed: true, attemptFailed: true,
}); });
}; };
_onReceiveMessage = event => { private onReceiveMessage = (event: MessageEvent) => {
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
if (this._popupWindow) { if (this.popupWindow) {
this._popupWindow.close(); this.popupWindow.close();
this._popupWindow = null; this.popupWindow = null;
} }
} }
}; };
onStartAuthClick = () => { private onStartAuthClick = () => {
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost // Note: We don't use PlatformPeg's startSsoAuth functions because we almost
// certainly will need to open the thing in a new tab to avoid losing application // certainly will need to open the thing in a new tab to avoid losing application
// context. // context.
this._popupWindow = window.open(this._ssoUrl, "_blank"); this.popupWindow = window.open(this.ssoUrl, "_blank");
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH}); this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH); this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
}; };
onConfirmClick = () => { private onConfirmClick = () => {
this.props.submitAuthDict({}); this.props.submitAuthDict({});
}; };
@ -716,46 +768,37 @@ export class SSOAuthEntry extends React.Component {
} }
@replaceableComponent("views.auth.FallbackAuthEntry") @replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component { export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
static propTypes = { private popupWindow: Window;
matrixClient: PropTypes.object.isRequired, private fallbackButton = createRef<HTMLAnchorElement>();
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
};
constructor(props) { constructor(props) {
super(props); super(props);
// we have to make the user click a button, as browsers will block // we have to make the user click a button, as browsers will block
// the popup if we open it immediately. // the popup if we open it immediately.
this._popupWindow = null; this.popupWindow = null;
window.addEventListener("message", this._onReceiveMessage); window.addEventListener("message", this.onReceiveMessage);
this._fallbackButton = createRef();
} }
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage); window.removeEventListener("message", this.onReceiveMessage);
if (this._popupWindow) { if (this.popupWindow) {
this._popupWindow.close(); this.popupWindow.close();
} }
} }
focus = () => { public focus = () => {
if (this._fallbackButton.current) { if (this.fallbackButton.current) {
this._fallbackButton.current.focus(); this.fallbackButton.current.focus();
} }
}; };
_onShowFallbackClick = e => { private onShowFallbackClick = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -763,10 +806,10 @@ export class FallbackAuthEntry extends React.Component {
this.props.loginType, this.props.loginType,
this.props.authSessionId, this.props.authSessionId,
); );
this._popupWindow = window.open(url, "_blank"); this.popupWindow = window.open(url, "_blank");
}; };
_onReceiveMessage = event => { private onReceiveMessage = (event: MessageEvent) => {
if ( if (
event.data === "authDone" && event.data === "authDone" &&
event.origin === this.props.matrixClient.getHomeserverUrl() event.origin === this.props.matrixClient.getHomeserverUrl()
@ -786,27 +829,31 @@ export class FallbackAuthEntry extends React.Component {
} }
return ( return (
<div> <div>
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a> <a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
_t("Start authentication")
}</a>
{errorSection} {errorSection}
</div> </div>
); );
} }
} }
const AuthEntryComponents = [ export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
PasswordAuthEntry, switch (loginType) {
RecaptchaAuthEntry, case AuthType.Password:
EmailIdentityAuthEntry, return PasswordAuthEntry;
MsisdnAuthEntry, case AuthType.Recaptcha:
TermsAuthEntry, return RecaptchaAuthEntry;
SSOAuthEntry, case AuthType.Email:
]; return EmailIdentityAuthEntry;
case AuthType.Msisdn:
export default function getEntryComponentForLoginType(loginType) { return MsisdnAuthEntry;
for (const c of AuthEntryComponents) { case AuthType.Terms:
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) { return TermsAuthEntry;
return c; case AuthType.Sso:
} case AuthType.SsoUnstable:
return SSOAuthEntry;
default:
return FallbackAuthEntry;
} }
return FallbackAuthEntry;
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,14 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useState, useEffect} from 'react'; import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight'; import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "../elements/Field"; import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { import {
PHASE_UNSENT, PHASE_UNSENT,
@ -30,27 +30,33 @@ import {
PHASE_DONE, PHASE_DONE,
PHASE_STARTED, PHASE_STARTED,
PHASE_CANCELLED, PHASE_CANCELLED,
VerificationRequest,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import WidgetStore from "../../../stores/WidgetStore"; import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import {SETTINGS} from "../../../settings/Settings"; import { SETTINGS } from "../../../settings/Settings";
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore"; import SettingsStore, { LEVEL_ORDER } from "../../../settings/SettingsStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog"; import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import {Room} from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SettingLevel } from '../../../settings/SettingLevel';
class GenericEditor extends React.PureComponent { interface IGenericEditorProps {
// static propTypes = {onBack: PropTypes.func.isRequired}; onBack: () => void;
}
constructor(props) { interface IGenericEditorState {
super(props); message?: string;
this._onChange = this._onChange.bind(this); [inputId: string]: boolean | string;
this.onBack = this.onBack.bind(this); }
}
onBack() { abstract class GenericEditor<
P extends IGenericEditorProps = IGenericEditorProps,
S extends IGenericEditorState = IGenericEditorState,
> extends React.PureComponent<P, S> {
protected onBack = () => {
if (this.state.message) { if (this.state.message) {
this.setState({ message: null }); this.setState({ message: null });
} else { } else {
@ -58,47 +64,60 @@ class GenericEditor extends React.PureComponent {
} }
} }
_onChange(e) { protected onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
// @ts-ignore: Unsure how to convince TS this is okay when the state
// type can be extended.
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
} }
_buttons() { protected abstract send();
protected buttons(): React.ReactNode {
return <div className="mx_Dialog_buttons"> return <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> } { !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
</div>; </div>;
} }
textInput(id, label) { protected textInput(id: string, label: string): React.ReactNode {
return <Field return <Field
id={id} id={id}
label={label} label={label}
size="42" size={42}
autoFocus={true} autoFocus={true}
type="text" type="text"
autoComplete="on" autoComplete="on"
value={this.state[id]} value={this.state[id] as string}
onChange={this._onChange} onChange={this.onChange}
/>; />;
} }
} }
export class SendCustomEvent extends GenericEditor { interface ISendCustomEventProps extends IGenericEditorProps {
static getLabel() { return _t('Send Custom Event'); } room: Room;
forceStateEvent?: boolean;
static propTypes = { forceGeneralEvent?: boolean;
onBack: PropTypes.func.isRequired, inputs?: {
room: PropTypes.instanceOf(Room).isRequired, eventType?: string;
forceStateEvent: PropTypes.bool, stateKey?: string;
forceGeneralEvent: PropTypes.bool, evContent?: string;
inputs: PropTypes.object,
}; };
}
interface ISendCustomEventState extends IGenericEditorState {
isStateEvent: boolean;
eventType: string;
stateKey: string;
evContent: string;
}
export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendCustomEventState> {
static getLabel() { return _t('Send Custom Event'); }
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
constructor(props) { constructor(props) {
super(props); super(props);
this._send = this._send.bind(this);
const {eventType, stateKey, evContent} = Object.assign({ const {eventType, stateKey, evContent} = Object.assign({
eventType: '', eventType: '',
@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor {
}; };
} }
send(content) { private doSend(content: object): Promise<void> {
const cli = this.context; const cli = this.context;
if (this.state.isStateEvent) { if (this.state.isStateEvent) {
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey); return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
@ -124,7 +143,7 @@ export class SendCustomEvent extends GenericEditor {
} }
} }
async _send() { protected send = async () => {
if (this.state.eventType === '') { if (this.state.eventType === '') {
this.setState({ message: _t('You must specify an event type!') }); this.setState({ message: _t('You must specify an event type!') });
return; return;
@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor {
let message; let message;
try { try {
const content = JSON.parse(this.state.evContent); const content = JSON.parse(this.state.evContent);
await this.send(content); await this.doSend(content);
message = _t('Event sent!'); message = _t('Event sent!');
} catch (e) { } catch (e) {
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')'; message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{ this.state.message } { this.state.message }
</div> </div>
{ this._buttons() } { this.buttons() }
</div>; </div>;
} }
@ -163,35 +182,51 @@ export class SendCustomEvent extends GenericEditor {
<br /> <br />
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea" <Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" /> autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> } { !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
{ showTglFlip && <div style={{float: "right"}}> { showTglFlip && <div style={{float: "right"}}>
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} /> <input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" /> type="checkbox"
checked={this.state.isStateEvent}
onChange={this.onChange}
/>
<label className="mx_DevTools_tgl-btn"
data-tg-off="Event"
data-tg-on="State Event"
htmlFor="isStateEvent"
/>
</div> } </div> }
</div> </div>
</div>; </div>;
} }
} }
class SendAccountData extends GenericEditor { interface ISendAccountDataProps extends IGenericEditorProps {
static getLabel() { return _t('Send Account Data'); } room: Room;
isRoomAccountData: boolean;
static propTypes = { forceMode: boolean;
room: PropTypes.instanceOf(Room).isRequired, inputs?: {
isRoomAccountData: PropTypes.bool, eventType?: string;
forceMode: PropTypes.bool, evContent?: string;
inputs: PropTypes.object,
}; };
}
interface ISendAccountDataState extends IGenericEditorState {
isRoomAccountData: boolean;
eventType: string;
evContent: string;
}
class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountDataState> {
static getLabel() { return _t('Send Account Data'); }
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
constructor(props) { constructor(props) {
super(props); super(props);
this._send = this._send.bind(this);
const {eventType, evContent} = Object.assign({ const {eventType, evContent} = Object.assign({
eventType: '', eventType: '',
@ -206,7 +241,7 @@ class SendAccountData extends GenericEditor {
}; };
} }
send(content) { private doSend(content: object): Promise<void> {
const cli = this.context; const cli = this.context;
if (this.state.isRoomAccountData) { if (this.state.isRoomAccountData) {
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content); return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
@ -214,7 +249,7 @@ class SendAccountData extends GenericEditor {
return cli.setAccountData(this.state.eventType, content); return cli.setAccountData(this.state.eventType, content);
} }
async _send() { protected send = async () => {
if (this.state.eventType === '') { if (this.state.eventType === '') {
this.setState({ message: _t('You must specify an event type!') }); this.setState({ message: _t('You must specify an event type!') });
return; return;
@ -223,7 +258,7 @@ class SendAccountData extends GenericEditor {
let message; let message;
try { try {
const content = JSON.parse(this.state.evContent); const content = JSON.parse(this.state.evContent);
await this.send(content); await this.doSend(content);
message = _t('Event sent!'); message = _t('Event sent!');
} catch (e) { } catch (e) {
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')'; message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
@ -237,7 +272,7 @@ class SendAccountData extends GenericEditor {
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{ this.state.message } { this.state.message }
</div> </div>
{ this._buttons() } { this.buttons() }
</div>; </div>;
} }
@ -247,14 +282,23 @@ class SendAccountData extends GenericEditor {
<br /> <br />
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea" <Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" /> autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> } { !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
{ !this.state.message && <div style={{float: "right"}}> { !this.state.message && <div style={{float: "right"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} /> <input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" /> type="checkbox"
checked={this.state.isRoomAccountData}
disabled={this.props.forceMode}
onChange={this.onChange}
/>
<label className="mx_DevTools_tgl-btn"
data-tg-off="Account Data"
data-tg-on="Room Data"
htmlFor="isRoomAccountData"
/>
</div> } </div> }
</div> </div>
</div>; </div>;
@ -264,17 +308,22 @@ class SendAccountData extends GenericEditor {
const INITIAL_LOAD_TILES = 20; const INITIAL_LOAD_TILES = 20;
const LOAD_TILES_STEP_SIZE = 50; const LOAD_TILES_STEP_SIZE = 50;
class FilteredList extends React.PureComponent { interface IFilteredListProps {
static propTypes = { children: React.ReactElement[];
children: PropTypes.any, query: string;
query: PropTypes.string, onChange: (value: string) => void;
onChange: PropTypes.func, }
};
static filterChildren(children, query) { interface IFilteredListState {
filteredChildren: React.ReactElement[];
truncateAt: number;
}
class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredListState> {
static filterChildren(children: React.ReactElement[], query: string): React.ReactElement[] {
if (!query) return children; if (!query) return children;
const lcQuery = query.toLowerCase(); const lcQuery = query.toLowerCase();
return children.filter((child) => child.key.toLowerCase().includes(lcQuery)); return children.filter((child) => child.key.toString().toLowerCase().includes(lcQuery));
} }
constructor(props) { constructor(props) {
@ -295,27 +344,27 @@ class FilteredList extends React.PureComponent {
}); });
} }
showAll = () => { private showAll = () => {
this.setState({ this.setState({
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE, truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
}); });
}; };
createOverflowElement = (overflowCount: number, totalCount: number) => { private createOverflowElement = (overflowCount: number, totalCount: number) => {
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}> return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
{ _t("and %(count)s others...", { count: overflowCount }) } { _t("and %(count)s others...", { count: overflowCount }) }
</button>; </button>;
}; };
onQuery = (ev) => { private onQuery = (ev: ChangeEvent<HTMLInputElement>) => {
if (this.props.onChange) this.props.onChange(ev.target.value); if (this.props.onChange) this.props.onChange(ev.target.value);
}; };
getChildren = (start: number, end: number) => { private getChildren = (start: number, end: number): React.ReactElement[] => {
return this.state.filteredChildren.slice(start, end); return this.state.filteredChildren.slice(start, end);
}; };
getChildCount = (): number => { private getChildCount = (): number => {
return this.state.filteredChildren.length; return this.state.filteredChildren.length;
}; };
@ -336,28 +385,31 @@ class FilteredList extends React.PureComponent {
} }
} }
class RoomStateExplorer extends React.PureComponent { interface IExplorerProps {
static getLabel() { return _t('Explore Room State'); } room: Room;
onBack: () => void;
}
static propTypes = { interface IRoomStateExplorerState {
onBack: PropTypes.func.isRequired, eventType?: string;
room: PropTypes.instanceOf(Room).isRequired, event?: MatrixEvent;
}; editing: boolean;
queryEventType: string;
queryStateKey: string;
}
class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateExplorerState> {
static getLabel() { return _t('Explore Room State'); }
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
roomStateEvents: Map<string, Map<string, MatrixEvent>>; private roomStateEvents: Map<string, Map<string, MatrixEvent>>;
constructor(props) { constructor(props) {
super(props); super(props);
this.roomStateEvents = this.props.room.currentState.events; this.roomStateEvents = this.props.room.currentState.events;
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
this.onQueryEventType = this.onQueryEventType.bind(this);
this.onQueryStateKey = this.onQueryStateKey.bind(this);
this.state = { this.state = {
eventType: null, eventType: null,
event: null, event: null,
@ -368,19 +420,19 @@ class RoomStateExplorer extends React.PureComponent {
}; };
} }
browseEventType(eventType) { private browseEventType(eventType: string) {
return () => { return () => {
this.setState({ eventType }); this.setState({ eventType });
}; };
} }
onViewSourceClick(event) { private onViewSourceClick(event: MatrixEvent) {
return () => { return () => {
this.setState({ event }); this.setState({ event });
}; };
} }
onBack() { private onBack = () => {
if (this.state.editing) { if (this.state.editing) {
this.setState({ editing: false }); this.setState({ editing: false });
} else if (this.state.event) { } else if (this.state.event) {
@ -392,15 +444,15 @@ class RoomStateExplorer extends React.PureComponent {
} }
} }
editEv() { private editEv = () => {
this.setState({ editing: true }); this.setState({ editing: true });
} }
onQueryEventType(filterEventType) { private onQueryEventType = (filterEventType: string) => {
this.setState({ queryEventType: filterEventType }); this.setState({ queryEventType: filterEventType });
} }
onQueryStateKey(filterStateKey) { private onQueryStateKey = (filterStateKey: string) => {
this.setState({ queryStateKey: filterStateKey }); this.setState({ queryStateKey: filterStateKey });
} }
@ -472,24 +524,22 @@ class RoomStateExplorer extends React.PureComponent {
} }
} }
class AccountDataExplorer extends React.PureComponent { interface IAccountDataExplorerState {
static getLabel() { return _t('Explore Account Data'); } isRoomAccountData: boolean;
event?: MatrixEvent;
editing: boolean;
queryEventType: string;
[inputId: string]: boolean | string;
}
static propTypes = { class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
onBack: PropTypes.func.isRequired, static getLabel() { return _t('Explore Account Data'); }
room: PropTypes.instanceOf(Room).isRequired,
};
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
constructor(props) { constructor(props) {
super(props); super(props);
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
this._onChange = this._onChange.bind(this);
this.onQueryEventType = this.onQueryEventType.bind(this);
this.state = { this.state = {
isRoomAccountData: false, isRoomAccountData: false,
event: null, event: null,
@ -499,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent {
}; };
} }
getData() { private getData(): Record<string, MatrixEvent> {
if (this.state.isRoomAccountData) { if (this.state.isRoomAccountData) {
return this.props.room.accountData; return this.props.room.accountData;
} }
return this.context.store.accountData; return this.context.store.accountData;
} }
onViewSourceClick(event) { private onViewSourceClick(event: MatrixEvent) {
return () => { return () => {
this.setState({ event }); this.setState({ event });
}; };
} }
onBack() { private onBack = () => {
if (this.state.editing) { if (this.state.editing) {
this.setState({ editing: false }); this.setState({ editing: false });
} else if (this.state.event) { } else if (this.state.event) {
@ -522,15 +572,15 @@ class AccountDataExplorer extends React.PureComponent {
} }
} }
_onChange(e) { private onChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
} }
editEv() { private editEv = () => {
this.setState({ editing: true }); this.setState({ editing: true });
} }
onQueryEventType(queryEventType) { private onQueryEventType = (queryEventType: string) => {
this.setState({ queryEventType }); this.setState({ queryEventType });
} }
@ -580,30 +630,39 @@ class AccountDataExplorer extends React.PureComponent {
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <div style={{float: "right"}}> <div style={{float: "right"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} /> <input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" /> type="checkbox"
</div> } checked={this.state.isRoomAccountData}
onChange={this.onChange}
/>
<label className="mx_DevTools_tgl-btn"
data-tg-off="Account Data"
data-tg-on="Room Data"
htmlFor="isRoomAccountData"
/>
</div>
</div> </div>
</div>; </div>;
} }
} }
class ServersInRoomList extends React.PureComponent { interface IServersInRoomListState {
query: string;
}
class ServersInRoomList extends React.PureComponent<IExplorerProps, IServersInRoomListState> {
static getLabel() { return _t('View Servers in Room'); } static getLabel() { return _t('View Servers in Room'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private servers: React.ReactElement[];
constructor(props) { constructor(props) {
super(props); super(props);
const room = this.props.room; const room = this.props.room;
const servers = new Set(); const servers = new Set<string>();
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1])); room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
this.servers = Array.from(servers).map(s => this.servers = Array.from(servers).map(s =>
<button key={s} className="mx_DevTools_ServersInRoomList_button"> <button key={s} className="mx_DevTools_ServersInRoomList_button">
@ -615,7 +674,7 @@ class ServersInRoomList extends React.PureComponent {
}; };
} }
onQuery = (query) => { private onQuery = (query: string) => {
this.setState({ query }); this.setState({ query });
} }
@ -642,7 +701,10 @@ const PHASE_MAP = {
[PHASE_CANCELLED]: "cancelled", [PHASE_CANCELLED]: "cancelled",
}; };
function VerificationRequest({txnId, request}) { const VerificationRequestExplorer: React.FC<{
txnId: string;
request: VerificationRequest;
}> = ({txnId, request}) => {
const [, updateState] = useState(); const [, updateState] = useState();
const [timeout, setRequestTimeout] = useState(request.timeout); const [timeout, setRequestTimeout] = useState(request.timeout);
@ -679,7 +741,7 @@ function VerificationRequest({txnId, request}) {
</div>); </div>);
} }
class VerificationExplorer extends React.Component { class VerificationExplorer extends React.PureComponent<IExplorerProps> {
static getLabel() { static getLabel() {
return _t("Verification Requests"); return _t("Verification Requests");
} }
@ -687,7 +749,7 @@ class VerificationExplorer extends React.Component {
/* Ensure this.context is the cli */ /* Ensure this.context is the cli */
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
onNewRequest = () => { private onNewRequest = () => {
this.forceUpdate(); this.forceUpdate();
} }
@ -710,7 +772,7 @@ class VerificationExplorer extends React.Component {
return (<div> return (<div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) => {Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
<VerificationRequest txnId={txnId} request={request} key={txnId} />, <VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
)} )}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
@ -720,7 +782,12 @@ class VerificationExplorer extends React.Component {
} }
} }
class WidgetExplorer extends React.Component { interface IWidgetExplorerState {
query: string;
editWidget?: IApp;
}
class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerState> {
static getLabel() { static getLabel() {
return _t("Active Widgets"); return _t("Active Widgets");
} }
@ -734,19 +801,19 @@ class WidgetExplorer extends React.Component {
}; };
} }
onWidgetStoreUpdate = () => { private onWidgetStoreUpdate = () => {
this.forceUpdate(); this.forceUpdate();
}; };
onQueryChange = (query) => { private onQueryChange = (query: string) => {
this.setState({query}); this.setState({query});
}; };
onEditWidget = (widget) => { private onEditWidget = (widget: IApp) => {
this.setState({editWidget: widget}); this.setState({editWidget: widget});
}; };
onBack = () => { private onBack = () => {
const widgets = WidgetStore.instance.getApps(this.props.room.roomId); const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
if (this.state.editWidget && widgets.includes(this.state.editWidget)) { if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
this.setState({editWidget: null}); this.setState({editWidget: null});
@ -769,8 +836,11 @@ class WidgetExplorer extends React.Component {
const editWidget = this.state.editWidget; const editWidget = this.state.editWidget;
const widgets = WidgetStore.instance.getApps(room.roomId); const widgets = WidgetStore.instance.getApps(room.roomId);
if (editWidget && widgets.includes(editWidget)) { if (editWidget && widgets.includes(editWidget)) {
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values())) const allState = Array.from(
.reduce((p, c) => {p.push(...c); return p;}, []); Array.from(room.currentState.events.values()).map((e: Map<string, MatrixEvent>) => {
return e.values();
}),
).reduce((p, c) => { p.push(...c); return p; }, []);
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId); const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
if (!stateEv) { // "should never happen" if (!stateEv) { // "should never happen"
return <div> return <div>
@ -811,7 +881,15 @@ class WidgetExplorer extends React.Component {
} }
} }
class SettingsExplorer extends React.Component { interface ISettingsExplorerState {
query: string;
editSetting?: string;
viewSetting?: string;
explicitValues?: string;
explicitRoomValues?: string;
}
class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExplorerState> {
static getLabel() { static getLabel() {
return _t("Settings Explorer"); return _t("Settings Explorer");
} }
@ -829,19 +907,19 @@ class SettingsExplorer extends React.Component {
}; };
} }
onQueryChange = (ev) => { private onQueryChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({query: ev.target.value}); this.setState({query: ev.target.value});
}; };
onExplValuesEdit = (ev) => { private onExplValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
this.setState({explicitValues: ev.target.value}); this.setState({explicitValues: ev.target.value});
}; };
onExplRoomValuesEdit = (ev) => { private onExplRoomValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
this.setState({explicitRoomValues: ev.target.value}); this.setState({explicitRoomValues: ev.target.value});
}; };
onBack = () => { private onBack = () => {
if (this.state.editSetting) { if (this.state.editSetting) {
this.setState({editSetting: null}); this.setState({editSetting: null});
} else if (this.state.viewSetting) { } else if (this.state.viewSetting) {
@ -851,12 +929,12 @@ class SettingsExplorer extends React.Component {
} }
}; };
onViewClick = (ev, settingId) => { private onViewClick = (ev: MouseEvent, settingId: string) => {
ev.preventDefault(); ev.preventDefault();
this.setState({viewSetting: settingId}); this.setState({viewSetting: settingId});
}; };
onEditClick = (ev, settingId) => { private onEditClick = (ev: MouseEvent, settingId: string) => {
ev.preventDefault(); ev.preventDefault();
this.setState({ this.setState({
editSetting: settingId, editSetting: settingId,
@ -865,7 +943,7 @@ class SettingsExplorer extends React.Component {
}); });
}; };
onSaveClick = async () => { private onSaveClick = async () => {
try { try {
const settingId = this.state.editSetting; const settingId = this.state.editSetting;
const parsedExplicit = JSON.parse(this.state.explicitValues); const parsedExplicit = JSON.parse(this.state.explicitValues);
@ -874,7 +952,7 @@ class SettingsExplorer extends React.Component {
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`); console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
try { try {
const val = parsedExplicit[level]; const val = parsedExplicit[level];
await SettingsStore.setValue(settingId, null, level, val); await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
@ -884,7 +962,7 @@ class SettingsExplorer extends React.Component {
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`); console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
try { try {
const val = parsedExplicitRoom[level]; const val = parsedExplicitRoom[level];
await SettingsStore.setValue(settingId, roomId, level, val); await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
@ -901,7 +979,7 @@ class SettingsExplorer extends React.Component {
} }
}; };
renderSettingValue(val) { private renderSettingValue(val: any): string {
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us // Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
const toStringTypes = ['boolean', 'number']; const toStringTypes = ['boolean', 'number'];
if (toStringTypes.includes(typeof(val))) { if (toStringTypes.includes(typeof(val))) {
@ -911,7 +989,7 @@ class SettingsExplorer extends React.Component {
} }
} }
renderExplicitSettingValues(setting, roomId) { private renderExplicitSettingValues(setting: string, roomId: string): string {
const vals = {}; const vals = {};
for (const level of LEVEL_ORDER) { for (const level of LEVEL_ORDER) {
try { try {
@ -926,7 +1004,7 @@ class SettingsExplorer extends React.Component {
return JSON.stringify(vals, null, 4); return JSON.stringify(vals, null, 4);
} }
renderCanEditLevel(roomId, level) { private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode {
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level); const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable'; const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
return <td className={className}><code>{canEdit.toString()}</code></td>; return <td className={className}><code>{canEdit.toString()}</code></td>;
@ -1062,27 +1140,37 @@ class SettingsExplorer extends React.Component {
<div> <div>
{_t("Value:")}&nbsp; {_t("Value:")}&nbsp;
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code> <code>{this.renderSettingValue(
SettingsStore.getValue(this.state.viewSetting),
)}</code>
</div> </div>
<div> <div>
{_t("Value in this room:")}&nbsp; {_t("Value in this room:")}&nbsp;
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code> <code>{this.renderSettingValue(
SettingsStore.getValue(this.state.viewSetting, room.roomId),
)}</code>
</div> </div>
<div> <div>
{_t("Values at explicit levels:")} {_t("Values at explicit levels:")}
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre> <pre><code>{this.renderExplicitSettingValues(
this.state.viewSetting, null,
)}</code></pre>
</div> </div>
<div> <div>
{_t("Values at explicit levels in this room:")} {_t("Values at explicit levels in this room:")}
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre> <pre><code>{this.renderExplicitSettingValues(
this.state.viewSetting, room.roomId,
)}</code></pre>
</div> </div>
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button> <button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{
_t("Edit Values")
}</button>
<button onClick={this.onBack}>{_t("Back")}</button> <button onClick={this.onBack}>{_t("Back")}</button>
</div> </div>
</div> </div>
@ -1091,7 +1179,11 @@ class SettingsExplorer extends React.Component {
} }
} }
const Entries = [ type DevtoolsDialogEntry = React.JSXElementConstructor<any> & {
getLabel: () => string;
};
const Entries: DevtoolsDialogEntry[] = [
SendCustomEvent, SendCustomEvent,
RoomStateExplorer, RoomStateExplorer,
SendAccountData, SendAccountData,
@ -1102,43 +1194,36 @@ const Entries = [
SettingsExplorer, SettingsExplorer,
]; ];
@replaceableComponent("views.dialogs.DevtoolsDialog") interface IProps {
export default class DevtoolsDialog extends React.PureComponent { roomId: string;
static propTypes = { onFinished: (finished: boolean) => void;
roomId: PropTypes.string.isRequired, }
onFinished: PropTypes.func.isRequired,
};
interface IState {
mode?: DevtoolsDialogEntry;
}
@replaceableComponent("views.dialogs.DevtoolsDialog")
export default class DevtoolsDialog extends React.PureComponent<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
this.onBack = this.onBack.bind(this);
this.onCancel = this.onCancel.bind(this);
this.state = { this.state = {
mode: null, mode: null,
}; };
} }
componentWillUnmount() { private setMode(mode: DevtoolsDialogEntry) {
this._unmounted = true;
}
_setMode(mode) {
return () => { return () => {
this.setState({ mode }); this.setState({ mode });
}; };
} }
onBack() { private onBack = () => {
if (this.prevMode) { this.setState({ mode: null });
this.setState({ mode: this.prevMode });
this.prevMode = null;
} else {
this.setState({ mode: null });
}
} }
onCancel() { private onCancel = () => {
this.props.onFinished(false); this.props.onFinished(false);
} }
@ -1165,7 +1250,7 @@ export default class DevtoolsDialog extends React.PureComponent {
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{ Entries.map((Entry) => { { Entries.map((Entry) => {
const label = Entry.getLabel(); const label = Entry.getLabel();
const onClick = this._setMode(Entry); const onClick = this.setMode(Entry);
return <button className={classes} key={label} onClick={onClick}>{ label }</button>; return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
}) } }) }
</div> </div>

View file

@ -105,12 +105,14 @@ function safeCounterpartTranslate(text: string, options?: object) {
return translated; return translated;
} }
type SubstitutionValue = number | string | React.ReactNode | ((sub: string) => React.ReactNode);
export interface IVariables { export interface IVariables {
count?: number; count?: number;
[key: string]: number | string; [key: string]: SubstitutionValue;
} }
type Tags = Record<string, (sub: string) => React.ReactNode>; type Tags = Record<string, SubstitutionValue>;
export type TranslatedString = string | React.ReactNode; export type TranslatedString = string | React.ReactNode;
@ -247,7 +249,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri
let replaced; let replaced;
// If substitution is a function, call it // If substitution is a function, call it
if (mapping[regexpString] instanceof Function) { if (mapping[regexpString] instanceof Function) {
replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups); replaced = ((mapping as Tags)[regexpString] as Function)(...capturedGroups);
} else { } else {
replaced = mapping[regexpString]; replaced = mapping[regexpString];
} }