Merge branch 'develop' into gsouquet/switch-rooms

pull/21833/head
Germain Souquet 2021-05-27 10:22:16 +01:00
commit 51d88062f9
10 changed files with 559 additions and 418 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
version: string;
[lang: string]: {
url: string;
};
[lang: string]: LocalisedPolicy;
}
type Policies = {
export type Policies = {
[policy: string]: Policy,
};

View File

@ -69,7 +69,7 @@ interface IState {
contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean;
selectedSpace?: Room;
pendingRoomJoin: string[]
pendingRoomJoin: Set<string>;
}
@replaceableComponent("structures.UserMenu")
@ -86,7 +86,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.state = {
contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(),
pendingRoomJoin: [],
pendingRoomJoin: new Set<string>(),
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
@ -106,6 +106,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
MatrixClientPeg.get().on("Room", this.onRoom);
}
public componentWillUnmount() {
@ -117,6 +118,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_spaces")) {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
MatrixClientPeg.get().removeListener("Room", this.onRoom);
}
private onRoom = (room: Room): void => {
this.removePendingJoinRoom(room.roomId);
}
private onTagStoreUpdate = () => {
@ -168,30 +174,21 @@ export default class UserMenu extends React.Component<IProps, IState> {
}
};
private addPendingJoinRoom(roomId) {
private addPendingJoinRoom(roomId: string): void {
this.setState({
pendingRoomJoin: [
...this.state.pendingRoomJoin,
roomId,
],
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
.add(roomId),
});
}
private removePendingJoinRoom(roomId) {
const newPendingRoomJoin = this.state.pendingRoomJoin.filter(pendingJoinRoomId => {
return pendingJoinRoomId !== roomId;
});
if (newPendingRoomJoin.length !== this.state.pendingRoomJoin.length) {
private removePendingJoinRoom(roomId: string): void {
if (this.state.pendingRoomJoin.delete(roomId)) {
this.setState({
pendingRoomJoin: newPendingRoomJoin,
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
})
}
}
get hasPendingActions(): boolean {
return this.state.pendingRoomJoin.length > 0;
}
private onOpenMenuClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -653,11 +650,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
</span>
{name}
{this.hasPendingActions && (
{this.state.pendingRoomJoin.size > 0 && (
<InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: this.state.pendingRoomJoin.length },
{ count: this.state.pendingRoomJoin.size },
)} />
</InlineSpinner>
)}

View File

@ -1,7 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (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.
*/
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
import classNames from 'classnames';
import { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms';
/* This file contains a collection of components which are used by the
* 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.
*/
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;
@replaceableComponent("views.auth.PasswordAuthEntry")
export class PasswordAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.password";
interface IAuthEntryProps {
matrixClient: MatrixClient;
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 = {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
// is the auth logic currently waiting for something to
// happen?
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
};
interface IPasswordAuthEntryState {
password: string;
}
@replaceableComponent("views.auth.PasswordAuthEntry")
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
static LOGIN_TYPE = AuthType.Password;
constructor(props) {
super(props);
this.state = {
password: "",
};
}
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
state = {
password: "",
};
_onSubmit = e => {
private onSubmit = (e: FormEvent) => {
e.preventDefault();
if (this.props.busy) return;
this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE,
type: AuthType.Password,
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
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
this.setState({
password: ev.target.value,
@ -123,7 +158,7 @@ export class PasswordAuthEntry extends React.Component {
};
render() {
const passwordBoxClass = classnames({
const passwordBoxClass = classNames({
"error": this.props.errorText,
});
@ -155,7 +190,7 @@ export class PasswordAuthEntry extends React.Component {
return (
<div>
<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
className={passwordBoxClass}
type="password"
@ -163,7 +198,7 @@ export class PasswordAuthEntry extends React.Component {
label={_t('Password')}
autoFocus={true}
value={this.state.password}
onChange={this._onPasswordFieldChange}
onChange={this.onPasswordFieldChange}
/>
<div className="mx_button_row">
{ submitButtonOrSpinner }
@ -175,26 +210,26 @@ export class PasswordAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.recaptcha";
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
/* eslint-disable camelcase */
interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
stageParams?: {
public_key?: string;
};
}
/* eslint-enable camelcase */
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
static LOGIN_TYPE = AuthType.Recaptcha;
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
_onCaptchaResponse = response => {
private onCaptchaResponse = (response: string) => {
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE,
type: AuthType.Recaptcha,
response: response,
});
};
@ -230,7 +265,7 @@ export class RecaptchaAuthEntry extends React.Component {
return (
<div>
<CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse}
onCaptchaResponse={this.onCaptchaResponse}
/>
{ errorSection }
</div>
@ -238,18 +273,28 @@ export class RecaptchaAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.terms";
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
interface ITermsAuthEntryProps extends IAuthEntryProps {
stageParams?: {
policies?: Policies;
};
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) {
super(props);
@ -294,8 +339,11 @@ export class TermsAuthEntry extends React.Component {
initToggles[policyId] = false;
langPolicy.id = policyId;
pickedPolicies.push(langPolicy);
pickedPolicies.push({
id: policyId,
name: langPolicy.name,
url: langPolicy.url,
});
}
this.state = {
@ -311,11 +359,11 @@ export class TermsAuthEntry extends React.Component {
this.props.onPhaseChange(DEFAULT_PHASE);
}
tryContinue = () => {
this._trySubmit();
public tryContinue = () => {
this.trySubmit();
};
_togglePolicy(policyId) {
private togglePolicy(policyId: string) {
const newToggles = {};
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
@ -326,7 +374,7 @@ export class TermsAuthEntry extends React.Component {
this.setState({"toggledPolicies": newToggles});
}
_trySubmit = () => {
private trySubmit = () => {
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
@ -334,7 +382,7 @@ export class TermsAuthEntry extends React.Component {
}
if (allChecked) {
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
this.props.submitAuthDict({type: AuthType.Terms});
CountlyAnalytics.instance.track("onboarding_terms_complete");
} else {
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(
// XXX: replace with StyledCheckbox
<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>
</label>,
);
@ -375,7 +423,7 @@ export class TermsAuthEntry extends React.Component {
if (this.props.showContinue !== false) {
// XXX: button classes
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 (
@ -389,21 +437,18 @@ export class TermsAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.email.identity";
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,
interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
inputs?: {
emailAddress?: string;
};
stageState?: {
emailSid: string;
};
}
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
static LOGIN_TYPE = AuthType.Email;
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
@ -427,7 +472,7 @@ export class EmailIdentityAuthEntry extends React.Component {
return (
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
<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>{ _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")
export class MsisdnAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.msisdn";
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
static LOGIN_TYPE = AuthType.Msisdn;
static propTypes = {
inputs: PropTypes.shape({
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
}),
fail: PropTypes.func,
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
onPhaseChange: PropTypes.func.isRequired,
};
private submitUrl: string;
private sid: string;
private msisdn: string;
state = {
token: '',
requestingToken: false,
};
constructor(props) {
super(props);
this.state = {
token: '',
requestingToken: false,
errorText: '',
};
}
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
this._submitUrl = null;
this._sid = null;
this._msisdn = null;
this._tokenBox = null;
this.setState({requestingToken: true});
this._requestMsisdnToken().catch((e) => {
this.requestMsisdnToken().catch((e) => {
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
@ -477,26 +529,26 @@ export class MsisdnAuthEntry extends React.Component {
/*
* Requests a verification token by SMS.
*/
_requestMsisdnToken() {
private requestMsisdnToken(): Promise<void> {
return this.props.matrixClient.requestRegisterMsisdnToken(
this.props.inputs.phoneCountry,
this.props.inputs.phoneNumber,
this.props.clientSecret,
1, // TODO: Multiple send attempts?
).then((result) => {
this._submitUrl = result.submit_url;
this._sid = result.sid;
this._msisdn = result.msisdn;
this.submitUrl = result.submit_url;
this.sid = result.sid;
this.msisdn = result.msisdn;
});
}
_onTokenChange = e => {
private onTokenChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
token: e.target.value,
});
};
_onFormSubmit = async e => {
private onFormSubmit = async (e: FormEvent) => {
e.preventDefault();
if (this.state.token == '') return;
@ -506,20 +558,20 @@ export class MsisdnAuthEntry extends React.Component {
try {
let result;
if (this._submitUrl) {
if (this.submitUrl) {
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 {
throw new Error("The registration with MSISDN flow is misconfigured");
}
if (result.success) {
const creds = {
sid: this._sid,
sid: this.sid,
client_secret: this.props.clientSecret,
};
this.props.submitAuthDict({
type: MsisdnAuthEntry.LOGIN_TYPE,
type: AuthType.Msisdn,
// 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
@ -543,7 +595,7 @@ export class MsisdnAuthEntry extends React.Component {
return <Loader />;
} else {
const enableSubmit = Boolean(this.state.token);
const submitClasses = classnames({
const submitClasses = classNames({
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
mx_GeneralButton: true,
});
@ -558,16 +610,16 @@ export class MsisdnAuthEntry extends React.Component {
return (
<div>
<p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>{ this._msisdn }</i> },
{ msisdn: <i>{ this.msisdn }</i> },
) }
</p>
<p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}>
<form onSubmit={this.onFormSubmit}>
<input type="text"
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
value={this.state.token}
onChange={this._onTokenChange}
onChange={this.onTokenChange}
aria-label={ _t("Code")}
/>
<br />
@ -584,40 +636,40 @@ export class MsisdnAuthEntry extends React.Component {
}
}
@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
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,
};
interface ISSOAuthEntryProps extends IAuthEntryProps {
continueText?: string;
continueKind?: string;
onCancel?: () => void;
}
static LOGIN_TYPE = "m.login.sso";
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
interface ISSOAuthEntryState {
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_POSTAUTH = 2; // button to confirm SSO completed
_ssoUrl: string;
private ssoUrl: string;
private popupWindow: Window;
constructor(props) {
super(props);
// We actually send the user through fallback auth so we don't have to
// 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.authSessionId,
);
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
this.popupWindow = null;
window.addEventListener("message", this.onReceiveMessage);
this.state = {
phase: SSOAuthEntry.PHASE_PREAUTH,
@ -625,44 +677,44 @@ export class SSOAuthEntry extends React.Component {
};
}
componentDidMount(): void {
componentDidMount() {
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
}
componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage);
if (this._popupWindow) {
this._popupWindow.close();
this._popupWindow = null;
window.removeEventListener("message", this.onReceiveMessage);
if (this.popupWindow) {
this.popupWindow.close();
this.popupWindow = null;
}
}
attemptFailed = () => {
public attemptFailed = () => {
this.setState({
attemptFailed: true,
});
};
_onReceiveMessage = event => {
private onReceiveMessage = (event: MessageEvent) => {
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
if (this._popupWindow) {
this._popupWindow.close();
this._popupWindow = null;
if (this.popupWindow) {
this.popupWindow.close();
this.popupWindow = null;
}
}
};
onStartAuthClick = () => {
private onStartAuthClick = () => {
// 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
// context.
this._popupWindow = window.open(this._ssoUrl, "_blank");
this.popupWindow = window.open(this.ssoUrl, "_blank");
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
};
onConfirmClick = () => {
private onConfirmClick = () => {
this.props.submitAuthDict({});
};
@ -716,46 +768,37 @@ export class SSOAuthEntry extends React.Component {
}
@replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
};
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
private popupWindow: Window;
private fallbackButton = createRef<HTMLAnchorElement>();
constructor(props) {
super(props);
// we have to make the user click a button, as browsers will block
// the popup if we open it immediately.
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
this._fallbackButton = createRef();
this.popupWindow = null;
window.addEventListener("message", this.onReceiveMessage);
}
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage);
if (this._popupWindow) {
this._popupWindow.close();
window.removeEventListener("message", this.onReceiveMessage);
if (this.popupWindow) {
this.popupWindow.close();
}
}
focus = () => {
if (this._fallbackButton.current) {
this._fallbackButton.current.focus();
public focus = () => {
if (this.fallbackButton.current) {
this.fallbackButton.current.focus();
}
};
_onShowFallbackClick = e => {
private onShowFallbackClick = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@ -763,10 +806,10 @@ export class FallbackAuthEntry extends React.Component {
this.props.loginType,
this.props.authSessionId,
);
this._popupWindow = window.open(url, "_blank");
this.popupWindow = window.open(url, "_blank");
};
_onReceiveMessage = event => {
private onReceiveMessage = (event: MessageEvent) => {
if (
event.data === "authDone" &&
event.origin === this.props.matrixClient.getHomeserverUrl()
@ -786,27 +829,31 @@ export class FallbackAuthEntry extends React.Component {
}
return (
<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}
</div>
);
}
}
const AuthEntryComponents = [
PasswordAuthEntry,
RecaptchaAuthEntry,
EmailIdentityAuthEntry,
MsisdnAuthEntry,
TermsAuthEntry,
SSOAuthEntry,
];
export default function getEntryComponentForLoginType(loginType) {
for (const c of AuthEntryComponents) {
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
return c;
}
export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
switch (loginType) {
case AuthType.Password:
return PasswordAuthEntry;
case AuthType.Recaptcha:
return RecaptchaAuthEntry;
case AuthType.Email:
return EmailIdentityAuthEntry;
case AuthType.Msisdn:
return MsisdnAuthEntry;
case AuthType.Terms:
return TermsAuthEntry;
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 2018-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (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.
*/
import React, {useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react';
import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import {
PHASE_UNSENT,
@ -30,27 +30,33 @@ import {
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
VerificationRequest,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import WidgetStore from "../../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import {SETTINGS} from "../../../settings/Settings";
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { SETTINGS } from "../../../settings/Settings";
import SettingsStore, { LEVEL_ORDER } from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SettingLevel } from '../../../settings/SettingLevel';
class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
interface IGenericEditorProps {
onBack: () => void;
}
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
this.onBack = this.onBack.bind(this);
}
interface IGenericEditorState {
message?: string;
[inputId: string]: boolean | string;
}
onBack() {
abstract class GenericEditor<
P extends IGenericEditorProps = IGenericEditorProps,
S extends IGenericEditorState = IGenericEditorState,
> extends React.PureComponent<P, S> {
protected onBack = () => {
if (this.state.message) {
this.setState({ message: null });
} 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});
}
_buttons() {
protected abstract send();
protected buttons(): React.ReactNode {
return <div className="mx_Dialog_buttons">
<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>;
}
textInput(id, label) {
protected textInput(id: string, label: string): React.ReactNode {
return <Field
id={id}
label={label}
size="42"
size={42}
autoFocus={true}
type="text"
autoComplete="on"
value={this.state[id]}
onChange={this._onChange}
value={this.state[id] as string}
onChange={this.onChange}
/>;
}
}
export class SendCustomEvent extends GenericEditor {
static getLabel() { return _t('Send Custom Event'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
forceStateEvent: PropTypes.bool,
forceGeneralEvent: PropTypes.bool,
inputs: PropTypes.object,
interface ISendCustomEventProps extends IGenericEditorProps {
room: Room;
forceStateEvent?: boolean;
forceGeneralEvent?: boolean;
inputs?: {
eventType?: string;
stateKey?: string;
evContent?: string;
};
}
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;
constructor(props) {
super(props);
this._send = this._send.bind(this);
const {eventType, stateKey, evContent} = Object.assign({
eventType: '',
@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor {
};
}
send(content) {
private doSend(content: object): Promise<void> {
const cli = this.context;
if (this.state.isStateEvent) {
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 === '') {
this.setState({ message: _t('You must specify an event type!') });
return;
@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor {
let message;
try {
const content = JSON.parse(this.state.evContent);
await this.send(content);
await this.doSend(content);
message = _t('Event sent!');
} catch (e) {
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
<div className="mx_Dialog_content">
{ this.state.message }
</div>
{ this._buttons() }
{ this.buttons() }
</div>;
}
@ -163,35 +182,51 @@ export class SendCustomEvent extends GenericEditor {
<br />
<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 className="mx_Dialog_buttons">
<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"}}>
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
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>;
}
}
class SendAccountData extends GenericEditor {
static getLabel() { return _t('Send Account Data'); }
static propTypes = {
room: PropTypes.instanceOf(Room).isRequired,
isRoomAccountData: PropTypes.bool,
forceMode: PropTypes.bool,
inputs: PropTypes.object,
interface ISendAccountDataProps extends IGenericEditorProps {
room: Room;
isRoomAccountData: boolean;
forceMode: boolean;
inputs?: {
eventType?: string;
evContent?: string;
};
}
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;
constructor(props) {
super(props);
this._send = this._send.bind(this);
const {eventType, evContent} = Object.assign({
eventType: '',
@ -206,7 +241,7 @@ class SendAccountData extends GenericEditor {
};
}
send(content) {
private doSend(content: object): Promise<void> {
const cli = this.context;
if (this.state.isRoomAccountData) {
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);
}
async _send() {
protected send = async () => {
if (this.state.eventType === '') {
this.setState({ message: _t('You must specify an event type!') });
return;
@ -223,7 +258,7 @@ class SendAccountData extends GenericEditor {
let message;
try {
const content = JSON.parse(this.state.evContent);
await this.send(content);
await this.doSend(content);
message = _t('Event sent!');
} catch (e) {
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
@ -237,7 +272,7 @@ class SendAccountData extends GenericEditor {
<div className="mx_Dialog_content">
{ this.state.message }
</div>
{ this._buttons() }
{ this.buttons() }
</div>;
}
@ -247,14 +282,23 @@ class SendAccountData extends GenericEditor {
<br />
<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 className="mx_Dialog_buttons">
<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"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
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>;
@ -264,17 +308,22 @@ class SendAccountData extends GenericEditor {
const INITIAL_LOAD_TILES = 20;
const LOAD_TILES_STEP_SIZE = 50;
class FilteredList extends React.PureComponent {
static propTypes = {
children: PropTypes.any,
query: PropTypes.string,
onChange: PropTypes.func,
};
interface IFilteredListProps {
children: React.ReactElement[];
query: string;
onChange: (value: string) => void;
}
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;
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) {
@ -295,27 +344,27 @@ class FilteredList extends React.PureComponent {
});
}
showAll = () => {
private showAll = () => {
this.setState({
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}>
{ _t("and %(count)s others...", { count: overflowCount }) }
</button>;
};
onQuery = (ev) => {
private onQuery = (ev: ChangeEvent<HTMLInputElement>) => {
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);
};
getChildCount = (): number => {
private getChildCount = (): number => {
return this.state.filteredChildren.length;
};
@ -336,28 +385,31 @@ class FilteredList extends React.PureComponent {
}
}
class RoomStateExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Room State'); }
interface IExplorerProps {
room: Room;
onBack: () => void;
}
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
interface IRoomStateExplorerState {
eventType?: string;
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;
roomStateEvents: Map<string, Map<string, MatrixEvent>>;
private roomStateEvents: Map<string, Map<string, MatrixEvent>>;
constructor(props) {
super(props);
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 = {
eventType: null,
event: null,
@ -368,19 +420,19 @@ class RoomStateExplorer extends React.PureComponent {
};
}
browseEventType(eventType) {
private browseEventType(eventType: string) {
return () => {
this.setState({ eventType });
};
}
onViewSourceClick(event) {
private onViewSourceClick(event: MatrixEvent) {
return () => {
this.setState({ event });
};
}
onBack() {
private onBack = () => {
if (this.state.editing) {
this.setState({ editing: false });
} else if (this.state.event) {
@ -392,15 +444,15 @@ class RoomStateExplorer extends React.PureComponent {
}
}
editEv() {
private editEv = () => {
this.setState({ editing: true });
}
onQueryEventType(filterEventType) {
private onQueryEventType = (filterEventType: string) => {
this.setState({ queryEventType: filterEventType });
}
onQueryStateKey(filterStateKey) {
private onQueryStateKey = (filterStateKey: string) => {
this.setState({ queryStateKey: filterStateKey });
}
@ -472,24 +524,22 @@ class RoomStateExplorer extends React.PureComponent {
}
}
class AccountDataExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Account Data'); }
interface IAccountDataExplorerState {
isRoomAccountData: boolean;
event?: MatrixEvent;
editing: boolean;
queryEventType: string;
[inputId: string]: boolean | string;
}
static propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
static getLabel() { return _t('Explore Account Data'); }
static contextType = MatrixClientContext;
constructor(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 = {
isRoomAccountData: false,
event: null,
@ -499,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent {
};
}
getData() {
private getData(): Record<string, MatrixEvent> {
if (this.state.isRoomAccountData) {
return this.props.room.accountData;
}
return this.context.store.accountData;
}
onViewSourceClick(event) {
private onViewSourceClick(event: MatrixEvent) {
return () => {
this.setState({ event });
};
}
onBack() {
private onBack = () => {
if (this.state.editing) {
this.setState({ editing: false });
} 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});
}
editEv() {
private editEv = () => {
this.setState({ editing: true });
}
onQueryEventType(queryEventType) {
private onQueryEventType = (queryEventType: string) => {
this.setState({ queryEventType });
}
@ -580,30 +630,39 @@ class AccountDataExplorer extends React.PureComponent {
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !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} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
</div> }
<div style={{float: "right"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
type="checkbox"
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>;
}
}
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 propTypes = {
onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
};
static contextType = MatrixClientContext;
private servers: React.ReactElement[];
constructor(props) {
super(props);
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]));
this.servers = Array.from(servers).map(s =>
<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 });
}
@ -642,7 +701,10 @@ const PHASE_MAP = {
[PHASE_CANCELLED]: "cancelled",
};
function VerificationRequest({txnId, request}) {
const VerificationRequestExplorer: React.FC<{
txnId: string;
request: VerificationRequest;
}> = ({txnId, request}) => {
const [, updateState] = useState();
const [timeout, setRequestTimeout] = useState(request.timeout);
@ -679,7 +741,7 @@ function VerificationRequest({txnId, request}) {
</div>);
}
class VerificationExplorer extends React.Component {
class VerificationExplorer extends React.PureComponent<IExplorerProps> {
static getLabel() {
return _t("Verification Requests");
}
@ -687,7 +749,7 @@ class VerificationExplorer extends React.Component {
/* Ensure this.context is the cli */
static contextType = MatrixClientContext;
onNewRequest = () => {
private onNewRequest = () => {
this.forceUpdate();
}
@ -710,7 +772,7 @@ class VerificationExplorer extends React.Component {
return (<div>
<div className="mx_Dialog_content">
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
<VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
)}
</div>
<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() {
return _t("Active Widgets");
}
@ -734,19 +801,19 @@ class WidgetExplorer extends React.Component {
};
}
onWidgetStoreUpdate = () => {
private onWidgetStoreUpdate = () => {
this.forceUpdate();
};
onQueryChange = (query) => {
private onQueryChange = (query: string) => {
this.setState({query});
};
onEditWidget = (widget) => {
private onEditWidget = (widget: IApp) => {
this.setState({editWidget: widget});
};
onBack = () => {
private onBack = () => {
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
this.setState({editWidget: null});
@ -769,8 +836,11 @@ class WidgetExplorer extends React.Component {
const editWidget = this.state.editWidget;
const widgets = WidgetStore.instance.getApps(room.roomId);
if (editWidget && widgets.includes(editWidget)) {
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values()))
.reduce((p, c) => {p.push(...c); return p;}, []);
const allState = Array.from(
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);
if (!stateEv) { // "should never happen"
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() {
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});
};
onExplValuesEdit = (ev) => {
private onExplValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
this.setState({explicitValues: ev.target.value});
};
onExplRoomValuesEdit = (ev) => {
private onExplRoomValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
this.setState({explicitRoomValues: ev.target.value});
};
onBack = () => {
private onBack = () => {
if (this.state.editSetting) {
this.setState({editSetting: null});
} 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();
this.setState({viewSetting: settingId});
};
onEditClick = (ev, settingId) => {
private onEditClick = (ev: MouseEvent, settingId: string) => {
ev.preventDefault();
this.setState({
editSetting: settingId,
@ -865,7 +943,7 @@ class SettingsExplorer extends React.Component {
});
};
onSaveClick = async () => {
private onSaveClick = async () => {
try {
const settingId = this.state.editSetting;
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`);
try {
const val = parsedExplicit[level];
await SettingsStore.setValue(settingId, null, level, val);
await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
} catch (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`);
try {
const val = parsedExplicitRoom[level];
await SettingsStore.setValue(settingId, roomId, level, val);
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);
} catch (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
const toStringTypes = ['boolean', 'number'];
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 = {};
for (const level of LEVEL_ORDER) {
try {
@ -926,7 +1004,7 @@ class SettingsExplorer extends React.Component {
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 className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
return <td className={className}><code>{canEdit.toString()}</code></td>;
@ -1062,27 +1140,37 @@ class SettingsExplorer extends React.Component {
<div>
{_t("Value:")}&nbsp;
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
<code>{this.renderSettingValue(
SettingsStore.getValue(this.state.viewSetting),
)}</code>
</div>
<div>
{_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>
{_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>
{_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 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>
</div>
</div>
@ -1091,7 +1179,11 @@ class SettingsExplorer extends React.Component {
}
}
const Entries = [
type DevtoolsDialogEntry = React.JSXElementConstructor<any> & {
getLabel: () => string;
};
const Entries: DevtoolsDialogEntry[] = [
SendCustomEvent,
RoomStateExplorer,
SendAccountData,
@ -1102,43 +1194,36 @@ const Entries = [
SettingsExplorer,
];
@replaceableComponent("views.dialogs.DevtoolsDialog")
export default class DevtoolsDialog extends React.PureComponent {
static propTypes = {
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
interface IProps {
roomId: string;
onFinished: (finished: boolean) => void;
}
interface IState {
mode?: DevtoolsDialogEntry;
}
@replaceableComponent("views.dialogs.DevtoolsDialog")
export default class DevtoolsDialog extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);
this.onBack = this.onBack.bind(this);
this.onCancel = this.onCancel.bind(this);
this.state = {
mode: null,
};
}
componentWillUnmount() {
this._unmounted = true;
}
_setMode(mode) {
private setMode(mode: DevtoolsDialogEntry) {
return () => {
this.setState({ mode });
};
}
onBack() {
if (this.prevMode) {
this.setState({ mode: this.prevMode });
this.prevMode = null;
} else {
this.setState({ mode: null });
}
private onBack = () => {
this.setState({ mode: null });
}
onCancel() {
private onCancel = () => {
this.props.onFinished(false);
}
@ -1165,7 +1250,7 @@ export default class DevtoolsDialog extends React.PureComponent {
<div className="mx_Dialog_content">
{ Entries.map((Entry) => {
const label = Entry.getLabel();
const onClick = this._setMode(Entry);
const onClick = this.setMode(Entry);
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
}) }
</div>

View File

@ -71,10 +71,14 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
// We should have a buffer to work with now: let's set it up
const playback = new Playback(buffer, waveform);
this.setState({playback});
this.setState({ playback });
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
}
public componentWillUnmount() {
this.state.playback?.destroy();
}
public render() {
if (this.state.error) {
// TODO: @@TR: Verify error state

View File

@ -81,19 +81,39 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
constructor(props, context) {
super(props, context);
if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.remove", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
this.state = {
myReactions: this.getMyReactions(),
showAll: false,
};
}
componentDidUpdate(prevProps) {
componentDidMount() {
const { mxEvent, reactions } = this.props;
if (mxEvent.isBeingDecrypted() || mxEvent.shouldAttemptDecryption()) {
mxEvent.once("Event.decrypted", this.onDecrypted);
}
if (reactions) {
reactions.on("Relations.add", this.onReactionsChange);
reactions.on("Relations.remove", this.onReactionsChange);
reactions.on("Relations.redaction", this.onReactionsChange);
}
}
componentWillUnmount() {
const { mxEvent, reactions } = this.props;
mxEvent.off("Event.decrypted", this.onDecrypted);
if (reactions) {
reactions.off("Relations.add", this.onReactionsChange);
reactions.off("Relations.remove", this.onReactionsChange);
reactions.off("Relations.redaction", this.onReactionsChange);
}
}
componentDidUpdate(prevProps: IProps) {
if (prevProps.reactions !== this.props.reactions) {
this.props.reactions.on("Relations.add", this.onReactionsChange);
this.props.reactions.on("Relations.remove", this.onReactionsChange);
@ -102,24 +122,12 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
}
}
componentWillUnmount() {
if (this.props.reactions) {
this.props.reactions.removeListener(
"Relations.add",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.remove",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
);
}
private onDecrypted = () => {
// Decryption changes whether the event is actionable
this.forceUpdate();
}
onReactionsChange = () => {
private onReactionsChange = () => {
// TODO: Call `onHeightChanged` as needed
this.setState({
myReactions: this.getMyReactions(),
@ -130,7 +138,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
this.forceUpdate();
}
getMyReactions() {
private getMyReactions() {
const reactions = this.props.reactions;
if (!reactions) {
return null;
@ -143,7 +151,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
return [...myReactions.values()];
}
onShowAllClick = () => {
private onShowAllClick = () => {
this.setState({
showAll: true,
});

View File

@ -812,13 +812,6 @@ export default class EventTile extends React.Component<IProps, IState> {
return null;
}
const eventId = this.props.mxEvent.getId();
if (!eventId) {
// XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120
console.error("EventTile attempted to get relations for an event without an ID");
// Use event's special `toJSON` method to log key data.
console.log(JSON.stringify(this.props.mxEvent, null, 4));
console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
}
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
};

View File

@ -152,5 +152,5 @@ export enum Action {
/**
* Fired when joining a room failed
*/
JoinRoomError = "join_room",
JoinRoomError = "join_room_error",
}

View File

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

View File

@ -176,7 +176,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
const event = payload.event; // TODO: Type out the dispatcher
if (!this.previews.has(event.getRoomId()) || !event.isLiveEvent) return; // not important
const isHistoricalEvent = payload.hasOwnProperty("isLiveEvent") && !payload.isLiveEvent
if (!this.previews.has(event.getRoomId()) || isHistoricalEvent) return; // not important
await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
}
}