Merge branches 'develop' and 't3chguy/leaks' of github.com:matrix-org/matrix-react-sdk into t3chguy/leaks
Conflicts: src/components/views/avatars/BaseAvatar.js
This commit is contained in:
commit
5844bee8f2
771 changed files with 25042 additions and 9044 deletions
|
@ -26,7 +26,7 @@ export default createReactClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_AuthFooter">
|
||||
<a href="https://matrix.org" target="_blank" rel="noopener">{ _t("powered by Matrix") }</a>
|
||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -16,12 +16,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'AuthHeader',
|
||||
|
||||
propTypes: {
|
||||
disableLanguageSelector: PropTypes.bool,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
|
||||
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
|
||||
|
@ -29,7 +34,7 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_AuthHeader">
|
||||
<AuthHeaderLogo />
|
||||
<LanguageSelector />
|
||||
<LanguageSelector disabled={this.props.disableLanguageSelector} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -46,7 +46,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._captchaWidgetId = null;
|
||||
|
||||
this._recaptchaContainer = createRef();
|
||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
import { COUNTRIES } from '../../../phonenumber';
|
||||
import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
|
@ -60,7 +60,7 @@ export default class CountryDropdown extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (!this.props.value) {
|
||||
// If no value is given, we start with the default
|
||||
// country selected, but our parent component
|
||||
|
@ -80,7 +80,7 @@ export default class CountryDropdown extends React.Component {
|
|||
}
|
||||
|
||||
_flagImgForIso2(iso2) {
|
||||
return <img src={require(`../../../../res/img/flags/${iso2}.png`)} />;
|
||||
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
|
||||
}
|
||||
|
||||
_getShortOption(iso2) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 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.
|
||||
|
@ -25,6 +25,7 @@ import classnames from 'classnames';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -59,11 +60,21 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
* session to be failed and the process to go back to the start.
|
||||
* setEmailSid: m.login.email.identity only: a function to be called with the
|
||||
* email sid after a token is requested.
|
||||
* onPhaseChange: A function which is called when the stage's phase changes. If
|
||||
* the stage has no phases, call this with DEFAULT_PHASE. Takes
|
||||
* one argument, the phase, and is always defined/required.
|
||||
* continueText: For stages which have a continue button, the text to use.
|
||||
* continueKind: For stages which have a continue button, the style of button to
|
||||
* use. For example, 'danger' or 'primary'.
|
||||
* onCancel A function with no arguments which is called by the stage if the
|
||||
* user knowingly cancelled/dismissed the authentication attempt.
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* focus: set the input focus appropriately in the form.
|
||||
*/
|
||||
|
||||
export const DEFAULT_PHASE = 0;
|
||||
|
||||
export const PasswordAuthEntry = createReactClass({
|
||||
displayName: 'PasswordAuthEntry',
|
||||
|
||||
|
@ -78,6 +89,11 @@ export const PasswordAuthEntry = createReactClass({
|
|||
// is the auth logic currently waiting for something to
|
||||
// happen?
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -145,7 +161,6 @@ export const PasswordAuthEntry = createReactClass({
|
|||
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
id="mx_InteractiveAuthEntryComponents_password"
|
||||
className={passwordBoxClass}
|
||||
type="password"
|
||||
name="passwordField"
|
||||
|
@ -176,6 +191,11 @@ export const RecaptchaAuthEntry = createReactClass({
|
|||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
_onCaptchaResponse: function(response) {
|
||||
|
@ -237,8 +257,14 @@ export const TermsAuthEntry = createReactClass({
|
|||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
showContinue: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
componentWillMount: function() {
|
||||
// example stageParams:
|
||||
//
|
||||
|
@ -331,7 +357,7 @@ export const TermsAuthEntry = createReactClass({
|
|||
checkboxes.push(
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
</label>,
|
||||
);
|
||||
}
|
||||
|
@ -379,6 +405,11 @@ export const EmailIdentityAuthEntry = createReactClass({
|
|||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -421,6 +452,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
clientSecret: PropTypes.func,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -430,7 +462,9 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
|
||||
this._submitUrl = null;
|
||||
this._sid = null;
|
||||
this._msisdn = null;
|
||||
|
@ -565,6 +599,91 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
},
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
static LOGIN_TYPE = "m.login.sso";
|
||||
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
|
||||
|
||||
static PHASE_PREAUTH = 1; // button to start SSO
|
||||
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
||||
|
||||
_ssoUrl: string;
|
||||
|
||||
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.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
window.open(this._ssoUrl, '_blank');
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
|
||||
onConfirmClick = () => {
|
||||
this.props.submitAuthDict({});
|
||||
};
|
||||
|
||||
render() {
|
||||
let continueButton = null;
|
||||
const cancelButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.props.onCancel}
|
||||
kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
|
||||
>{_t("Cancel")}</AccessibleButton>
|
||||
);
|
||||
if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
|
||||
continueButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.onStartAuthClick}
|
||||
kind={this.props.continueKind || 'primary'}
|
||||
>{this.props.continueText || _t("Single Sign On")}</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
continueButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.onConfirmClick}
|
||||
kind={this.props.continueKind || 'primary'}
|
||||
>{this.props.continueText || _t("Confirm")}</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export const FallbackAuthEntry = createReactClass({
|
||||
displayName: 'FallbackAuthEntry',
|
||||
|
||||
|
@ -574,9 +693,15 @@ export const FallbackAuthEntry = createReactClass({
|
|||
loginType: PropTypes.string.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
// we have to make the user click a button, as browsers will block
|
||||
// the popup if we open it immediately.
|
||||
this._popupWindow = null;
|
||||
|
@ -598,12 +723,16 @@ export const FallbackAuthEntry = createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onShowFallbackClick: function() {
|
||||
_onShowFallbackClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const url = this.props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this._popupWindow.opener = null;
|
||||
},
|
||||
|
||||
_onReceiveMessage: function(event) {
|
||||
|
@ -626,7 +755,7 @@ export const FallbackAuthEntry = createReactClass({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
|
@ -639,11 +768,12 @@ const AuthEntryComponents = [
|
|||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
TermsAuthEntry,
|
||||
SSOAuthEntry,
|
||||
];
|
||||
|
||||
export default function getEntryComponentForLoginType(loginType) {
|
||||
for (const c of AuthEntryComponents) {
|
||||
if (c.LOGIN_TYPE == loginType) {
|
||||
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,12 +28,14 @@ function onChange(newLang) {
|
|||
}
|
||||
}
|
||||
|
||||
export default function LanguageSelector() {
|
||||
export default function LanguageSelector({disabled}) {
|
||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <LanguageDropdown className="mx_AuthBody_language"
|
||||
return <LanguageDropdown
|
||||
className="mx_AuthBody_language"
|
||||
onOptionChange={onChange}
|
||||
value={getCurrentLanguage()}
|
||||
disabled={disabled}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -99,14 +99,15 @@ export default class ModularServerConfig extends ServerConfig {
|
|||
"Enter the location of your Modular homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>modular.im</a>.",
|
||||
{}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
)}
|
||||
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
|
|
|
@ -23,6 +23,7 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
|
@ -44,6 +45,7 @@ export default class PasswordLogin extends React.Component {
|
|||
loginIncorrect: PropTypes.bool,
|
||||
disableSubmit: PropTypes.bool,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
busy: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -183,7 +185,7 @@ export default class PasswordLogin extends React.Component {
|
|||
this.props.onPasswordChanged(ev.target.value);
|
||||
}
|
||||
|
||||
renderLoginField(loginType) {
|
||||
renderLoginField(loginType, autoFocus) {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
const classes = {};
|
||||
|
@ -193,7 +195,6 @@ export default class PasswordLogin extends React.Component {
|
|||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
id="mx_PasswordLogin_email"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="email_input"
|
||||
type="text"
|
||||
|
@ -203,13 +204,12 @@ export default class PasswordLogin extends React.Component {
|
|||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
id="mx_PasswordLogin_username"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="username_input"
|
||||
type="text"
|
||||
|
@ -218,7 +218,7 @@ export default class PasswordLogin extends React.Component {
|
|||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
|
@ -233,7 +233,6 @@ export default class PasswordLogin extends React.Component {
|
|||
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
id="mx_PasswordLogin_phoneNumber"
|
||||
name="phoneNumber"
|
||||
key="phone_input"
|
||||
type="text"
|
||||
|
@ -243,7 +242,7 @@ export default class PasswordLogin extends React.Component {
|
|||
onChange={this.onPhoneNumberChanged}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
@ -268,12 +267,16 @@ export default class PasswordLogin extends React.Component {
|
|||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = <span>
|
||||
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
|
||||
a: sub => <a className="mx_Login_forgot"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
href="#"
|
||||
>
|
||||
{sub}
|
||||
</a>,
|
||||
a: sub => (
|
||||
<AccessibleButton
|
||||
className="mx_Login_forgot"
|
||||
disabled={this.props.busy}
|
||||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</span>;
|
||||
}
|
||||
|
@ -282,7 +285,10 @@ export default class PasswordLogin extends React.Component {
|
|||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
const loginField = this.renderLoginField(this.state.loginType);
|
||||
// If login is empty, autoFocus login, otherwise autoFocus password.
|
||||
// this is for when auto server discovery remounts us when the user tries to tab from username to password
|
||||
const autoFocusPassword = !this.isLoginEmpty();
|
||||
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
|
||||
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
|
@ -290,7 +296,6 @@ export default class PasswordLogin extends React.Component {
|
|||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Field
|
||||
id="mx_PasswordLogin_type"
|
||||
element="select"
|
||||
value={this.state.loginType}
|
||||
onChange={this.onLoginTypeChange}
|
||||
|
@ -328,20 +333,20 @@ export default class PasswordLogin extends React.Component {
|
|||
{loginField}
|
||||
<Field
|
||||
className={pwFieldClass}
|
||||
id="mx_PasswordLogin_password"
|
||||
type="password"
|
||||
name="password"
|
||||
label={_t('Password')}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChanged}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocusPassword}
|
||||
/>
|
||||
{forgotPasswordJsx}
|
||||
<input className="mx_Login_submit"
|
||||
{ !this.props.busy && <input className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Sign in')}
|
||||
disabled={this.props.disableSubmit}
|
||||
/>
|
||||
/> }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -76,7 +76,7 @@ export default createReactClass({
|
|||
email: this.props.defaultEmail || "",
|
||||
phoneNumber: this.props.defaultPhoneNumber || "",
|
||||
password: this.props.defaultPassword || "",
|
||||
passwordConfirm: "",
|
||||
passwordConfirm: this.props.defaultPassword || "",
|
||||
passwordComplexity: null,
|
||||
passwordSafe: false,
|
||||
};
|
||||
|
@ -102,11 +102,15 @@ export default createReactClass({
|
|||
"No identity server is configured so you cannot add an email address in order to " +
|
||||
"reset your password in the future.",
|
||||
);
|
||||
} else {
|
||||
} else if (this._showEmail()) {
|
||||
desc = _t(
|
||||
"If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?",
|
||||
);
|
||||
} else {
|
||||
// user can't set an e-mail so don't prompt them to
|
||||
self._doSubmit(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
@ -470,7 +474,6 @@ export default createReactClass({
|
|||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
return <Field
|
||||
id="mx_RegistrationForm_email"
|
||||
ref={field => this[FIELD_EMAIL] = field}
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
|
@ -524,7 +527,6 @@ export default createReactClass({
|
|||
onOptionChange={this.onPhoneCountryChange}
|
||||
/>;
|
||||
return <Field
|
||||
id="mx_RegistrationForm_phoneNumber"
|
||||
ref={field => this[FIELD_PHONE_NUMBER] = field}
|
||||
type="text"
|
||||
label={phoneLabel}
|
||||
|
|
|
@ -72,7 +72,8 @@ export default class ServerConfig extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.state.isUrl) return;
|
||||
|
||||
|
@ -223,7 +224,8 @@ export default class ServerConfig extends React.PureComponent {
|
|||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
|
@ -246,7 +248,7 @@ export default class ServerConfig extends React.PureComponent {
|
|||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
<Field
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.serverConfig.isUrl}
|
||||
value={this.state.isUrl || ''}
|
||||
|
|
|
@ -46,7 +46,7 @@ export const TYPES = {
|
|||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
|
|
|
@ -18,6 +18,12 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import AuthPage from "./AuthPage";
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import {_td} from "../../../languageHandler";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
||||
export default class Welcome extends React.PureComponent {
|
||||
render() {
|
||||
|
@ -33,11 +39,25 @@ export default class Welcome extends React.PureComponent {
|
|||
pageUrl = 'welcome.html';
|
||||
}
|
||||
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
const tmpClient = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
const plaf = PlatformPeg.get();
|
||||
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(),
|
||||
this.props.fragmentAfterLogin);
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<div className="mx_Welcome">
|
||||
<EmbeddedPage className="mx_WelcomePage"
|
||||
<EmbeddedPage
|
||||
className="mx_WelcomePage"
|
||||
url={pageUrl}
|
||||
replaceMap={{
|
||||
"$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"),
|
||||
"$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"),
|
||||
}}
|
||||
/>
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import toRem from "../../../utils/rem";
|
||||
|
||||
const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => {
|
||||
const [imageUrls, setUrls] = useState([]);
|
||||
|
@ -104,9 +105,9 @@ const BaseAvatar = (props) => {
|
|||
className="mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px",
|
||||
fontSize: toRem(width * 0.65),
|
||||
width: toRem(width),
|
||||
lineHeight: toRem(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
|
@ -119,8 +120,10 @@ const BaseAvatar = (props) => {
|
|||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
width: toRem(width),
|
||||
height: toRem(height),
|
||||
}}
|
||||
aria-hidden="true" />
|
||||
);
|
||||
|
||||
|
@ -155,7 +158,10 @@ const BaseAvatar = (props) => {
|
|||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
width={width} height={height}
|
||||
style={{
|
||||
width: toRem(width),
|
||||
height: toRem(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
|
@ -166,7 +172,10 @@ const BaseAvatar = (props) => {
|
|||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
width={width} height={height}
|
||||
style={{
|
||||
width: toRem(width),
|
||||
height: toRem(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
|
|
|
@ -51,7 +51,8 @@ export default createReactClass({
|
|||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
||||
this.setState(this._getState(nextProps));
|
||||
},
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
this._button = createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||
}
|
||||
|
|
|
@ -63,7 +63,8 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
|
|
|
@ -61,7 +61,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
|
||||
this._checkPermissions();
|
||||
},
|
||||
|
@ -90,7 +90,8 @@ export default createReactClass({
|
|||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
||||
if (!pinnedEvent) return false;
|
||||
return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId());
|
||||
const content = pinnedEvent.getContent();
|
||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||
},
|
||||
|
||||
onResendClick: function() {
|
||||
|
@ -129,22 +130,24 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onViewSourceClick: function() {
|
||||
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
content: this.props.mxEvent.event,
|
||||
roomId: ev.getRoomId(),
|
||||
eventId: ev.getId(),
|
||||
content: ev.event,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onViewClearSourceClick: function() {
|
||||
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
roomId: ev.getRoomId(),
|
||||
eventId: ev.getId(),
|
||||
// FIXME: _clearEvent is private
|
||||
content: this.props.mxEvent._clearEvent,
|
||||
content: ev._clearEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
@ -420,7 +423,7 @@ export default createReactClass({
|
|||
onClick={this.onPermalinkClick}
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
|
@ -445,7 +448,7 @@ export default createReactClass({
|
|||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
onClick={this.closeMenu}
|
||||
href={mxEvent.event.content.external_url}
|
||||
>
|
||||
|
|
|
@ -82,7 +82,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { user } = this.props;
|
||||
if (!user) {
|
||||
return;
|
||||
|
|
|
@ -26,6 +26,7 @@ import { getHostingLink } from '../../../utils/HostingLink';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import * as sdk from "../../../index";
|
||||
import {getHomePageUrl} from "../../../utils/pages";
|
||||
|
||||
export default class TopLeftMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,15 +48,7 @@ export default class TopLeftMenu extends React.Component {
|
|||
}
|
||||
|
||||
hasHomePage() {
|
||||
const config = SdkConfig.get();
|
||||
const pagesConfig = config.embeddedPages;
|
||||
if (pagesConfig && pagesConfig.homeUrl) {
|
||||
return true;
|
||||
}
|
||||
// This is a deprecated config option for the home page
|
||||
// (despite the name, given we also now have a welcome
|
||||
// page, which is not the same).
|
||||
return !!config.welcomePageUrl;
|
||||
return !!getHomePageUrl(SdkConfig.get());
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -68,10 +61,11 @@ export default class TopLeftMenu extends React.Component {
|
|||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex={-1}>{sub}</a>,
|
||||
a: sub =>
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
|
|
|
@ -107,6 +107,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
},
|
||||
|
|
|
@ -72,7 +72,7 @@ export default createReactClass({
|
|||
<button onClick={this._onInviteNeverWarnClicked}>
|
||||
{ _t('Invite anyway and never warn me again') }
|
||||
</button>
|
||||
<button onClick={this._onInviteClicked} autoFocus="true">
|
||||
<button onClick={this._onInviteClicked} autoFocus={true}>
|
||||
{ _t('Invite anyway') }
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -86,7 +86,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
|
|
|
@ -137,12 +137,20 @@ export default class BugReportDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (window.Modernizr && Object.values(window.Modernizr).some(support => support === false)) {
|
||||
warning = <p><b>
|
||||
{ _t("Reminder: Your browser is unsupported, so your experience may be unpredictable.") }
|
||||
</b></p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
||||
title={_t('Submit debug logs')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ warning }
|
||||
<p>
|
||||
{ _t(
|
||||
"Debug logs contain application usage data including your " +
|
||||
|
@ -166,7 +174,6 @@ export default class BugReportDialog extends React.Component {
|
|||
) }
|
||||
</b></p>
|
||||
<Field
|
||||
id="mx_BugReportDialog_issueUrl"
|
||||
type="text"
|
||||
className="mx_BugReportDialog_field_input"
|
||||
label={_t("GitHub issue")}
|
||||
|
@ -175,7 +182,6 @@ export default class BugReportDialog extends React.Component {
|
|||
placeholder="https://github.com/vector-im/riot-web/issues/..."
|
||||
/>
|
||||
<Field
|
||||
id="mx_BugReportDialog_notes"
|
||||
className="mx_BugReportDialog_field_input"
|
||||
element="textarea"
|
||||
label={_t("Notes")}
|
||||
|
|
|
@ -52,7 +52,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
_elementsForCommit(commit) {
|
||||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noopener">
|
||||
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||
{commit.commit.message.split('\n')[0]}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -55,7 +55,8 @@ export default createReactClass({
|
|||
askReason: false,
|
||||
}),
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._reasonField = null;
|
||||
},
|
||||
|
||||
|
|
|
@ -24,17 +24,20 @@ import withValidation from '../elements/Validation';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
defaultPublic: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
const config = SdkConfig.get();
|
||||
return {
|
||||
isPublic: false,
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: true,
|
||||
name: "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
|
@ -62,6 +65,11 @@ export default createReactClass({
|
|||
if (this.state.noFederate) {
|
||||
createOpts.creation_content = {'m.federate': false};
|
||||
}
|
||||
|
||||
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
|
||||
opts.encryption = this.state.isEncrypted;
|
||||
}
|
||||
|
||||
return opts;
|
||||
},
|
||||
|
||||
|
@ -127,6 +135,10 @@ export default createReactClass({
|
|||
this.setState({isPublic});
|
||||
},
|
||||
|
||||
onEncryptedChange(isEncrypted) {
|
||||
this.setState({isEncrypted});
|
||||
},
|
||||
|
||||
onAliasChange(alias) {
|
||||
this.setState({alias});
|
||||
},
|
||||
|
@ -166,19 +178,31 @@ export default createReactClass({
|
|||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
|
||||
let privateLabel;
|
||||
let publicLabel;
|
||||
let publicPrivateLabel;
|
||||
let aliasField;
|
||||
if (this.state.isPublic) {
|
||||
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
|
||||
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
||||
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
|
||||
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
|
||||
}
|
||||
|
||||
let e2eeSection;
|
||||
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
|
||||
e2eeSection = <React.Fragment>
|
||||
<LabelledToggleSwitch
|
||||
label={ _t("Enable end-to-end encryption")}
|
||||
onChange={this.onEncryptedChange}
|
||||
value={this.state.isEncrypted}
|
||||
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
|
||||
/>
|
||||
<p>{ _t("You can’t disable this later. Bridges & most bots won’t work yet.") }</p>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
||||
|
@ -188,11 +212,11 @@ export default createReactClass({
|
|||
>
|
||||
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
|
||||
<div className="mx_Dialog_content">
|
||||
<Field id="name" ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
||||
<Field id="topic" label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
|
||||
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
||||
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
|
||||
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
||||
{ privateLabel }
|
||||
{ publicLabel }
|
||||
{ publicPrivateLabel }
|
||||
{ e2eeSection }
|
||||
{ aliasField }
|
||||
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 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.
|
||||
|
@ -23,106 +23,160 @@ import Analytics from '../../../Analytics';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
|
||||
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
password: "",
|
||||
busy: false,
|
||||
shouldErase: false,
|
||||
errStr: null,
|
||||
authData: null, // for UIA
|
||||
authEnabled: true, // see usages for information
|
||||
|
||||
// A few strings that are passed to InteractiveAuth for design or are displayed
|
||||
// next to the InteractiveAuth component.
|
||||
bodyText: null,
|
||||
continueText: null,
|
||||
continueKind: null,
|
||||
};
|
||||
|
||||
this._initAuth(/* shouldErase= */false);
|
||||
}
|
||||
|
||||
_onPasswordFieldChange(ev) {
|
||||
this.setState({
|
||||
password: ev.target.value,
|
||||
});
|
||||
}
|
||||
_onStagePhaseChange = (stage, phase) => {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
|
||||
continueText: _t("Single Sign On"),
|
||||
continueKind: "danger",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
body: _t("Are you sure you want to deactivate your account? This is irreversible."),
|
||||
continueText: _t("Confirm account deactivation"),
|
||||
continueKind: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
_onEraseFieldChange(ev) {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
async _onOk() {
|
||||
this.setState({busy: true});
|
||||
|
||||
try {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
const auth = {
|
||||
type: 'm.login.password',
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title`
|
||||
const DEACTIVATE_AESTHETICS = {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
[PasswordAuthEntry.LOGIN_TYPE]: {
|
||||
[DEFAULT_PHASE]: {
|
||||
body: _t("To continue, please enter your password:"),
|
||||
},
|
||||
password: this.state.password,
|
||||
};
|
||||
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
|
||||
} catch (err) {
|
||||
let errStr = _t('Unknown error');
|
||||
// https://matrix.org/jira/browse/SYN-744
|
||||
if (err.httpStatus === 401 || err.httpStatus === 403) {
|
||||
errStr = _t('Incorrect password');
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errStr: errStr,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const aesthetics = DEACTIVATE_AESTHETICS[stage];
|
||||
let bodyText = null;
|
||||
let continueText = null;
|
||||
let continueKind = null;
|
||||
if (aesthetics) {
|
||||
const phaseAesthetics = aesthetics[phase];
|
||||
if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body;
|
||||
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
|
||||
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
|
||||
}
|
||||
this.setState({bodyText, continueText, continueKind});
|
||||
};
|
||||
|
||||
_onUIAuthFinished = (success, result, extra) => {
|
||||
if (success) return; // great! makeRequest() will be called too.
|
||||
|
||||
if (result === ERROR_USER_CANCELLED) {
|
||||
this._onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
console.error("Error during UI Auth:", {result, extra});
|
||||
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
||||
};
|
||||
|
||||
_onUIAuthComplete = (auth) => {
|
||||
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
|
||||
// Deactivation worked - logout & close this dialog
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(true);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
||||
});
|
||||
};
|
||||
|
||||
_onEraseFieldChange = (ev) => {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
|
||||
// Disable the auth form because we're going to have to reinitialize the auth
|
||||
// information. We do this because we can't modify the parameters in the UIA
|
||||
// session, and the user will have selected something which changes the request.
|
||||
// Therefore, we throw away the last auth session and try a new one.
|
||||
authEnabled: false,
|
||||
});
|
||||
|
||||
// As mentioned above, set up for auth again to get updated UIA session info
|
||||
this._initAuth(/* shouldErase= */ev.target.checked);
|
||||
};
|
||||
|
||||
_onCancel() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_initAuth(shouldErase) {
|
||||
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
|
||||
// If we got here, oops. The server didn't require any auth.
|
||||
// Our application lifecycle will catch the error and do the logout bits.
|
||||
// We'll try to log something in an vain attempt to record what happened (storage
|
||||
// is also obliterated on logout).
|
||||
console.warn("User's account got deactivated without confirmation: Server had no auth");
|
||||
this.setState({errStr: _t("Server did not require any authentication")});
|
||||
}).catch(e => {
|
||||
if (e && e.httpStatus === 401 && e.data) {
|
||||
// Valid UIA response
|
||||
this.setState({authData: e.data, authEnabled: true});
|
||||
} else {
|
||||
this.setState({errStr: _t("Server did not return valid authentication information.")});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
let passwordBoxClass = '';
|
||||
|
||||
let error = null;
|
||||
if (this.state.errStr) {
|
||||
error = <div className="error">
|
||||
{ this.state.errStr }
|
||||
</div>;
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
|
||||
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
|
||||
const okEnabled = this.state.password && !this.state.busy;
|
||||
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||
{ _t("Cancel") }
|
||||
</button>;
|
||||
let auth = <div>{_t("Loading...")}</div>;
|
||||
if (this.state.authData && this.state.authEnabled) {
|
||||
auth = (
|
||||
<div>
|
||||
{this.state.bodyText}
|
||||
<InteractiveAuth
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
authData={this.state.authData}
|
||||
makeRequest={this._onUIAuthComplete}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
onStagePhaseChange={this._onStagePhaseChange}
|
||||
continueText={this.state.continueText}
|
||||
continueKind={this.state.continueKind}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
|
||||
return (
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
titleClass="danger"
|
||||
title={_t("Deactivate Account")}
|
||||
>
|
||||
|
@ -172,29 +226,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
</label>
|
||||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<Field
|
||||
id="mx_DeactivateAccountDialog_password"
|
||||
type="password"
|
||||
label={_t('Password')}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
value={this.state.password}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
{error}
|
||||
{auth}
|
||||
</div>
|
||||
|
||||
{ error }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
className="mx_Dialog_primary danger"
|
||||
onClick={this._onOk}
|
||||
disabled={!okEnabled}
|
||||
>
|
||||
{ okLabel }
|
||||
</button>
|
||||
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -27,7 +27,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
|||
import {ensureDMExists} from "../../../createRoom";
|
||||
import dis from "../../../dispatcher";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import {SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";
|
||||
|
||||
const MODE_LEGACY = 'legacy';
|
||||
|
@ -131,11 +131,10 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
} else {
|
||||
this._verifier = request.verifier;
|
||||
}
|
||||
} else if (verifyingOwnDevice && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
} else if (verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
|
||||
this._request = await client.requestVerification(this.props.userId, [
|
||||
verificationMethods.SAS,
|
||||
SHOW_QR_CODE_METHOD,
|
||||
SCAN_QR_CODE_METHOD,
|
||||
verificationMethods.RECIPROCATE_QR_CODE,
|
||||
]);
|
||||
|
||||
|
@ -280,6 +279,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
onDone={this._onSasMatchesClick}
|
||||
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
|
||||
onStartEmoji={this._onUseSasClick}
|
||||
inDialog={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
|
@ -22,6 +22,16 @@ import { _t } from '../../../languageHandler';
|
|||
import { Room } from "matrix-js-sdk";
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
|
||||
import {
|
||||
PHASE_UNSENT,
|
||||
PHASE_REQUESTED,
|
||||
PHASE_READY,
|
||||
PHASE_DONE,
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
|
@ -257,7 +267,8 @@ class FilteredList extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
|
@ -292,7 +303,7 @@ class FilteredList extends React.PureComponent {
|
|||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<Field id="DevtoolsDialog_FilteredList_filter" label={_t('Filter results')} autoFocus={true} size={64}
|
||||
<Field label={_t('Filter results')} autoFocus={true} size={64}
|
||||
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
|
@ -605,12 +616,100 @@ class ServersInRoomList extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const PHASE_MAP = {
|
||||
[PHASE_UNSENT]: "unsent",
|
||||
[PHASE_REQUESTED]: "requested",
|
||||
[PHASE_READY]: "ready",
|
||||
[PHASE_DONE]: "done",
|
||||
[PHASE_STARTED]: "started",
|
||||
[PHASE_CANCELLED]: "cancelled",
|
||||
};
|
||||
|
||||
function VerificationRequest({txnId, request}) {
|
||||
const [, updateState] = useState();
|
||||
const [timeout, setRequestTimeout] = useState(request.timeout);
|
||||
|
||||
/* Re-render if something changes state */
|
||||
useEventEmitter(request, "change", updateState);
|
||||
|
||||
/* Keep re-rendering if there's a timeout */
|
||||
useEffect(() => {
|
||||
if (request.timeout == 0) return;
|
||||
|
||||
/* Note that request.timeout is a getter, so its value changes */
|
||||
const id = setInterval(() => {
|
||||
setRequestTimeout(request.timeout);
|
||||
}, 500);
|
||||
|
||||
return () => { clearInterval(id); };
|
||||
}, [request]);
|
||||
|
||||
return (<div className="mx_DevTools_VerificationRequest">
|
||||
<dl>
|
||||
<dt>Transaction</dt>
|
||||
<dd>{txnId}</dd>
|
||||
<dt>Phase</dt>
|
||||
<dd>{PHASE_MAP[request.phase] || request.phase}</dd>
|
||||
<dt>Timeout</dt>
|
||||
<dd>{Math.floor(timeout / 1000)}</dd>
|
||||
<dt>Methods</dt>
|
||||
<dd>{request.methods && request.methods.join(", ")}</dd>
|
||||
<dt>requestingUserId</dt>
|
||||
<dd>{request.requestingUserId}</dd>
|
||||
<dt>observeOnly</dt>
|
||||
<dd>{JSON.stringify(request.observeOnly)}</dd>
|
||||
</dl>
|
||||
</div>);
|
||||
}
|
||||
|
||||
class VerificationExplorer extends React.Component {
|
||||
static getLabel() {
|
||||
return _t("Verification Requests");
|
||||
}
|
||||
|
||||
/* Ensure this.context is the cli */
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
onNewRequest = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const cli = this.context;
|
||||
cli.on("crypto.verification.request", this.onNewRequest);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = this.context;
|
||||
cli.off("crypto.verification.request", this.onNewRequest);
|
||||
}
|
||||
|
||||
render() {
|
||||
const cli = this.context;
|
||||
const room = this.props.room;
|
||||
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
|
||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||
|
||||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
SendAccountData,
|
||||
AccountDataExplorer,
|
||||
ServersInRoomList,
|
||||
VerificationExplorer,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
|
|
|
@ -196,7 +196,8 @@ export default class IncomingSasDialog extends React.Component {
|
|||
sas={this._showSasEvent.sas}
|
||||
onCancel={this._onCancelClick}
|
||||
onDone={this._onSasMatchesClick}
|
||||
isSelf={this.props.verifier.userId == MatrixClientPeg.get().getUserId()}
|
||||
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
|
||||
inDialog={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export default createReactClass({
|
|||
button: PropTypes.string,
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -50,10 +51,13 @@ export default createReactClass({
|
|||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_InfoDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={this.props.hasCloseButton}
|
||||
onKeyDown={this.props.onKeyDown}
|
||||
>
|
||||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -23,6 +24,8 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
|
||||
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'InteractiveAuthDialog',
|
||||
|
@ -44,12 +47,60 @@ export default createReactClass({
|
|||
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Optional title and body to show when not showing a particular stage
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
|
||||
// Optional title and body pairs for particular stages and phases within
|
||||
// those stages. Object structure/example is:
|
||||
// {
|
||||
// "org.example.stage_type": {
|
||||
// 1: {
|
||||
// "body": "This is a body for phase 1" of org.example.stage_type,
|
||||
// "title": "Title for phase 1 of org.example.stage_type"
|
||||
// },
|
||||
// 2: {
|
||||
// "body": "This is a body for phase 2 of org.example.stage_type",
|
||||
// "title": "Title for phase 2 of org.example.stage_type"
|
||||
// "continueText": "Confirm identity with Example Auth",
|
||||
// "continueKind": "danger"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Default is defined in _getDefaultDialogAesthetics()
|
||||
aestheticsForStagePhases: PropTypes.object,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authError: null,
|
||||
|
||||
// See _onUpdateStagePhase()
|
||||
uiaStage: null,
|
||||
uiaStagePhase: null,
|
||||
};
|
||||
},
|
||||
|
||||
_getDefaultDialogAesthetics: function() {
|
||||
const ssoAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
body: _t("To continue, use Single Sign On to prove your identity."),
|
||||
continueText: _t("Single Sign On"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("Confirm to continue"),
|
||||
body: _t("Click the button below to confirm your identity."),
|
||||
continueText: _t("Confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: ssoAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: ssoAesthetics,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -57,12 +108,21 @@ export default createReactClass({
|
|||
if (success) {
|
||||
this.props.onFinished(true, result);
|
||||
} else {
|
||||
this.setState({
|
||||
authError: result,
|
||||
});
|
||||
if (result === ERROR_USER_CANCELLED) {
|
||||
this.props.onFinished(false, null);
|
||||
} else {
|
||||
this.setState({
|
||||
authError: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onUpdateStagePhase: function(newStage, newPhase) {
|
||||
// We copy the stage and stage phase params into state for title selection in render()
|
||||
this.setState({uiaStage: newStage, uiaStagePhase: newPhase});
|
||||
},
|
||||
|
||||
_onDismissClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
@ -71,6 +131,24 @@ export default createReactClass({
|
|||
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
// Let's pick a title, body, and other params text that we'll show to the user. The order
|
||||
// is most specific first, so stagePhase > our props > defaults.
|
||||
|
||||
let title = this.state.authError ? 'Error' : (this.props.title || _t('Authentication'));
|
||||
let body = this.state.authError ? null : this.props.body;
|
||||
let continueText = null;
|
||||
let continueKind = null;
|
||||
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics();
|
||||
if (!this.state.authError && dialogAesthetics) {
|
||||
if (dialogAesthetics[this.state.uiaStage]) {
|
||||
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
|
||||
if (aesthetics && aesthetics.title) title = aesthetics.title;
|
||||
if (aesthetics && aesthetics.body) body = aesthetics.body;
|
||||
if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText;
|
||||
if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind;
|
||||
}
|
||||
}
|
||||
|
||||
let content;
|
||||
if (this.state.authError) {
|
||||
content = (
|
||||
|
@ -88,11 +166,16 @@ export default createReactClass({
|
|||
} else {
|
||||
content = (
|
||||
<div id='mx_Dialog_content'>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
{body}
|
||||
<InteractiveAuth
|
||||
ref={this._collectInteractiveAuth}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authData={this.props.authData}
|
||||
makeRequest={this.props.makeRequest}
|
||||
onAuthFinished={this._onAuthFinished}
|
||||
onStagePhaseChange={this._onUpdateStagePhase}
|
||||
continueText={continueText}
|
||||
continueKind={continueKind}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -101,7 +184,7 @@ export default createReactClass({
|
|||
return (
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
|
|
|
@ -34,6 +34,8 @@ import {humanizeTime} from "../../../utils/humanize";
|
|||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
|
@ -124,7 +126,7 @@ class ThreepidMember extends Member {
|
|||
class DMUserTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed
|
||||
onRemove: PropTypes.func, // takes 1 argument, the member being removed
|
||||
};
|
||||
|
||||
_onRemove = (e) => {
|
||||
|
@ -155,18 +157,25 @@ class DMUserTile extends React.PureComponent {
|
|||
width={avatarSize}
|
||||
height={avatarSize} />;
|
||||
|
||||
return (
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
let closeButton;
|
||||
if (this.props.onRemove) {
|
||||
closeButton = (
|
||||
<AccessibleButton
|
||||
className='mx_InviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
{ closeButton }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -218,7 +227,7 @@ class DMRoomTile extends React.PureComponent {
|
|||
}
|
||||
|
||||
// Push any text we missed (end of text)
|
||||
if (i < (str.length - 1)) {
|
||||
if (i < str.length) {
|
||||
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
|
||||
}
|
||||
|
||||
|
@ -332,7 +341,23 @@ export default class InviteDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
|
||||
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
||||
const taggedRooms = RoomListStore.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[TAG_DM];
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
for (const dmRoom of dmTaggedRooms) {
|
||||
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
||||
for (const member of otherMembers) {
|
||||
if (rooms[member.userId]) continue; // already have a room
|
||||
|
||||
console.warn(`Adding DM room for ${member.userId} as ${dmRoom.roomId} from tag, not DM map`);
|
||||
rooms[member.userId] = dmRoom;
|
||||
}
|
||||
}
|
||||
|
||||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
// Filter out user IDs that are already in the room / should be excluded
|
||||
|
@ -512,9 +537,27 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return false;
|
||||
}
|
||||
|
||||
_convertFilter(): Member[] {
|
||||
// Check to see if there's anything to convert first
|
||||
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
||||
|
||||
let newMember: Member;
|
||||
if (this.state.filterText.startsWith('@')) {
|
||||
// Assume mxid
|
||||
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
|
||||
} else {
|
||||
// Assume email
|
||||
newMember = new ThreepidMember(this.state.filterText);
|
||||
}
|
||||
const newTargets = [...(this.state.targets || []), newMember];
|
||||
this.setState({targets: newTargets, filterText: ''});
|
||||
return newTargets;
|
||||
}
|
||||
|
||||
_startDm = async () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||
|
@ -529,24 +572,30 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const createRoomOptions = {};
|
||||
const createRoomOptions = {inlineErrors: true};
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const client = MatrixClientPeg.get();
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
||||
if (!has3PidMembers) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a traditional DM and create the room if required.
|
||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||
let createRoomPromise = Promise.resolve();
|
||||
if (targetIds.length === 1) {
|
||||
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
|
||||
if (targetIds.length === 1 && !isSelf) {
|
||||
createRoomOptions.dmUserId = targetIds[0];
|
||||
createRoomPromise = createRoom(createRoomOptions);
|
||||
} else if (isSelf) {
|
||||
createRoomPromise = createRoom(createRoomOptions);
|
||||
} else {
|
||||
// Create a boring room and try to invite the targets manually.
|
||||
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
|
||||
|
@ -573,7 +622,9 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
_inviteUsers = () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!room) {
|
||||
|
@ -600,11 +651,14 @@ export default class InviteDialog extends React.PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
_cancel = () => {
|
||||
// We do not want the user to close the dialog while an action is in progress
|
||||
if (this.state.busy) return;
|
||||
|
||||
this.props.onFinished();
|
||||
_onKeyDown = (e) => {
|
||||
// when the field is empty and the user hits backspace remove the right-most target
|
||||
if (!e.target.value && !this.state.busy && this.state.targets.length > 0 && e.key === Key.BACKSPACE &&
|
||||
!e.ctrlKey && !e.shiftKey && !e.metaKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
|
@ -630,13 +684,14 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
// While we're here, try and autocomplete a search result for the mxid itself
|
||||
// if there's no matches (and the input looks like a mxid).
|
||||
if (term[0] === '@' && term.indexOf(':') > 1 && r.results.length === 0) {
|
||||
if (term[0] === '@' && term.indexOf(':') > 1) {
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||
if (profile) {
|
||||
// If we have a profile, we have enough information to assume that
|
||||
// the mxid can be invited - add it to the list
|
||||
r.results.push({
|
||||
// the mxid can be invited - add it to the list. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: profile['displayname'],
|
||||
avatar_url: profile['avatar_url'],
|
||||
|
@ -645,6 +700,14 @@ export default class InviteDialog extends React.PureComponent {
|
|||
} catch (e) {
|
||||
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||
console.warn(e);
|
||||
|
||||
// Add a result anyways, just without a profile. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: term,
|
||||
avatar_url: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -769,7 +832,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
];
|
||||
const toAdd = [];
|
||||
const failed = [];
|
||||
const potentialAddresses = text.split(/[\s,]+/);
|
||||
const potentialAddresses = text.split(/[\s,]+/).map(p => p.trim()).filter(p => !!p); // filter empty strings
|
||||
for (const address of potentialAddresses) {
|
||||
const member = possibleMembers.find(m => m.userId === address);
|
||||
if (member) {
|
||||
|
@ -840,7 +903,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
_onManageSettingsClick = (e) => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
this._cancel();
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_renderSection(kind: "recents"|"suggestions") {
|
||||
|
@ -857,24 +920,24 @@ export default class InviteDialog extends React.PureComponent {
|
|||
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||
// the right members in them.
|
||||
let additionalMembers = [];
|
||||
let priorityAdditionalMembers = []; // Shows up before our own suggestions, higher quality
|
||||
let otherAdditionalMembers = []; // Shows up after our own suggestions, lower quality
|
||||
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||
const notAlreadyExists = (u: Member): boolean => {
|
||||
return !sourceMembers.some(m => m.userId === u.userId)
|
||||
&& !additionalMembers.some(m => m.userId === u.userId);
|
||||
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
|
||||
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
|
||||
};
|
||||
|
||||
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueServerResults);
|
||||
|
||||
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
|
||||
otherAdditionalMembers = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
priorityAdditionalMembers = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
}
|
||||
const hasAdditionalMembers = priorityAdditionalMembers.length > 0 || otherAdditionalMembers.length > 0;
|
||||
|
||||
// Hide the section if there's nothing to filter by
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) return null;
|
||||
|
||||
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
||||
if (this.state.filterText) {
|
||||
|
@ -882,7 +945,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
sourceMembers = sourceMembers
|
||||
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
||||
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
|
||||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
|
@ -894,7 +957,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
// Now we mix in the additional members. Again, we presume these have already been filtered. We
|
||||
// also assume they are more relevant than our suggestions and prepend them to the list.
|
||||
sourceMembers = [...additionalMembers, ...sourceMembers];
|
||||
sourceMembers = [...priorityAdditionalMembers, ...sourceMembers, ...otherAdditionalMembers];
|
||||
|
||||
// If we're going to hide one member behind 'show more', just use up the space of the button
|
||||
// with the member's tile instead.
|
||||
|
@ -935,17 +998,18 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
_renderEditor() {
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
||||
));
|
||||
const input = (
|
||||
<textarea
|
||||
key={"input"}
|
||||
rows={1}
|
||||
onKeyDown={this._onKeyDown}
|
||||
onChange={this._updateFilter}
|
||||
value={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
autoFocus={true}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
|
@ -1006,34 +1070,41 @@ export default class InviteDialog extends React.PureComponent {
|
|||
let buttonText;
|
||||
let goButtonFn;
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
if (this.props.kind === KIND_DM) {
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
|
||||
title = _t("Direct Messages");
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username, share your " +
|
||||
"username (%(userId)s) or <a>profile link</a>.",
|
||||
{userId},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||
{},
|
||||
{userId: () => {
|
||||
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
|
||||
}},
|
||||
);
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else { // KIND_INVITE
|
||||
title = _t("Invite to this room");
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
|
||||
"<a>share this room</a>.", {},
|
||||
{a: (sub) => <a href={makeRoomPermalink(this.props.roomId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
},
|
||||
);
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
}
|
||||
|
||||
const hasSelection = this.state.targets.length > 0
|
||||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_InviteDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this._cancel}
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div className='mx_InviteDialog_content'>
|
||||
|
@ -1045,7 +1116,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
kind="primary"
|
||||
onClick={goButtonFn}
|
||||
className='mx_InviteDialog_goButton'
|
||||
disabled={this.state.busy}
|
||||
disabled={this.state.busy || !hasSelection}
|
||||
>
|
||||
{buttonText}
|
||||
</AccessibleButton>
|
||||
|
|
108
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
108
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState, useCallback, useRef} from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default function KeySignatureUploadFailedDialog({
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const [retry, setRetry] = useState(RETRIES);
|
||||
const [cancelled, setCancelled] = useState(false);
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const onCancel = useRef(onFinished);
|
||||
|
||||
const causes = new Map([
|
||||
["_afterCrossSigningLocalKeyChange", _t("a new master key signature")],
|
||||
["checkOwnCrossSigningTrust", _t("a new cross-signing key signature")],
|
||||
["setDeviceVerification", _t("a device cross-signing signature")],
|
||||
]);
|
||||
const defaultCause = _t("a key signature");
|
||||
|
||||
const onRetry = useCallback(async () => {
|
||||
try {
|
||||
setRetrying(true);
|
||||
const cancel = new Promise((resolve, reject) => {
|
||||
onCancel.current = reject;
|
||||
}).finally(() => {
|
||||
setCancelled(true);
|
||||
});
|
||||
await Promise.race([
|
||||
continuation(),
|
||||
cancel,
|
||||
]);
|
||||
setSuccess(true);
|
||||
} catch (e) {
|
||||
setRetry(r => r-1);
|
||||
} finally {
|
||||
onCancel.current = onFinished;
|
||||
setRetrying(false);
|
||||
}
|
||||
}, [continuation, onFinished]);
|
||||
|
||||
let body;
|
||||
if (!success && !cancelled && continuation && retry > 0) {
|
||||
const reason = causes.get(source) || defaultCause;
|
||||
|
||||
body = (<div>
|
||||
<p>{_t("Riot encountered an error during upload of:")}</p>
|
||||
<p>{reason}</p>
|
||||
{retrying && <Spinner />}
|
||||
<pre>{JSON.stringify(failures, null, 2)}</pre>
|
||||
<DialogButtons
|
||||
primaryButton='Retry'
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={onRetry}
|
||||
onCancel={onCancel.current}
|
||||
primaryDisabled={retrying}
|
||||
/>
|
||||
</div>);
|
||||
} else {
|
||||
body = (<div>
|
||||
{success ?
|
||||
<span>{_t("Upload completed")}</span> :
|
||||
cancelled ?
|
||||
<span>{_t("Cancelled signature upload")}</span> :
|
||||
<span>{_t("Unable to upload")}</span>}
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={onFinished}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
title={success ?
|
||||
_t("Signature upload success") :
|
||||
_t("Signature upload failed")}
|
||||
fixedWidth={false}
|
||||
onFinished={() => {}}
|
||||
>
|
||||
{body}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
|
@ -21,7 +21,6 @@ import * as sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class LogoutDialog extends React.Component {
|
||||
defaultProps = {
|
||||
|
@ -36,8 +35,8 @@ export default class LogoutDialog extends React.Component {
|
|||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
||||
|
||||
const lowBandwidth = SettingsStore.getValue("lowBandwidth");
|
||||
const shouldLoadBackupStatus = !lowBandwidth && !MatrixClientPeg.get().getKeyBackupEnabled();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||
|
||||
this.state = {
|
||||
shouldLoadBackupStatus: shouldLoadBackupStatus,
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class ManualDeviceKeyVerificationDialog extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onLegacyFinished = (confirm) => {
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.deviceId, true,
|
||||
);
|
||||
}
|
||||
this.props.onFinished(confirm);
|
||||
}
|
||||
|
||||
render() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
let text;
|
||||
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
|
||||
text = _t("Confirm by comparing the following with the User Settings in your other session:");
|
||||
} else {
|
||||
text = _t("Confirm this user's session by comparing the following with their User Settings:");
|
||||
}
|
||||
|
||||
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
|
||||
const body = (
|
||||
<div>
|
||||
<p>
|
||||
{ text }
|
||||
</p>
|
||||
<div className="mx_DeviceVerifyDialog_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
|
||||
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{ _t("If they don't match, the security of your communication may be compromised.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("Verify session")}
|
||||
description={body}
|
||||
button={_t("Verify session")}
|
||||
onFinished={this._onLegacyFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import VerificationRequestDialog from './VerificationRequestDialog';
|
|||
import BaseDialog from './BaseDialog';
|
||||
import DialogButtons from '../elements/DialogButtons';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
|
||||
export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
|
@ -33,20 +34,38 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning.svg"),
|
||||
title: _t("Your account is not secure"),
|
||||
description: <div>
|
||||
{_t("One of the following may be compromised:")}
|
||||
<ul>
|
||||
<li>{_t("Your password")}</li>
|
||||
<li>{_t("Your homeserver")}</li>
|
||||
<li>{_t("This session, or the other session")}</li>
|
||||
<li>{_t("The internet connection either session is using")}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{_t("We recommend you change your password and recovery key in Settings immediately")}
|
||||
</div>
|
||||
</div>,
|
||||
onFinished: () => this.props.onFinished(false),
|
||||
});
|
||||
}
|
||||
|
||||
onContinueClick = async () => {
|
||||
onContinueClick = () => {
|
||||
const { userId, device } = this.props;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const request = await cli.requestVerification(
|
||||
const requestPromise = cli.requestVerification(
|
||||
userId,
|
||||
[device.deviceId],
|
||||
);
|
||||
|
||||
this.props.onFinished(true);
|
||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||
verificationRequest: request,
|
||||
verificationRequestPromise: requestPromise,
|
||||
member: cli.getUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ export default createReactClass({
|
|||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
|
||||
fixedWidth: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -42,6 +44,7 @@ export default createReactClass({
|
|||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
quitOnly: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -61,11 +64,14 @@ export default createReactClass({
|
|||
primaryButtonClass = "danger";
|
||||
}
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_QuestionDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
headerImage={this.props.headerImage}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
fixedWidth={this.props.fixedWidth}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description }
|
||||
|
@ -73,7 +79,7 @@ export default createReactClass({
|
|||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
hasCancel={this.props.hasCancelButton && !this.props.quitOnly}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
|
|
|
@ -123,7 +123,6 @@ export default class ReportEventDialog extends PureComponent {
|
|||
</p>
|
||||
{adminMessage}
|
||||
<Field
|
||||
id="mx_ReportEventDialog_reason"
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
|
|
|
@ -36,12 +36,12 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
|
@ -72,7 +72,7 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Notifications"),
|
||||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
"mx_RoomSettingsDialog_notificationsIcon",
|
||||
<NotificationSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export default createReactClass({
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: async function() {
|
||||
componentDidMount: async function() {
|
||||
const recommended = await this.props.room.getRecommendedVersion();
|
||||
this._targetVersion = recommended.version;
|
||||
this.setState({busy: false});
|
||||
|
|
|
@ -62,6 +62,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._input_value = createRef();
|
||||
this._uiAuth = createRef();
|
||||
|
|
|
@ -75,8 +75,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
console.info('SetPasswordDialog component will mount');
|
||||
componentDidMount: function() {
|
||||
console.info('SetPasswordDialog component did mount');
|
||||
},
|
||||
|
||||
_onPasswordChanged: function(res) {
|
||||
|
|
65
src/components/views/dialogs/SetupEncryptionDialog.js
Normal file
65
src/components/views/dialogs/SetupEncryptionDialog.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
|
||||
import BaseDialog from './BaseDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { SetupEncryptionStore, PHASE_DONE } from '../../../stores/SetupEncryptionStore';
|
||||
|
||||
function iconFromPhase(phase) {
|
||||
if (phase === PHASE_DONE) {
|
||||
return require("../../../../res/img/e2e/verified.svg");
|
||||
} else {
|
||||
return require("../../../../res/img/e2e/warning.svg");
|
||||
}
|
||||
}
|
||||
|
||||
export default class SetupEncryptionDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.store = SetupEncryptionStore.sharedInstance();
|
||||
this.state = {icon: iconFromPhase(this.store.phase)};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.store.on("update", this._onStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.store.removeListener("update", this._onStoreUpdate);
|
||||
}
|
||||
|
||||
_onStoreUpdate = () => {
|
||||
this.setState({icon: iconFromPhase(this.store.phase)});
|
||||
};
|
||||
|
||||
render() {
|
||||
return <BaseDialog
|
||||
headerImage={this.state.icon}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Verify this session")}
|
||||
>
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 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,15 +15,20 @@ 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 {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {User} from "matrix-js-sdk/src/models/user";
|
||||
import {Group} from "matrix-js-sdk/src/models/group";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import QRCode from "../elements/QRCode";
|
||||
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import {copyPlaintext, selectText} from "../../../utils/strings";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
|
@ -52,7 +58,18 @@ const socials = [
|
|||
},
|
||||
];
|
||||
|
||||
export default class ShareDialog extends React.Component {
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
target: Room | User | Group | RoomMember | MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
linkSpecificEvent: boolean;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
target: PropTypes.oneOfType([
|
||||
|
@ -64,55 +81,45 @@ export default class ShareDialog extends React.Component {
|
|||
]).isRequired,
|
||||
};
|
||||
|
||||
protected closeCopiedTooltip: () => void;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onCopyClick = this.onCopyClick.bind(this);
|
||||
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
|
||||
|
||||
let permalinkCreator: RoomPermalinkCreator = null;
|
||||
if (props.target instanceof Room) {
|
||||
permalinkCreator = new RoomPermalinkCreator(props.target);
|
||||
permalinkCreator.load();
|
||||
}
|
||||
|
||||
this.state = {
|
||||
// MatrixEvent defaults to share linkSpecificEvent
|
||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||
permalinkCreator,
|
||||
};
|
||||
|
||||
this._link = createRef();
|
||||
}
|
||||
|
||||
static _selectText(target) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
static onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
const {target} = e;
|
||||
ShareDialog._selectText(target);
|
||||
selectText(e.target);
|
||||
}
|
||||
|
||||
onCopyClick(e) {
|
||||
async onCopyClick(e) {
|
||||
e.preventDefault();
|
||||
const target = e.target; // copy target before we go async and React throws it away
|
||||
|
||||
ShareDialog._selectText(this._link.current);
|
||||
|
||||
let successful;
|
||||
try {
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
const successful = await copyPlaintext(this.getUrl());
|
||||
const buttonRect = target.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
// Drop a reference to this close handler for componentWillUnmount
|
||||
this.closeCopiedTooltip = e.target.onmouseleave = close;
|
||||
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||
}
|
||||
|
||||
onLinkSpecificEventCheckboxClick() {
|
||||
|
@ -121,24 +128,38 @@ export default class ShareDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.target instanceof Room) {
|
||||
const permalinkCreator = new RoomPermalinkCreator(this.props.target);
|
||||
permalinkCreator.load();
|
||||
this.setState({permalinkCreator});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
|
||||
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
|
||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
getUrl() {
|
||||
let matrixToUrl;
|
||||
|
||||
if (this.props.target instanceof Room) {
|
||||
if (this.state.linkSpecificEvent) {
|
||||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = this.state.permalinkCreator.forRoom();
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = this.props.permalinkCreator.forRoom();
|
||||
}
|
||||
}
|
||||
return matrixToUrl;
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let checkbox;
|
||||
|
||||
if (this.props.target instanceof Room) {
|
||||
|
@ -156,18 +177,10 @@ export default class ShareDialog extends React.Component {
|
|||
</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = this.state.permalinkCreator.forRoom();
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
title = _t('Share Community');
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
|
@ -179,14 +192,9 @@ export default class ShareDialog extends React.Component {
|
|||
{ _t('Link to selected message') }
|
||||
</label>
|
||||
</div>;
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = this.props.permalinkCreator.forRoom();
|
||||
}
|
||||
}
|
||||
|
||||
const matrixToUrl = this.getUrl();
|
||||
const encodedUrl = encodeURIComponent(matrixToUrl);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
@ -197,8 +205,7 @@ export default class ShareDialog extends React.Component {
|
|||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a ref={this._link}
|
||||
href={matrixToUrl}
|
||||
<a href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
>
|
||||
|
@ -214,20 +221,21 @@ export default class ShareDialog extends React.Component {
|
|||
|
||||
<div className="mx_ShareDialog_split">
|
||||
<div className="mx_ShareDialog_qrcode_container">
|
||||
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo={require("../../../../res/img/matrix-m.svg")} />
|
||||
<QRCode data={matrixToUrl} width={256} />
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
{
|
||||
socials.map((social) => <a rel="noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
name={social.name}
|
||||
href={social.url(encodedUrl)}
|
||||
className="mx_ShareDialog_social_icon"
|
||||
{ socials.map((social) => (
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
title={social.name}
|
||||
href={social.url(encodedUrl)}
|
||||
className="mx_ShareDialog_social_icon"
|
||||
>
|
||||
<img src={social.img} alt={social.name} height={64} width={64} />
|
||||
</a>)
|
||||
}
|
||||
</a>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -16,14 +16,14 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {CommandCategories, CommandMap} from "../../../SlashCommands";
|
||||
import {CommandCategories, Commands} from "../../../SlashCommands";
|
||||
import * as sdk from "../../../index";
|
||||
|
||||
export default ({onFinished}) => {
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
|
||||
const categories = {};
|
||||
Object.values(CommandMap).forEach(cmd => {
|
||||
Commands.forEach(cmd => {
|
||||
if (!categories[cmd.category]) {
|
||||
categories[cmd.category] = [];
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ export default ({onFinished}) => {
|
|||
|
||||
categories[category].forEach(cmd => {
|
||||
rows.push(<tr key={cmd.command}>
|
||||
<td><strong>{cmd.command}</strong></td>
|
||||
<td><strong>{cmd.getCommand()}</strong></td>
|
||||
<td>{cmd.args}</td>
|
||||
<td>{cmd.description}</td>
|
||||
</tr>);
|
||||
|
|
|
@ -135,7 +135,7 @@ export default class TermsDialog extends React.PureComponent {
|
|||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td>{termDoc[termsLang].name} <a rel="noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a></td>
|
||||
<td><TermsCheckbox
|
||||
|
|
|
@ -18,6 +18,7 @@ import React, {createRef} from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'TextInputDialog',
|
||||
|
@ -28,9 +29,13 @@ export default createReactClass({
|
|||
PropTypes.string,
|
||||
]),
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
hasCancel: PropTypes.bool,
|
||||
validator: PropTypes.func, // result of withValidation
|
||||
fixedWidth: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -39,34 +44,71 @@ export default createReactClass({
|
|||
value: "",
|
||||
description: "",
|
||||
focus: true,
|
||||
hasCancel: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: this.props.value,
|
||||
valid: false,
|
||||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
this._field = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this._textinput.current.value = this.props.value;
|
||||
// this._field.current.value = this.props.value;
|
||||
this._field.current.focus();
|
||||
}
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this._textinput.current.value);
|
||||
onOk: async function(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.props.validator) {
|
||||
await this._field.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!this._field.current.state.valid) {
|
||||
this._field.current.focus();
|
||||
this._field.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.props.onFinished(true, this.state.value);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
onChange: function(ev) {
|
||||
this.setState({
|
||||
value: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onValidate: async function(fieldState) {
|
||||
const result = await this.props.validator(fieldState);
|
||||
this.setState({
|
||||
valid: result.valid,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_TextInputDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
fixedWidth={this.props.fixedWidth}
|
||||
>
|
||||
<form onSubmit={this.onOk}>
|
||||
<div className="mx_Dialog_content">
|
||||
|
@ -74,19 +116,25 @@ export default createReactClass({
|
|||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id="textinput"
|
||||
ref={this._textinput}
|
||||
<Field
|
||||
className="mx_TextInputDialog_input"
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}
|
||||
size="64" />
|
||||
ref={this._field}
|
||||
type="text"
|
||||
label={this.props.placeholder}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onValidate={this.props.validator ? this.onValidate : undefined}
|
||||
size="64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
<DialogButtons
|
||||
primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
onCancel={this.onCancel}
|
||||
hasCancel={this.props.hasCancel}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -87,7 +87,7 @@ export default createReactClass({
|
|||
onSend: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
|
@ -122,7 +122,6 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.props.devices === null) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
|
@ -168,7 +167,7 @@ export default createReactClass({
|
|||
title={_t('Room contains unknown sessions')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<h4>
|
||||
{ _t('"%(RoomName)s" contains sessions that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
||||
</h4>
|
||||
|
@ -176,7 +175,7 @@ export default createReactClass({
|
|||
{ _t("Unknown sessions") }:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
<DialogButtons primaryButton={sendButtonLabel}
|
||||
onPrimaryButtonClick={sendButtonOnClick}
|
||||
onCancel={this._onDismissClicked} />
|
||||
|
|
|
@ -89,7 +89,7 @@ export default class UserSettingsDialog extends React.Component {
|
|||
tabs.push(new Tab(
|
||||
_td("Security & Privacy"),
|
||||
"mx_UserSettingsDialog_securityIcon",
|
||||
<SecurityUserSettingsTab />,
|
||||
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
|
||||
tabs.push(new Tab(
|
||||
|
|
|
@ -22,24 +22,55 @@ import { _t } from '../../../languageHandler';
|
|||
|
||||
export default class VerificationRequestDialog extends React.Component {
|
||||
static propTypes = {
|
||||
verificationRequest: PropTypes.object.isRequired,
|
||||
verificationRequest: PropTypes.object,
|
||||
verificationRequestPromise: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.onFinished = this.onFinished.bind(this);
|
||||
this.state = {};
|
||||
if (this.props.verificationRequest) {
|
||||
this.state.verificationRequest = this.props.verificationRequest;
|
||||
} else if (this.props.verificationRequestPromise) {
|
||||
this.props.verificationRequestPromise.then(r => {
|
||||
this.setState({verificationRequest: r});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
const request = this.state.verificationRequest;
|
||||
const otherUserId = request && request.otherUserId;
|
||||
const member = this.props.member ||
|
||||
otherUserId && MatrixClientPeg.get().getUser(otherUserId);
|
||||
const title = request && request.isSelfVerification ?
|
||||
_t("Verify other session") : _t("Verification Request");
|
||||
|
||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
|
||||
contentId="mx_Dialog_content"
|
||||
title={_t("Verification Request")}
|
||||
title={title}
|
||||
hasCancel={true}
|
||||
>
|
||||
<EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.props.verificationRequest}
|
||||
verificationRequestPromise={this.props.verificationRequestPromise}
|
||||
onClose={this.props.onFinished}
|
||||
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
|
||||
member={member}
|
||||
/>
|
||||
</BaseDialog>;
|
||||
}
|
||||
|
||||
async onFinished() {
|
||||
this.props.onFinished();
|
||||
let request = this.props.verificationRequest;
|
||||
if (!request && this.props.verificationRequestPromise) {
|
||||
request = await this.props.verificationRequestPromise;
|
||||
}
|
||||
request.cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||
import Modal from '../../../../Modal';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
|
||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||
|
@ -36,6 +37,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
// if false, will close the dialog as soon as the restore completes succesfully
|
||||
// default: true
|
||||
showSummary: PropTypes.bool,
|
||||
// If specified, gather the key from the user but then call the function with the backup
|
||||
// key rather than actually (necessarily) restoring the backup.
|
||||
keyCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -56,6 +60,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
forceRecoveryKey: false,
|
||||
passPhrase: '',
|
||||
restoreType: null,
|
||||
progress: { stage: "prefetch" },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -77,16 +82,29 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
_progressCallback = (data) => {
|
||||
this.setState({
|
||||
progress: data,
|
||||
});
|
||||
}
|
||||
|
||||
_onResetRecoveryClick = () => {
|
||||
this.props.onFinished(false);
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
// If cross-signing is enabled, we reset the SSSS recovery passphrase (and cross-signing keys)
|
||||
this.props.onFinished(false);
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
|
@ -103,9 +121,19 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
restoreType: RESTORE_TYPE_PASSPHRASE,
|
||||
});
|
||||
try {
|
||||
// We do still restore the key backup: we must ensure that the key backup key
|
||||
// is the right one and restoring it is currently the only way we can do this.
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
|
||||
{ progressCallback: this._progressCallback },
|
||||
);
|
||||
if (this.props.keyCallback) {
|
||||
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
|
||||
this.state.passPhrase, this.state.backupInfo,
|
||||
);
|
||||
this.props.keyCallback(key);
|
||||
}
|
||||
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
|
@ -134,7 +162,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
|
||||
{ progressCallback: this._progressCallback },
|
||||
);
|
||||
if (this.props.keyCallback) {
|
||||
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
|
||||
this.props.keyCallback(key);
|
||||
}
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
|
@ -169,6 +202,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
const recoverInfo = await accessSecretStorage(async () => {
|
||||
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||
this.state.backupInfo,
|
||||
{ progressCallback: this._progressCallback },
|
||||
);
|
||||
});
|
||||
this.setState({
|
||||
|
@ -184,6 +218,25 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
async _restoreWithCachedKey(backupInfo) {
|
||||
if (!backupInfo) return false;
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache(
|
||||
undefined, /* targetRoomId */
|
||||
undefined, /* targetSessionId */
|
||||
backupInfo,
|
||||
{ progressCallback: this._progressCallback },
|
||||
);
|
||||
this.setState({
|
||||
recoverInfo,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("restoreWithCachedKey failed:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
@ -197,6 +250,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
backupKeyStored,
|
||||
});
|
||||
|
||||
const gotCache = await this._restoreWithCachedKey(backupInfo);
|
||||
if (gotCache) {
|
||||
console.log("RestoreKeyBackupDialog: found cached backup key");
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If the backup key is stored, we can proceed directly to restore.
|
||||
if (backupKeyStored) {
|
||||
return this._restoreWithSecretStorage();
|
||||
|
@ -229,8 +291,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
let content;
|
||||
let title;
|
||||
if (this.state.loading) {
|
||||
title = _t("Loading...");
|
||||
content = <Spinner />;
|
||||
title = _t("Restoring keys from backup");
|
||||
let details;
|
||||
if (this.state.progress.stage === "fetch") {
|
||||
details = _t("Fetching keys from server...");
|
||||
} else if (this.state.progress.stage === "load_keys") {
|
||||
const { total, successes, failures } = this.state.progress;
|
||||
details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures });
|
||||
} else if (this.state.progress.stage === "prefetch") {
|
||||
details = _t("Fetching keys from server...");
|
||||
}
|
||||
content = <div>
|
||||
<div>{details}</div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
} else if (this.state.loadError) {
|
||||
title = _t("Error");
|
||||
content = _t("Unable to load backup status");
|
||||
|
@ -240,7 +314,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
title = _t("Recovery key mismatch");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this key: " +
|
||||
"Backup could not be decrypted with this recovery key: " +
|
||||
"please verify that you entered the correct recovery key.",
|
||||
)}</p>
|
||||
</div>;
|
||||
|
@ -248,7 +322,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
title = _t("Incorrect recovery passphrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this passphrase: " +
|
||||
"Backup could not be decrypted with this recovery passphrase: " +
|
||||
"please verify that you entered the correct recovery passphrase.",
|
||||
)}</p>
|
||||
</div>;
|
||||
|
@ -262,7 +336,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
content = _t("No backup found!");
|
||||
} else if (this.state.recoverInfo) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
title = _t("Backup restored");
|
||||
title = _t("Keys restored");
|
||||
let failedToDecrypt;
|
||||
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
|
||||
failedToDecrypt = <p>{_t(
|
||||
|
@ -271,7 +345,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
)}</p>;
|
||||
}
|
||||
content = <div>
|
||||
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
|
||||
<p>{_t("Successfully restored %(sessionCount)s keys", {sessionCount: this.state.recoverInfo.imported})}</p>
|
||||
{failedToDecrypt}
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
|
@ -392,7 +466,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div>
|
||||
<div className='mx_RestoreKeyBackupDialog_content'>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -21,6 +21,7 @@ import * as sdk from '../../../../index';
|
|||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
|
||||
/*
|
||||
* Access Secure Secret Storage by requesting the user's passphrase.
|
||||
|
@ -55,8 +56,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
_onResetRecoveryClick = () => {
|
||||
// Re-enter the access flow, but resetting storage this time around.
|
||||
this.props.onFinished(false);
|
||||
throw new Error("Resetting secret storage unimplemented");
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
}
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
|
@ -119,14 +121,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
if (hasPassphrase && !this.state.forceRecoveryKey) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
title = _t("Enter secret storage passphrase");
|
||||
title = _t("Enter recovery passphrase");
|
||||
|
||||
let keyStatus;
|
||||
if (this.state.keyMatches === false) {
|
||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t(
|
||||
"Unable to access secret storage. Please verify that you " +
|
||||
"entered the correct passphrase.",
|
||||
"Unable to access secret storage. " +
|
||||
"Please verify that you entered the correct recovery passphrase.",
|
||||
)}
|
||||
</div>;
|
||||
} else {
|
||||
|
@ -135,13 +137,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: You should only access secret storage " +
|
||||
"from a trusted computer.", {},
|
||||
"<b>Warning</b>: You should only do this on a trusted computer.", {},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Access your secure message history and your cross-signing " +
|
||||
"identity for verifying other sessions by entering your passphrase.",
|
||||
"identity for verifying other sessions by entering your recovery passphrase.",
|
||||
)}</p>
|
||||
|
||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
|
||||
|
@ -164,7 +165,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
/>
|
||||
</form>
|
||||
{_t(
|
||||
"If you've forgotten your passphrase you can "+
|
||||
"If you've forgotten your recovery passphrase you can "+
|
||||
"<button1>use your recovery key</button1> or " +
|
||||
"<button2>set up new recovery options</button2>."
|
||||
, {}, {
|
||||
|
@ -183,7 +184,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
})}
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Enter secret storage recovery key");
|
||||
title = _t("Enter recovery key");
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
|
@ -193,8 +194,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
} else if (this.state.keyMatches === false) {
|
||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t(
|
||||
"Unable to access secret storage. Please verify that you " +
|
||||
"entered the correct recovery key.",
|
||||
"Unable to access secret storage. " +
|
||||
"Please verify that you entered the correct recovery key.",
|
||||
)}
|
||||
</div>;
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
|
@ -209,8 +210,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: You should only access secret storage " +
|
||||
"from a trusted computer.", {},
|
||||
"<b>Warning</b>: You should only do this on a trusted computer.", {},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 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.
|
||||
|
@ -15,241 +16,275 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
||||
import {
|
||||
ContextMenu,
|
||||
useContextMenu,
|
||||
ContextMenuButton,
|
||||
MenuItemRadio,
|
||||
MenuItem,
|
||||
MenuGroup,
|
||||
} from "../../structures/ContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {useSettingValue} from "../../../hooks/useSettings";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import withValidation from "../elements/Validation";
|
||||
|
||||
const DEFAULT_ICON_URL = require("../../../../res/img/network-matrix.svg");
|
||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||
|
||||
export default class NetworkDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const SETTING_NAME = "room_directory_servers";
|
||||
|
||||
this.dropdownRootElement = null;
|
||||
this.ignoreEvent = null;
|
||||
const inPlaceOf = (elementRect) => ({
|
||||
right: window.innerWidth - elementRect.right,
|
||||
top: elementRect.top,
|
||||
chevronOffset: 0,
|
||||
chevronFace: "none",
|
||||
});
|
||||
|
||||
this.onInputClick = this.onInputClick.bind(this);
|
||||
this.onRootClick = this.onRootClick.bind(this);
|
||||
this.onDocumentClick = this.onDocumentClick.bind(this);
|
||||
this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
|
||||
this.onInputKeyUp = this.onInputKeyUp.bind(this);
|
||||
this.collectRoot = this.collectRoot.bind(this);
|
||||
this.collectInputTextBox = this.collectInputTextBox.bind(this);
|
||||
const validServer = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value,
|
||||
invalid: () => _t("Enter a server name"),
|
||||
}, {
|
||||
key: "available",
|
||||
final: true,
|
||||
test: async ({ value }) => {
|
||||
try {
|
||||
const opts = {
|
||||
limit: 1,
|
||||
server: value,
|
||||
};
|
||||
// check if we can successfully load this server's room directory
|
||||
await MatrixClientPeg.get().publicRooms(opts);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
valid: () => _t("Looks good"),
|
||||
invalid: () => _t("Can't find this server or its room list"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.inputTextBox = null;
|
||||
// This dropdown sources homeservers from three places:
|
||||
// + your currently connected homeserver
|
||||
// + homeservers in config.json["roomDirectory"]
|
||||
// + homeservers in SettingsStore["room_directory_servers"]
|
||||
// if a server exists in multiple, only keep the top-most entry.
|
||||
|
||||
const server = MatrixClientPeg.getHomeserverName();
|
||||
this.state = {
|
||||
expanded: false,
|
||||
selectedServer: server,
|
||||
selectedInstanceId: null,
|
||||
includeAllNetworks: false,
|
||||
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
const _userDefinedServers = useSettingValue(SETTING_NAME);
|
||||
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
||||
|
||||
const handlerFactory = (server, instanceId) => {
|
||||
return () => {
|
||||
onOptionChange(server, instanceId);
|
||||
closeMenu();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
// Listen for all clicks on the document so we can close the
|
||||
// menu when the user clicks somewhere else
|
||||
document.addEventListener('click', this.onDocumentClick, false);
|
||||
const setUserDefinedServers = servers => {
|
||||
_setUserDefinedServers(servers);
|
||||
SettingsStore.setValue(SETTING_NAME, null, "account", servers);
|
||||
};
|
||||
// keep local echo up to date with external changes
|
||||
useEffect(() => {
|
||||
_setUserDefinedServers(_userDefinedServers);
|
||||
}, [_userDefinedServers]);
|
||||
|
||||
// fire this now so the defaults can be set up
|
||||
const {selectedServer, selectedInstanceId, includeAllNetworks} = this.state;
|
||||
this.props.onOptionChange(selectedServer, selectedInstanceId, includeAllNetworks);
|
||||
}
|
||||
// we either show the button or the dropdown in its place.
|
||||
let content;
|
||||
if (menuDisplayed) {
|
||||
const config = SdkConfig.get();
|
||||
const roomDirectory = config.roomDirectory || {};
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.onDocumentClick, false);
|
||||
}
|
||||
const hsName = MatrixClientPeg.getHomeserverName();
|
||||
const configServers = new Set(roomDirectory.servers);
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.expanded && this.inputTextBox) {
|
||||
this.inputTextBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onDocumentClick(ev) {
|
||||
// Close the dropdown if the user clicks anywhere that isn't
|
||||
// within our root element
|
||||
if (ev !== this.ignoreEvent) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRootClick(ev) {
|
||||
// This captures any clicks that happen within our elements,
|
||||
// such that we can then ignore them when they're seen by the
|
||||
// click listener on the document handler, ie. not close the
|
||||
// dropdown immediately after opening it.
|
||||
// NB. We can't just stopPropagation() because then the event
|
||||
// doesn't reach the React onClick().
|
||||
this.ignoreEvent = ev;
|
||||
}
|
||||
|
||||
onInputClick(ev) {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
onMenuOptionClick(server, instance, includeAll) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
selectedServer: server,
|
||||
selectedInstanceId: instance ? instance.instance_id : null,
|
||||
includeAllNetworks: includeAll,
|
||||
});
|
||||
this.props.onOptionChange(server, instance ? instance.instance_id : null, includeAll);
|
||||
}
|
||||
|
||||
onInputKeyUp(e) {
|
||||
if (e.key === 'Enter') {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
selectedServer: e.target.value,
|
||||
selectedNetwork: null,
|
||||
includeAllNetworks: false,
|
||||
});
|
||||
this.props.onOptionChange(e.target.value, null);
|
||||
}
|
||||
}
|
||||
|
||||
collectRoot(e) {
|
||||
if (this.dropdownRootElement) {
|
||||
this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
if (e) {
|
||||
e.addEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
this.dropdownRootElement = e;
|
||||
}
|
||||
|
||||
collectInputTextBox(e) {
|
||||
this.inputTextBox = e;
|
||||
}
|
||||
|
||||
_getMenuOptions() {
|
||||
const options = [];
|
||||
const roomDirectory = this.props.config.roomDirectory || {};
|
||||
|
||||
let servers = [];
|
||||
if (roomDirectory.servers) {
|
||||
servers = servers.concat(roomDirectory.servers);
|
||||
}
|
||||
|
||||
if (!servers.includes(MatrixClientPeg.getHomeserverName())) {
|
||||
servers.unshift(MatrixClientPeg.getHomeserverName());
|
||||
}
|
||||
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
||||
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
||||
const servers = [
|
||||
// we always show our connected HS, this takes precedence over it being configured or user-defined
|
||||
hsName,
|
||||
...Array.from(configServers).filter(s => s !== hsName).sort(),
|
||||
...Array.from(removableServers).sort(),
|
||||
];
|
||||
|
||||
// For our own HS, we can use the instance_ids given in the third party protocols
|
||||
// response to get the server to filter the room list by network for us.
|
||||
// We can't get thirdparty protocols for remote server yet though, so for those
|
||||
// we can only show the default room list.
|
||||
for (const server of servers) {
|
||||
options.push(this._makeMenuOption(server, null, true));
|
||||
if (server === MatrixClientPeg.getHomeserverName()) {
|
||||
options.push(this._makeMenuOption(server, null, false));
|
||||
if (this.props.protocols) {
|
||||
for (const proto of Object.keys(this.props.protocols)) {
|
||||
if (!this.props.protocols[proto].instances) continue;
|
||||
const options = servers.map(server => {
|
||||
const serverSelected = server === selectedServerName;
|
||||
const entries = [];
|
||||
|
||||
const sortedInstances = this.props.protocols[proto].instances;
|
||||
sortedInstances.sort(function(x, y) {
|
||||
const a = x.desc;
|
||||
const b = y.desc;
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (a > b) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
for (const instance of sortedInstances) {
|
||||
if (!instance.instance_id) continue;
|
||||
options.push(this._makeMenuOption(server, instance, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
const protocolsList = server === hsName ? Object.values(protocols) : [];
|
||||
if (protocolsList.length > 0) {
|
||||
// add a fake protocol with the ALL_ROOMS symbol
|
||||
protocolsList.push({
|
||||
instances: [{
|
||||
instance_id: ALL_ROOMS,
|
||||
desc: _t("All rooms"),
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
protocolsList.forEach(({instances=[]}) => {
|
||||
[...instances].sort((b, a) => {
|
||||
return a.desc.localeCompare(b.desc);
|
||||
}).forEach(({desc, instance_id: instanceId}) => {
|
||||
entries.push(
|
||||
<MenuItemRadio
|
||||
key={String(instanceId)}
|
||||
active={serverSelected && instanceId === selectedInstanceId}
|
||||
onClick={handlerFactory(server, instanceId)}
|
||||
label={desc}
|
||||
className="mx_NetworkDropdown_server_network"
|
||||
>
|
||||
{ desc }
|
||||
</MenuItemRadio>);
|
||||
});
|
||||
});
|
||||
|
||||
_makeMenuOption(server, instance, includeAll, handleClicks) {
|
||||
if (handleClicks === undefined) handleClicks = true;
|
||||
let subtitle;
|
||||
if (server === hsName) {
|
||||
subtitle = (
|
||||
<div className="mx_NetworkDropdown_server_subtitle">
|
||||
{_t("Your server")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let icon;
|
||||
let name;
|
||||
let key;
|
||||
let removeButton;
|
||||
if (removableServers.has(server)) {
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
||||
title: _t("Are you sure?"),
|
||||
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
||||
serverName: server,
|
||||
}, {
|
||||
b: serverName => <b>{ serverName }</b>,
|
||||
}),
|
||||
button: _t("Remove"),
|
||||
fixedWidth: false,
|
||||
}, "mx_NetworkDropdown_dialog");
|
||||
|
||||
if (!instance && includeAll) {
|
||||
key = server;
|
||||
name = server;
|
||||
} else if (!instance) {
|
||||
key = server + '_all';
|
||||
name = 'Matrix';
|
||||
icon = <img src={require("../../../../res/img/network-matrix.svg")} />;
|
||||
} else {
|
||||
key = server + '_inst_' + instance.instance_id;
|
||||
const imgUrl = instance.icon ?
|
||||
MatrixClientPeg.get().mxcUrlToHttp(instance.icon, 25, 25, 'crop', true) :
|
||||
DEFAULT_ICON_URL;
|
||||
icon = <img src={imgUrl} />;
|
||||
name = instance.desc;
|
||||
}
|
||||
const [ok] = await finished;
|
||||
if (!ok) return;
|
||||
|
||||
const clickHandler = handleClicks ? this.onMenuOptionClick.bind(this, server, instance, includeAll) : null;
|
||||
// delete from setting
|
||||
setUserDefinedServers(servers.filter(s => s !== server));
|
||||
|
||||
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={clickHandler}>
|
||||
{icon}
|
||||
<span className="mx_NetworkDropdown_menu_network">{name}</span>
|
||||
</div>;
|
||||
}
|
||||
// the selected server is being removed, reset to our HS
|
||||
if (serverSelected === server) {
|
||||
onOptionChange(hsName, undefined);
|
||||
}
|
||||
};
|
||||
removeButton = <MenuItem onClick={onClick} label={_t("Remove server")} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
let currentValue;
|
||||
// ARIA: in actual fact the entire menu is one large radio group but for better screen reader support
|
||||
// we use group to notate server wrongly.
|
||||
return (
|
||||
<MenuGroup label={server} className="mx_NetworkDropdown_server" key={server}>
|
||||
<div className="mx_NetworkDropdown_server_title">
|
||||
{ server }
|
||||
{ removeButton }
|
||||
</div>
|
||||
{ subtitle }
|
||||
|
||||
let menu;
|
||||
if (this.state.expanded) {
|
||||
const menuOptions = this._getMenuOptions();
|
||||
menu = <div className="mx_NetworkDropdown_menu">
|
||||
{menuOptions}
|
||||
</div>;
|
||||
currentValue = <input type="text" className="mx_NetworkDropdown_networkoption"
|
||||
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
||||
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
|
||||
/>;
|
||||
} else {
|
||||
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
|
||||
currentValue = this._makeMenuOption(
|
||||
this.state.selectedServer, instance, this.state.includeAllNetworks, false,
|
||||
<MenuItemRadio
|
||||
active={serverSelected && !selectedInstanceId}
|
||||
onClick={handlerFactory(server, undefined)}
|
||||
label={_t("Matrix")}
|
||||
className="mx_NetworkDropdown_server_network"
|
||||
>
|
||||
{_t("Matrix")}
|
||||
</MenuItemRadio>
|
||||
{ entries }
|
||||
</MenuGroup>
|
||||
);
|
||||
});
|
||||
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
||||
title: _t("Add a new server"),
|
||||
description: _t("Enter the name of a new server you want to explore."),
|
||||
button: _t("Add"),
|
||||
hasCancel: false,
|
||||
placeholder: _t("Server name"),
|
||||
validator: validServer,
|
||||
fixedWidth: false,
|
||||
}, "mx_NetworkDropdown_dialog");
|
||||
|
||||
const [ok, newServer] = await finished;
|
||||
if (!ok) return;
|
||||
|
||||
if (!userDefinedServers.includes(newServer)) {
|
||||
setUserDefinedServers([...userDefinedServers, newServer]);
|
||||
}
|
||||
|
||||
onOptionChange(newServer); // change filter to the new server
|
||||
};
|
||||
|
||||
const buttonRect = handle.current.getBoundingClientRect();
|
||||
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
|
||||
<div className="mx_NetworkDropdown_menu">
|
||||
{options}
|
||||
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
||||
{_t("Add a new server...")}
|
||||
</MenuItem>
|
||||
</div>
|
||||
</ContextMenu>;
|
||||
} else {
|
||||
let currentValue;
|
||||
if (selectedInstanceId === ALL_ROOMS) {
|
||||
currentValue = _t("All rooms");
|
||||
} else if (selectedInstanceId) {
|
||||
const instance = instanceForInstanceId(protocols, selectedInstanceId);
|
||||
currentValue = _t("%(networkName)s rooms", {
|
||||
networkName: instance.desc,
|
||||
});
|
||||
} else {
|
||||
currentValue = _t("Matrix rooms");
|
||||
}
|
||||
|
||||
return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
|
||||
<div className="mx_NetworkDropdown_input mx_no_textinput" onClick={this.onInputClick}>
|
||||
content = <ContextMenuButton
|
||||
className="mx_NetworkDropdown_handle"
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
>
|
||||
<span>
|
||||
{currentValue}
|
||||
<span className="mx_NetworkDropdown_arrow" />
|
||||
{menu}
|
||||
</div>
|
||||
</div>;
|
||||
</span> <span className="mx_NetworkDropdown_handle_server">
|
||||
({selectedServerName})
|
||||
</span>
|
||||
</ContextMenuButton>;
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="mx_NetworkDropdown" ref={handle}>
|
||||
{content}
|
||||
</div>;
|
||||
};
|
||||
|
||||
NetworkDropdown.propTypes = {
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
protocols: PropTypes.object,
|
||||
// The room directory config. May have a 'servers' key that is a list of server names to include in the dropdown
|
||||
config: PropTypes.object,
|
||||
};
|
||||
|
||||
NetworkDropdown.defaultProps = {
|
||||
protocols: {},
|
||||
config: {},
|
||||
};
|
||||
export default NetworkDropdown;
|
||||
|
|
|
@ -46,7 +46,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(props) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(props) {
|
||||
// Make sure the selected item isn't outside the list bounds
|
||||
const selected = this.state.selected;
|
||||
const maxSelected = this._maxSelected(props.addressList);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 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.
|
||||
|
@ -37,16 +38,35 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
|||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement from "./PersistedElement";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
||||
/**
|
||||
* Does template substitution on a URL (or any string). Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
function uriFromTemplate(uriTemplate, variables) {
|
||||
let out = uriTemplate;
|
||||
for (const [key, val] of Object.entries(variables)) {
|
||||
out = out.replace(
|
||||
'$' + key, encodeURIComponent(val),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = 'widget_' + this.props.id;
|
||||
this._persistKey = 'widget_' + this.props.app.id;
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
|
@ -78,7 +98,7 @@ export default class AppTile extends React.Component {
|
|||
// This is a function to make the impact of calling SettingsStore slightly less
|
||||
const hasPermissionToLoad = () => {
|
||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
|
||||
return !!currentlyAllowedWidgets[newProps.eventId];
|
||||
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
||||
};
|
||||
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
|
@ -86,7 +106,7 @@ export default class AppTile extends React.Component {
|
|||
initialising: true, // True while we are mangling the widget URL
|
||||
// True while the iframe content is loading
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetUrl: this._addWurlParams(newProps.app.url),
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
|
||||
|
@ -103,7 +123,7 @@ export default class AppTile extends React.Component {
|
|||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
|
||||
return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,70 +137,68 @@ export default class AppTile extends React.Component {
|
|||
* If url can not be parsed, it is returned unmodified.
|
||||
*/
|
||||
_addWurlParams(urlString) {
|
||||
const u = url.parse(urlString);
|
||||
if (!u) {
|
||||
console.error("_addWurlParams", "Invalid URL", urlString);
|
||||
return url;
|
||||
try {
|
||||
const parsed = new URL(urlString);
|
||||
|
||||
// TODO: Replace these with proper widget params
|
||||
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
|
||||
parsed.searchParams.set('widgetId', this.props.app.id);
|
||||
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
|
||||
|
||||
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
|
||||
// in HTTP, but URL parsers encode them anyways.
|
||||
return parsed.toString().replace(/%24/g, '$');
|
||||
} catch (e) {
|
||||
console.error("Failed to add widget URL params:", e);
|
||||
return urlString;
|
||||
}
|
||||
|
||||
const params = qs.parse(u.query);
|
||||
// Append widget ID to query parameters
|
||||
params.widgetId = this.props.id;
|
||||
// Append current / parent URL, minus the hash because that will change when
|
||||
// we view a different room (ie. may change for persistent widgets)
|
||||
params.parentUrl = window.location.href.split('#', 2)[0];
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
|
||||
return u.format();
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.url);
|
||||
const u = url.parse(this.props.app.url);
|
||||
const childContentProtocol = u.protocol;
|
||||
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
|
||||
console.warn("Refusing to load mixed-content app:",
|
||||
parentContentProtocol, childContentProtocol, window.location, this.props.url);
|
||||
parentContentProtocol, childContentProtocol, window.location, this.props.app.url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
// Only fetch IM token on mount if we're showing and have permission to load
|
||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Widget action listeners
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Widget action listeners
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
|
||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Generify the name of this function. It's not just scalar tokens.
|
||||
/**
|
||||
* Adds a scalar token to the widget URL, if required
|
||||
* Component initialisation is only complete when this function has resolved
|
||||
*/
|
||||
setScalarToken() {
|
||||
if (!WidgetUtils.isScalarUrl(this.props.url)) {
|
||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
|
||||
console.warn('Widget does not match integration manager, refusing to set auth token', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.url),
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
|
@ -191,7 +209,7 @@ export default class AppTile extends React.Component {
|
|||
console.warn("No integration manager - not setting scalar token", url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.url),
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
|
@ -201,10 +219,10 @@ export default class AppTile extends React.Component {
|
|||
|
||||
const defaultManager = managers.getPrimaryManager();
|
||||
if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
|
||||
console.warn('Non-scalar manager, not setting scalar token!', url);
|
||||
console.warn('Unknown integration manager, refusing to set auth token', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.url),
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
|
@ -217,7 +235,7 @@ export default class AppTile extends React.Component {
|
|||
this._scalarClient.getScalarToken().then((token) => {
|
||||
// Append scalar_token as a query param if not already present
|
||||
this._scalarClient.scalarToken = token;
|
||||
const u = url.parse(this._addWurlParams(this.props.url));
|
||||
const u = url.parse(this._addWurlParams(this.props.app.url));
|
||||
const params = qs.parse(u.query);
|
||||
if (!params.scalar_token) {
|
||||
params.scalar_token = encodeURIComponent(token);
|
||||
|
@ -245,14 +263,17 @@ export default class AppTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.url !== this.props.url) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
if (nextProps.app.url !== this.props.app.url) {
|
||||
this._getNewState(nextProps);
|
||||
// Fetch IM token for new URL if we're showing and have permission to load
|
||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
}
|
||||
} else if (nextProps.show && !this.props.show) {
|
||||
}
|
||||
|
||||
if (nextProps.show && !this.props.show) {
|
||||
// We assume that persisted widgets are loaded and don't need a spinner.
|
||||
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
|
||||
this.setState({
|
||||
|
@ -263,7 +284,9 @@ export default class AppTile extends React.Component {
|
|||
if (this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
}
|
||||
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
|
||||
}
|
||||
|
||||
if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
|
||||
this.setState({
|
||||
widgetPageTitle: nextProps.widgetPageTitle,
|
||||
});
|
||||
|
@ -280,7 +303,7 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
_onEditClick() {
|
||||
console.log("Edit widget ID ", this.props.id);
|
||||
console.log("Edit widget ID ", this.props.app.id);
|
||||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
|
@ -289,21 +312,21 @@ export default class AppTile extends React.Component {
|
|||
IntegrationManagers.sharedInstance().openAll(
|
||||
this.props.room,
|
||||
'type_' + this.props.type,
|
||||
this.props.id,
|
||||
this.props.app.id,
|
||||
);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
this.props.room,
|
||||
'type_' + this.props.type,
|
||||
this.props.id,
|
||||
this.props.app.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onSnapshotClick() {
|
||||
console.warn("Requesting widget snapshot");
|
||||
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
|
||||
console.log("Requesting widget snapshot");
|
||||
ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
|
@ -315,6 +338,28 @@ export default class AppTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends all widget interaction, such as cancelling calls and disabling webcams.
|
||||
* @private
|
||||
*/
|
||||
_endWidgetActions() {
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||
if (this._appFrame.current) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Riot instance is located.
|
||||
this._appFrame.current.src = 'about:blank';
|
||||
}
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
* otherwise revoke access for the widget to load in the user's browser
|
||||
*/
|
||||
|
@ -336,22 +381,11 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
this.setState({deleting: true});
|
||||
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||
if (this._appFrame.current) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Riot instance is located.
|
||||
this._appFrame.current.src = 'about:blank';
|
||||
}
|
||||
this._endWidgetActions();
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
this.props.id,
|
||||
this.props.app.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -369,7 +403,7 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
_onRevokeClicked() {
|
||||
console.info("Revoke widget permissions - %s", this.props.id);
|
||||
console.info("Revoke widget permissions - %s", this.props.app.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
|
||||
|
@ -380,10 +414,10 @@ export default class AppTile extends React.Component {
|
|||
// Destroy the old widget messaging before starting it back up again. Some widgets
|
||||
// have startup routines that run when they are loaded, so we just need to reinitialize
|
||||
// the messaging for them.
|
||||
ActiveWidgetStore.delWidgetMessaging(this.props.id);
|
||||
ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
|
||||
this._setupWidgetMessaging();
|
||||
|
||||
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
|
||||
ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
|
@ -391,10 +425,15 @@ export default class AppTile extends React.Component {
|
|||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
const widgetMessaging = new WidgetMessaging(
|
||||
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
||||
this.props.app.id,
|
||||
this.props.app.url,
|
||||
this._getRenderedUrl(),
|
||||
this.props.userWidget,
|
||||
this._appFrame.current.contentWindow,
|
||||
);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
||||
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
|
@ -406,7 +445,7 @@ export default class AppTile extends React.Component {
|
|||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
|
||||
console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
|
||||
requestedWhitelistCapabilies,
|
||||
);
|
||||
}
|
||||
|
@ -414,18 +453,24 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
|
||||
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
|
||||
ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
}
|
||||
|
||||
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
|
||||
// using this custom extension to the widget API.
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
widgetMessaging.flagReadyToContinue();
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
|
||||
console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.widgetId === this.props.id) {
|
||||
if (payload.widgetId === this.props.app.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
|
@ -454,9 +499,9 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_grantWidgetPermission() {
|
||||
const roomId = this.props.room.roomId;
|
||||
console.info("Granting permission for widget to load: " + this.props.eventId);
|
||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
current[this.props.eventId] = true;
|
||||
current[this.props.app.eventId] = true;
|
||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
||||
this.setState({hasPermissionToLoad: true});
|
||||
|
||||
|
@ -470,14 +515,14 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_revokeWidgetPermission() {
|
||||
const roomId = this.props.room.roomId;
|
||||
console.info("Revoking permission for widget to load: " + this.props.eventId);
|
||||
console.info("Revoking permission for widget to load: " + this.props.app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
current[this.props.eventId] = false;
|
||||
current[this.props.app.eventId] = false;
|
||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
||||
this.setState({hasPermissionToLoad: false});
|
||||
|
||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}).catch(err => {
|
||||
|
@ -488,8 +533,8 @@ export default class AppTile extends React.Component {
|
|||
|
||||
formatAppTileName() {
|
||||
let appTileName = "No name";
|
||||
if (this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
if (this.props.app.name && this.props.app.name.trim()) {
|
||||
appTileName = this.props.app.name.trim();
|
||||
}
|
||||
return appTileName;
|
||||
}
|
||||
|
@ -506,6 +551,10 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.userWidget) {
|
||||
this._onMinimiseClick();
|
||||
} else {
|
||||
if (this.props.show) {
|
||||
// if we were being shown, end the widget as we're about to be minimized.
|
||||
this._endWidgetActions();
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: !this.props.show,
|
||||
|
@ -513,17 +562,88 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getSafeUrl() {
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
|
||||
/**
|
||||
* Replace the widget template variables in a url with their values
|
||||
*
|
||||
* @param {string} u The URL with template variables
|
||||
* @param {string} widgetType The widget's type
|
||||
*
|
||||
* @returns {string} url with temlate variables replaced
|
||||
*/
|
||||
_templatedUrl(u, widgetType: string) {
|
||||
const targetData = {};
|
||||
if (WidgetType.JITSI.matches(widgetType)) {
|
||||
targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
|
||||
}
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUser = MatrixClientPeg.get().getUser(myUserId);
|
||||
const vars = Object.assign(targetData, this.props.app.data, {
|
||||
'matrix_user_id': myUserId,
|
||||
'matrix_room_id': this.props.room.roomId,
|
||||
'matrix_display_name': myUser ? myUser.displayName : myUserId,
|
||||
'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
|
||||
|
||||
// TODO: Namespace themes through some standard
|
||||
'theme': SettingsStore.getValue("theme"),
|
||||
});
|
||||
|
||||
if (vars.conferenceId === undefined) {
|
||||
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
|
||||
const parsedUrl = new URL(this.props.app.url);
|
||||
vars.conferenceId = parsedUrl.searchParams.get("confId");
|
||||
}
|
||||
|
||||
return uriFromTemplate(u, vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL used in the iframe
|
||||
* In cases where we supply our own UI for a widget, this is an internal
|
||||
* URL different to the one used if the widget is popped out to a separate
|
||||
* tab / browser
|
||||
*
|
||||
* @returns {string} url
|
||||
*/
|
||||
_getRenderedUrl() {
|
||||
let url;
|
||||
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
console.log("Replacing Jitsi widget URL with local wrapper");
|
||||
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
|
||||
url = this._addWurlParams(url);
|
||||
} else {
|
||||
url = this._getSafeUrl(this.state.widgetUrl);
|
||||
}
|
||||
return this._templatedUrl(url, this.props.app.type);
|
||||
}
|
||||
|
||||
_getPopoutUrl() {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
return this._templatedUrl(
|
||||
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
|
||||
this.props.app.type,
|
||||
);
|
||||
} else {
|
||||
// use app.url, not state.widgetUrl, because we want the one without
|
||||
// the wURL params for the popped-out version.
|
||||
return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
|
||||
}
|
||||
}
|
||||
|
||||
_getSafeUrl(u) {
|
||||
const parsedWidgetUrl = url.parse(u, true);
|
||||
if (ENABLE_REACT_PERF) {
|
||||
parsedWidgetUrl.search = null;
|
||||
parsedWidgetUrl.query.react_perf = true;
|
||||
}
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
|
||||
// Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
|
||||
// We also need the dollar signs in-tact for variable substitution.
|
||||
return safeWidgetUrl.replace(/%24/g, '$');
|
||||
}
|
||||
|
||||
_getTileTitle() {
|
||||
|
@ -550,9 +670,9 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_onPopoutWidgetClick() {
|
||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
|
||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
|
||||
{ target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick() {
|
||||
|
@ -629,7 +749,7 @@ export default class AppTile extends React.Component {
|
|||
<iframe
|
||||
allow={iframeFeatures}
|
||||
ref={this._appFrame}
|
||||
src={this._getSafeUrl()}
|
||||
src={this._getRenderedUrl()}
|
||||
allowFullScreen={true}
|
||||
sandbox={sandboxFlags}
|
||||
onLoad={this._onLoaded} />
|
||||
|
@ -694,7 +814,7 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className={appTileClass} id={this.props.id}>
|
||||
<div className={appTileClass} id={this.props.app.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
|
@ -741,12 +861,8 @@ export default class AppTile extends React.Component {
|
|||
AppTile.displayName = 'AppTile';
|
||||
|
||||
AppTile.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
eventId: PropTypes.string, // required for room widgets
|
||||
url: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
app: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: PropTypes.bool,
|
||||
|
@ -793,7 +909,6 @@ AppTile.propTypes = {
|
|||
};
|
||||
|
||||
AppTile.defaultProps = {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
showMenubar: true,
|
||||
showTitle: true,
|
||||
|
|
|
@ -19,8 +19,22 @@ import TagTile from './TagTile';
|
|||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu = null;
|
||||
if (menuDisplayed && handle.current) {
|
||||
const elementRect = handle.current.getBoundingClientRect();
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
|
||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
return <div>
|
||||
<Draggable
|
||||
key={props.tag}
|
||||
|
@ -28,18 +42,21 @@ export default function DNDTagTile(props) {
|
|||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile {...props} />
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile
|
||||
{...props}
|
||||
contextMenuButtonRef={handle}
|
||||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
)}
|
||||
</Draggable>
|
||||
{contextMenu}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
|
|
@ -116,7 +116,8 @@ export default class Dropdown extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
this._button = createRef();
|
||||
// Listen for all clicks on the document so we can close the
|
||||
// menu when the user clicks somewhere else
|
||||
|
@ -127,7 +128,8 @@ export default class Dropdown extends React.Component {
|
|||
document.removeEventListener('click', this._onDocumentClick, false);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
if (!nextProps.children || nextProps.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -78,8 +78,7 @@ export class EditableItem extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
|
||||
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
|
||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -122,9 +121,10 @@ export default class EditableItemList extends React.Component {
|
|||
return (
|
||||
<form onSubmit={this._onItemAdded} autoComplete="off"
|
||||
noValidate={true} className="mx_EditableItemList_newItem">
|
||||
<Field id={`mx_EditableItemList_new_${this.props.id}`} label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary">
|
||||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
|
|
@ -62,7 +62,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this._editable_div.current) {
|
||||
|
@ -71,7 +72,8 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
// we track value as an JS object field rather than in React state
|
||||
// as React doesn't play nice with contentEditable.
|
||||
this.value = '';
|
||||
|
|
|
@ -42,7 +42,7 @@ export default class EditableTextContainer extends React.Component {
|
|||
this._onValueChanged = this._onValueChanged.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (this.props.getInitialValue === undefined) {
|
||||
// use whatever was given in the initialValue property.
|
||||
return;
|
||||
|
|
|
@ -20,6 +20,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixEvent, RoomMember} from "matrix-js-sdk";
|
||||
import {useStateToggle} from "../../../hooks/useStateToggle";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
|
||||
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
|
||||
|
@ -42,24 +43,15 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
|
|||
);
|
||||
}
|
||||
|
||||
let body;
|
||||
if (expanded) {
|
||||
return (
|
||||
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
|
||||
{ _t('collapse') }
|
||||
</div>
|
||||
<div className="mx_EventListSummary_line"> </div>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
|
||||
return (
|
||||
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
|
||||
{ _t('expand') }
|
||||
</div>
|
||||
body = <React.Fragment>
|
||||
<div className="mx_EventListSummary_line"> </div>
|
||||
{ children }
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
|
||||
body = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
<span className="mx_EventListSummary_avatars" onClick={toggleExpanded}>
|
||||
|
@ -70,7 +62,16 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</AccessibleButton>
|
||||
{ body }
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -89,7 +90,7 @@ EventListSummary.propTypes = {
|
|||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers: PropTypes.arrayOf(PropTypes.instanceOf(RoomMember)),
|
||||
// The text to show as the summary of this event list
|
||||
summaryText: PropTypes.string.isRequired,
|
||||
summaryText: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EventListSummary;
|
||||
|
|
|
@ -23,15 +23,23 @@ import { debounce } from 'lodash';
|
|||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
||||
const VALIDATION_THROTTLE_MS = 200;
|
||||
|
||||
const BASE_ID = "mx_Field";
|
||||
let count = 1;
|
||||
function getId() {
|
||||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The field's ID, which binds the input and label together.
|
||||
id: PropTypes.string.isRequired,
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id: PropTypes.string,
|
||||
// The element to create. Defaults to "input".
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: PropTypes.oneOf(["input", "select", "textarea"]),
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type: PropTypes.string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list: PropTypes.string,
|
||||
// The field's label string.
|
||||
label: PropTypes.string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
|
@ -61,13 +69,15 @@ export default class Field extends React.PureComponent {
|
|||
// All other props pass through to the <input>.
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
valid: undefined,
|
||||
feedback: undefined,
|
||||
focused: false,
|
||||
};
|
||||
|
||||
this.id = this.props.id || getId();
|
||||
}
|
||||
|
||||
onFocus = (ev) => {
|
||||
|
@ -157,7 +167,7 @@ export default class Field extends React.PureComponent {
|
|||
render() {
|
||||
const {
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
|
||||
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
|
@ -165,10 +175,12 @@ export default class Field extends React.PureComponent {
|
|||
inputProps.type = inputProps.type || "text";
|
||||
inputProps.ref = input => this.input = input;
|
||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
||||
inputProps.id = this.id; // this overwrites the id from props
|
||||
|
||||
inputProps.onFocus = this.onFocus;
|
||||
inputProps.onChange = this.onChange;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
inputProps.list = list;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
|
||||
|
@ -208,7 +220,7 @@ export default class Field extends React.PureComponent {
|
|||
return <div className={fieldClasses}>
|
||||
{prefixContainer}
|
||||
{fieldInput}
|
||||
<label htmlFor={this.props.id}>{this.props.label}</label>
|
||||
<label htmlFor={this.id}>{this.props.label}</label>
|
||||
{postfixContainer}
|
||||
{fieldTooltip}
|
||||
</div>;
|
||||
|
|
|
@ -81,7 +81,8 @@ export default class Flair extends React.Component {
|
|||
this._unmounted = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
this._generateAvatars(newProps.groups);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
function GeminiScrollbarWrapper(props) {
|
||||
const {wrappedRef, ...wrappedProps} = props;
|
||||
|
||||
// Enable forceGemini so that gemini is always enabled. This is
|
||||
// to avoid future issues where a feature is implemented without
|
||||
// doing QA on every OS/browser combination.
|
||||
//
|
||||
// By default GeminiScrollbar allows native scrollbars to be used
|
||||
// on macOS. Use forceGemini to enable Gemini's non-native
|
||||
// scrollbars on all OSs.
|
||||
return <GeminiScrollbar ref={wrappedRef} forceGemini={true} {...wrappedProps}>
|
||||
{ props.children }
|
||||
</GeminiScrollbar>;
|
||||
}
|
||||
export default GeminiScrollbarWrapper;
|
||||
|
|
@ -73,6 +73,7 @@ export default class ImageView extends React.Component {
|
|||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
this.props.onFinished();
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
|
||||
).catch(function(e) {
|
||||
|
@ -91,7 +92,7 @@ export default class ImageView extends React.Component {
|
|||
getName() {
|
||||
let name = this.props.name;
|
||||
if (name && this.props.link) {
|
||||
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
|
||||
name = <a href={ this.props.link } target="_blank" rel="noreferrer noopener">{ name }</a>;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@ export default class LabelledToggleSwitch extends React.Component {
|
|||
// True to put the toggle in front of the label
|
||||
// Default false.
|
||||
toggleInFront: PropTypes.bool,
|
||||
|
||||
// Additional class names to append to the switch. Optional.
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -50,8 +53,9 @@ export default class LabelledToggleSwitch extends React.Component {
|
|||
secondPart = temp;
|
||||
}
|
||||
|
||||
const classes = `mx_SettingsFlag ${this.props.className || ""}`;
|
||||
return (
|
||||
<div className="mx_SettingsFlag">
|
||||
<div className={classes}>
|
||||
{firstPart}
|
||||
{secondPart}
|
||||
</div>
|
||||
|
|
|
@ -24,8 +24,8 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { _t } from "../../../languageHandler";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
if (language.value.toUpperCase() == query.toUpperCase()) return true;
|
||||
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||
if (language.value.toUpperCase() === query.toUpperCase()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ export default class LanguageDropdown extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||
langs.sort(function(a, b) {
|
||||
if (a.label < b.label) return -1;
|
||||
|
@ -114,6 +114,7 @@ export default class LanguageDropdown extends React.Component {
|
|||
searchEnabled={true}
|
||||
value={value}
|
||||
label={_t("Language Dropdown")}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
|
|
|
@ -113,10 +113,12 @@ export default class PersistedElement extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
this.updateChild();
|
||||
this.renderApp();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateChild();
|
||||
this.renderApp();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -141,6 +143,14 @@ export default class PersistedElement extends React.Component {
|
|||
this.updateChildVisibility(this.child, true);
|
||||
}
|
||||
|
||||
renderApp() {
|
||||
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
}
|
||||
|
||||
updateChildVisibility(child, visible) {
|
||||
if (!child) return;
|
||||
child.style.display = visible ? 'block' : 'none';
|
||||
|
@ -160,12 +170,6 @@ export default class PersistedElement extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 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.
|
||||
|
@ -33,7 +33,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
||||
},
|
||||
|
@ -75,11 +75,7 @@ export default createReactClass({
|
|||
const AppTile = sdk.getComponent('elements.AppTile');
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
eventId={app.eventId}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
app={app}
|
||||
fullWidth={true}
|
||||
room={persistentWidgetInRoom}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
|
|
|
@ -23,7 +23,6 @@ import classNames from 'classnames';
|
|||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
@ -83,7 +82,8 @@ const Pill = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
let resourceId;
|
||||
let prefix;
|
||||
|
||||
|
@ -128,7 +128,8 @@ const Pill = createReactClass({
|
|||
case Pill.TYPE_ROOM_MENTION: {
|
||||
const localRoom = resourceId[0] === '#' ?
|
||||
MatrixClientPeg.get().getRooms().find((r) => {
|
||||
return r.getAliases().includes(resourceId);
|
||||
return r.getCanonicalAlias() === resourceId ||
|
||||
r.getAltAliases().includes(resourceId);
|
||||
}) : MatrixClientPeg.get().getRoom(resourceId);
|
||||
room = localRoom;
|
||||
if (!localRoom) {
|
||||
|
@ -155,10 +156,12 @@ const Pill = createReactClass({
|
|||
this.setState({resourceId, pillType, member, group, room});
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
this.componentWillReceiveProps(this.props);
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
this.UNSAFE_componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves.
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -236,12 +239,12 @@ const Pill = createReactClass({
|
|||
case Pill.TYPE_ROOM_MENTION: {
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||
linkText = resource;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
|
|
|
@ -62,11 +62,13 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to class constructor
|
||||
this._initStateFromProps(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
this._initStateFromProps(newProps);
|
||||
},
|
||||
|
||||
|
@ -132,7 +134,7 @@ export default createReactClass({
|
|||
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
||||
if (this.state.custom) {
|
||||
picker = (
|
||||
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
|
||||
<Field type="number"
|
||||
label={label} max={this.props.maxValue}
|
||||
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
|
||||
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
||||
|
@ -151,7 +153,7 @@ export default createReactClass({
|
|||
});
|
||||
|
||||
picker = (
|
||||
<Field id={`powerSelector_notCustom_${this.props.powerLevelKey}`} element="select"
|
||||
<Field element="select"
|
||||
label={label} onChange={this.onSelectChange}
|
||||
value={String(this.state.selectValue)} disabled={this.props.disabled}>
|
||||
{options}
|
||||
|
|
51
src/components/views/elements/QRCode.tsx
Normal file
51
src/components/views/elements/QRCode.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {toDataURL, QRCodeSegment, QRCodeToDataURLOptions} from "qrcode";
|
||||
import classNames from "classnames";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
interface IProps extends QRCodeToDataURLOptions {
|
||||
data: string | QRCodeSegment[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultOptions: QRCodeToDataURLOptions = {
|
||||
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
|
||||
};
|
||||
|
||||
const QRCode: React.FC<IProps> = ({data, className, ...options}) => {
|
||||
const [dataUri, setUri] = React.useState<string>(null);
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
toDataURL(data, {...defaultOptions, ...options}).then(uri => {
|
||||
if (cancelled) return;
|
||||
setUri(uri);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [JSON.stringify(data), options]);
|
||||
|
||||
return <div className={classNames("mx_QRCode", className)}>
|
||||
{ dataUri ? <img src={dataUri} className="mx_VerificationQRCode" alt={_t("QR Code")} /> : <Spinner /> }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default QRCode;
|
|
@ -184,7 +184,7 @@ export default class ReplyThread extends React.Component {
|
|||
ref={ref} permalinkCreator={permalinkCreator} />;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||
this.room.on("Room.redaction", this.onRoomRedaction);
|
||||
|
|
|
@ -23,7 +23,6 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
||||
export default class RoomAliasField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
|
@ -50,7 +49,6 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
className="mx_RoomAliasField"
|
||||
prefix={poundSign}
|
||||
postfix={domain}
|
||||
id={this.props.id}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
|
@ -92,6 +90,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
invalid: () => _t("Please provide a room alias"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
test: async ({value}) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
|
|
42
src/components/views/elements/SSOButton.js
Normal file
42
src/components/views/elements/SSOButton.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => {
|
||||
const onClick = () => {
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} kind="primary" onClick={onClick}>
|
||||
{_t("Sign in with single sign-on")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
SSOButton.propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
|
||||
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
|
||||
fragmentAfterLogin: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SSOButton;
|
|
@ -31,7 +31,6 @@ export default class SyntaxHighlight extends React.Component {
|
|||
}
|
||||
|
||||
// componentDidUpdate used here for reusability
|
||||
// componentWillReceiveProps fires too early to call highlightBlock on.
|
||||
componentDidUpdate() {
|
||||
if (this._el) highlightBlock(this._el);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
|
@ -28,7 +28,6 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
|
|||
import FlairStore from '../../../stores/FlairStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import {ContextMenu, toRightOf} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
|
@ -43,6 +42,9 @@ export default createReactClass({
|
|||
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
|
||||
// For now, only group IDs are handled.
|
||||
tag: PropTypes.string,
|
||||
contextMenuButtonRef: PropTypes.object,
|
||||
openMenu: PropTypes.func,
|
||||
menuDisplayed: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -55,14 +57,10 @@ export default createReactClass({
|
|||
hover: false,
|
||||
// The profile data of the group if this.props.tag is a group ID
|
||||
profile: null,
|
||||
// Whether or not the context menu is open
|
||||
menuDisplayed: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this._contextMenuButton = createRef();
|
||||
|
||||
this.unmounted = false;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||
|
@ -86,7 +84,7 @@ export default createReactClass({
|
|||
this.props.tag,
|
||||
).then((profile) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({profile});
|
||||
this.setState({ profile });
|
||||
}).catch((err) => {
|
||||
console.warn('Could not fetch group profile for ' + this.props.tag, err);
|
||||
});
|
||||
|
@ -113,28 +111,19 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({hover: true});
|
||||
this.setState({ hover: true });
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
this.setState({hover: false});
|
||||
this.setState({ hover: false });
|
||||
},
|
||||
|
||||
openMenu: function(e) {
|
||||
// Prevent the TagTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
menuDisplayed: true,
|
||||
hover: false,
|
||||
});
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
this.setState({
|
||||
menuDisplayed: false,
|
||||
});
|
||||
this.setState({ hover: false });
|
||||
this.props.openMenu();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -154,7 +143,7 @@ export default createReactClass({
|
|||
|
||||
const badge = TagOrderStore.getGroupBadge(this.props.tag);
|
||||
let badgeElement;
|
||||
if (badge && !this.state.hover) {
|
||||
if (badge && !this.state.hover && !this.props.menuDisplayed) {
|
||||
const badgeClasses = classNames({
|
||||
"mx_TagTile_badge": true,
|
||||
"mx_TagTile_badgeHighlight": badge.highlight,
|
||||
|
@ -163,39 +152,34 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
|
||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div ref={this._contextMenuButton} />;
|
||||
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={this.closeMenu}>
|
||||
<TagTileContextMenu tag={this.props.tag} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
const contextButton = this.state.hover || this.props.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this.props.contextMenuButtonRef}>
|
||||
{"\u00B7\u00B7\u00B7"}
|
||||
</div> : <div ref={this.props.contextMenuButtonRef} />;
|
||||
|
||||
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
|
||||
return <React.Fragment>
|
||||
<AccessibleTooltipButton className={className} onClick={this.onClick} onContextMenu={this.openMenu} title={name}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</AccessibleTooltipButton>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
return <AccessibleTooltipButton
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.openMenu}
|
||||
title={name}
|
||||
>
|
||||
<div
|
||||
className="mx_TagTile_avatar"
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{contextButton}
|
||||
{badgeElement}
|
||||
</div>
|
||||
</AccessibleTooltipButton>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -36,11 +36,9 @@ const TintableSvg = createReactClass({
|
|||
idSequence: 0,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.fixups = [];
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.fixups = [];
|
||||
|
||||
this.id = TintableSvg.idSequence++;
|
||||
TintableSvg.mounts[this.id] = this;
|
||||
},
|
||||
|
|
|
@ -35,6 +35,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._user_id_input = createRef();
|
||||
},
|
||||
|
|
|
@ -28,9 +28,11 @@ import classNames from 'classnames';
|
|||
* An array of rules describing how to check to input value. Each rule in an object
|
||||
* and may have the following properties:
|
||||
* - `key`: A unique ID for the rule. Required.
|
||||
* - `skip`: A function used to determine whether the rule should even be evaluated.
|
||||
* - `test`: A function used to determine the rule's current validity. Required.
|
||||
* - `valid`: Function returning text to show when the rule is valid. Only shown if set.
|
||||
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
|
||||
* - `final`: A Boolean if true states that this rule will only be considered if all rules before it returned valid.
|
||||
* @returns {Function}
|
||||
* A validation function that takes in the current input value and returns
|
||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
|
@ -51,9 +53,20 @@ export default function withValidation({ description, rules }) {
|
|||
if (!rule.key || !rule.test) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!valid && rule.final) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
|
||||
if (rule.skip && rule.skip.call(this, data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const ruleValid = await rule.test.call(this, { value, allowEmpty });
|
||||
const ruleValid = await rule.test.call(this, data);
|
||||
valid = valid && ruleValid;
|
||||
if (ruleValid && rule.valid) {
|
||||
// If the rule's result is valid and has text to show for
|
||||
|
|
|
@ -17,111 +17,20 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import * as qs from "qs";
|
||||
import QRCode from "qrcode-react";
|
||||
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {ToDeviceChannel} from "matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel";
|
||||
import QRCode from "../QRCode";
|
||||
|
||||
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||
export default class VerificationQRCode extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Common for all kinds of QR codes
|
||||
keys: PropTypes.array.isRequired, // array of [Key ID, Base64 Key] pairs
|
||||
action: PropTypes.string.isRequired,
|
||||
keyholderUserId: PropTypes.string.isRequired,
|
||||
|
||||
// User verification use case only
|
||||
secret: PropTypes.string,
|
||||
otherUserKey: PropTypes.string, // Base64 key being verified
|
||||
otherUserDeviceKey: PropTypes.string, // Base64 key of the other user's device (or what we think it is; optional)
|
||||
requestEventId: PropTypes.string, // for DM verification only
|
||||
qrCodeData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
action: "verify",
|
||||
};
|
||||
|
||||
static async getPropsForRequest(verificationRequest: VerificationRequest) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const myUserId = cli.getUserId();
|
||||
const otherUserId = verificationRequest.otherUserId;
|
||||
const myDeviceId = cli.getDeviceId();
|
||||
const otherDevice = verificationRequest.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
|
||||
const qrProps = {
|
||||
secret: verificationRequest.encodedSharedSecret,
|
||||
keyholderUserId: myUserId,
|
||||
action: "verify",
|
||||
keys: [], // array of pairs: keyId, base64Key
|
||||
otherUserKey: "", // base64key
|
||||
otherUserDeviceKey: "", // base64key
|
||||
requestEventId: "", // we figure this out in a moment
|
||||
};
|
||||
|
||||
const requestEvent = verificationRequest.requestEvent;
|
||||
qrProps.requestEventId = requestEvent.getId()
|
||||
? requestEvent.getId()
|
||||
: ToDeviceChannel.getTransactionId(requestEvent);
|
||||
|
||||
// Populate the keys we need depending on which direction and users are involved in the verification.
|
||||
if (myUserId === otherUserId) {
|
||||
if (!otherDeviceId) {
|
||||
// Existing scanning New session's QR code
|
||||
qrProps.otherUserDeviceKey = null;
|
||||
} else {
|
||||
// New scanning Existing session's QR code
|
||||
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||
const device = myDevices.find(d => d.deviceId === otherDeviceId);
|
||||
if (device) qrProps.otherUserDeviceKey = device.getFingerprint();
|
||||
}
|
||||
|
||||
// Either direction shares these next few props
|
||||
|
||||
const xsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
qrProps.otherUserKey = xsignInfo.getId("master");
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[xsignInfo.getId("master"), xsignInfo.getId("master")],
|
||||
];
|
||||
} else {
|
||||
// Doesn't matter which direction the verification is, we always show the same QR code
|
||||
// for not-ourself verification.
|
||||
const myXsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
const otherXsignInfo = cli.getStoredCrossSigningForUser(otherUserId);
|
||||
const otherDevices = (await cli.getStoredDevicesForUser(otherUserId)) || [];
|
||||
const otherDevice = otherDevices.find(d => d.deviceId === otherDeviceId);
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[myXsignInfo.getId("master"), myXsignInfo.getId("master")],
|
||||
];
|
||||
qrProps.otherUserKey = otherXsignInfo.getId("master");
|
||||
if (otherDevice) qrProps.otherUserDeviceKey = otherDevice.getFingerprint();
|
||||
}
|
||||
|
||||
return qrProps;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const query = {
|
||||
request: this.props.requestEventId,
|
||||
action: this.props.action,
|
||||
other_user_key: this.props.otherUserKey,
|
||||
secret: this.props.secret,
|
||||
};
|
||||
for (const key of this.props.keys) {
|
||||
query[`key_${key[0]}`] = key[1];
|
||||
}
|
||||
|
||||
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
|
||||
|
||||
return <QRCode value={uri} size={512} logoWidth={64} logo={require("../../../../../res/img/matrix-m.svg")} />;
|
||||
return (
|
||||
<QRCode
|
||||
data={[{data: this.props.qrCodeData.buffer, mode: 'byte'}]}
|
||||
className="mx_VerificationQRCode"
|
||||
width={196} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { _t } from '../../../languageHandler';
|
|||
|
||||
import * as recent from '../../../emojipicker/recent';
|
||||
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 22;
|
||||
export const EMOJI_HEIGHT = 37;
|
||||
|
@ -214,7 +215,7 @@ class EmojiPicker extends React.Component {
|
|||
<div className="mx_EmojiPicker">
|
||||
<Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory} />
|
||||
<Search query={this.state.filter} onChange={this.onChangeFilter} />
|
||||
<div className="mx_EmojiPicker_body" ref={this.bodyRef} onScroll={this.onScroll}>
|
||||
<AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={e => this.bodyRef.current = e} onScroll={this.onScroll}>
|
||||
{this.categories.map(category => {
|
||||
const emojis = this.memoizedDataByCategory[category.id];
|
||||
const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
|
||||
|
@ -226,7 +227,7 @@ class EmojiPicker extends React.Component {
|
|||
heightBefore += height;
|
||||
return categoryElement;
|
||||
})}
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
{this.state.previewEmoji || !this.props.showQuickReactions
|
||||
? <Preview emoji={this.state.previewEmoji} />
|
||||
: <QuickReactions onClick={this.onClickEmoji} selectedEmojis={this.props.selectedEmojis} /> }
|
||||
|
|
|
@ -27,6 +27,7 @@ import { GroupMemberType } from '../../../groups';
|
|||
import GroupStore from '../../../stores/GroupStore';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupMemberInfo',
|
||||
|
@ -48,12 +49,13 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
|
@ -182,10 +184,9 @@ export default createReactClass({
|
|||
this.props.groupMember.displayname || this.props.groupMember.userId
|
||||
);
|
||||
|
||||
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
||||
return (
|
||||
<div className="mx_MemberInfo" role="tabpanel">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AutoHideScrollbar>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
|
@ -199,7 +200,7 @@ export default createReactClass({
|
|||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -26,6 +26,7 @@ import { showGroupInviteDialog } from '../../../GroupAddressPicker';
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import TintableSvg from '../elements/TintableSvg';
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
|
||||
|
@ -46,7 +47,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
@ -172,7 +173,6 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.state.fetching || this.state.fetchingInvitedMembers) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (<div className="mx_MemberList">
|
||||
|
@ -225,10 +225,10 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_MemberList" role="tabpanel">
|
||||
{ inviteButton }
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AutoHideScrollbar>
|
||||
{ joined }
|
||||
{ invited }
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
{ inputBox }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -60,6 +60,7 @@ export default createReactClass({
|
|||
|
||||
const av = (
|
||||
<BaseAvatar
|
||||
aria-hidden="true"
|
||||
name={this.props.member.displayname || this.props.member.userId}
|
||||
idName={this.props.member.userId}
|
||||
width={36} height={36}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupRoomInfo',
|
||||
|
@ -46,11 +47,12 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
|
@ -153,7 +155,6 @@ export default createReactClass({
|
|||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <div className="mx_MemberInfo">
|
||||
|
@ -216,7 +217,7 @@ export default createReactClass({
|
|||
const groupRoomName = this.state.groupRoom.displayname;
|
||||
return (
|
||||
<div className="mx_MemberInfo" role="tabpanel">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AutoHideScrollbar>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
|
@ -231,7 +232,7 @@ export default createReactClass({
|
|||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -22,6 +22,7 @@ import PropTypes from 'prop-types';
|
|||
import { showGroupAddRoomDialog } from '../../../GroupAddressPicker';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import TintableSvg from '../elements/TintableSvg';
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||
|
||||
|
@ -38,7 +39,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
@ -150,17 +151,16 @@ export default createReactClass({
|
|||
placeholder={_t('Filter community rooms')} autoComplete="off" />
|
||||
);
|
||||
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return (
|
||||
<div className="mx_GroupRoomList" role="tabpanel">
|
||||
{ inviteButton }
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||
<AutoHideScrollbar className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{ this.makeGroupRoomTiles(this.state.searchQuery) }
|
||||
</TruncatedList>
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
{ inputBox }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -55,7 +55,7 @@ const GroupTile = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
|
||||
this.setState({profile});
|
||||
}).catch((err) => {
|
||||
|
|
|
@ -34,7 +34,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.context.getJoinedGroups().then((result) => {
|
||||
this.setState({groups: result.groups || [], error: null});
|
||||
}, (err) => {
|
||||
|
|
|
@ -20,12 +20,13 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
|||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
import {formatTime} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import classNames from 'classnames';
|
||||
import RedactedBody from "./RedactedBody";
|
||||
|
||||
function getReplacedContent(event) {
|
||||
const originalContent = event.getOriginalContent();
|
||||
|
@ -53,6 +54,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
|
||||
|
||||
this._content = createRef();
|
||||
this._pills = [];
|
||||
}
|
||||
|
||||
_onAssociatedStatusChanged = () => {
|
||||
|
@ -81,7 +83,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
pillifyLinks() {
|
||||
// not present for redacted events
|
||||
if (this._content.current) {
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent);
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +92,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unmountPills(this._pills);
|
||||
const event = this.props.mxEvent;
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
||||
|
@ -130,8 +133,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
const content = getReplacedContent(mxEvent);
|
||||
let contentContainer;
|
||||
if (mxEvent.isRedacted()) {
|
||||
const UnknownBody = sdk.getComponent('messages.UnknownBody');
|
||||
contentContainer = <UnknownBody mxEvent={this.props.mxEvent} />;
|
||||
contentContainer = <RedactedBody mxEvent={this.props.mxEvent} />;
|
||||
} else {
|
||||
let contentElements;
|
||||
if (this.props.previousEdit) {
|
||||
|
@ -156,7 +158,6 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
|
||||
const classes = classNames({
|
||||
"mx_EventTile": true,
|
||||
"mx_EventTile_redacted": mxEvent.isRedacted(),
|
||||
"mx_EventTile_sending": isSending,
|
||||
"mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
|
||||
});
|
||||
|
|
|
@ -170,6 +170,7 @@ export default createReactClass({
|
|||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._iframe = createRef();
|
||||
this._dummyLink = createRef();
|
||||
|
@ -247,6 +248,8 @@ export default createReactClass({
|
|||
});
|
||||
};
|
||||
|
||||
// This button should actually Download because usercontent/ will try to click itself
|
||||
// but it is not guaranteed between various browsers' settings.
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MFileBody_download">
|
||||
|
@ -269,6 +272,8 @@ export default createReactClass({
|
|||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
// only auto-download if a user triggered this iframe explicitly
|
||||
auto: !this.props.decryptedBlob,
|
||||
}, "*");
|
||||
};
|
||||
|
||||
|
@ -290,14 +295,14 @@ export default createReactClass({
|
|||
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
|
||||
onLoad={onIframeLoad}
|
||||
ref={this._iframe}
|
||||
sandbox="allow-scripts allow-downloads" />
|
||||
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
} else if (contentUrl) {
|
||||
const downloadProps = {
|
||||
target: "_blank",
|
||||
rel: "noopener",
|
||||
rel: "noreferrer noopener",
|
||||
|
||||
// We set the href regardless of whether or not we intercept the download
|
||||
// because we don't really want to convert the file to a blob eagerly, and
|
||||
|
|
|
@ -67,11 +67,6 @@ export default class MImageBody extends React.Component {
|
|||
this._image = createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
}
|
||||
|
||||
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
|
||||
onClientSync(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
@ -258,6 +253,9 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
|
|
|
@ -62,8 +62,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
await request.accept();
|
||||
this._openRequest();
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
|
@ -136,9 +136,9 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
} else if (request.cancelled) {
|
||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||
} else if (request.accepting) {
|
||||
stateLabel = _t("accepting …");
|
||||
stateLabel = _t("Accepting …");
|
||||
} else if (request.declining) {
|
||||
stateLabel = _t("declining …");
|
||||
stateLabel = _t("Declining …");
|
||||
}
|
||||
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import Modal from '../../../Modal';
|
|||
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -48,7 +49,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
|
|||
};
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (mxEvent.isEncrypted()) {
|
||||
if (mxEvent.isEncrypted() && !SettingsStore.getValue("feature_cross_signing")) {
|
||||
e2eInfoCallback = onCryptoClick;
|
||||
}
|
||||
|
||||
|
@ -121,10 +122,12 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
onDecrypted = () => {
|
||||
|
@ -133,6 +136,11 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onBeforeRedaction = () => {
|
||||
// When an event is redacted, we can't edit it so update the available actions.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
|
|
|
@ -20,6 +20,8 @@ import createReactClass from 'create-react-class';
|
|||
import * as sdk from '../../../index';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Mjolnir} from "../../../mjolnir/Mjolnir";
|
||||
import RedactedBody from "./RedactedBody";
|
||||
import UnknownBody from "./UnknownBody";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MessageEvent',
|
||||
|
@ -47,6 +49,7 @@ export default createReactClass({
|
|||
maxImageHeight: PropTypes.number,
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._body = createRef();
|
||||
},
|
||||
|
@ -60,8 +63,6 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const UnknownBody = sdk.getComponent('messages.UnknownBody');
|
||||
|
||||
const bodyTypes = {
|
||||
'm.text': sdk.getComponent('messages.TextualBody'),
|
||||
'm.notice': sdk.getComponent('messages.TextualBody'),
|
||||
|
@ -78,7 +79,7 @@ export default createReactClass({
|
|||
const content = this.props.mxEvent.getContent();
|
||||
const type = this.props.mxEvent.getType();
|
||||
const msgtype = content.msgtype;
|
||||
let BodyType = UnknownBody;
|
||||
let BodyType = RedactedBody;
|
||||
if (!this.props.mxEvent.isRedacted()) {
|
||||
// only resolve BodyType if event is not redacted
|
||||
if (type && evTypes[type]) {
|
||||
|
@ -88,6 +89,9 @@ export default createReactClass({
|
|||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
} else {
|
||||
// Fallback to UnknownBody otherwise if not redacted
|
||||
BodyType = UnknownBody;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
46
src/components/views/messages/RedactedBody.tsx
Normal file
46
src/components/views/messages/RedactedBody.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext} from "react";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
const RedactedBody = React.forwardRef<any, IProps>(({mxEvent}, ref) => {
|
||||
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||
|
||||
let text = _t("Message deleted");
|
||||
const unsigned = mxEvent.getUnsigned();
|
||||
const redactedBecauseUserId = unsigned && unsigned.redacted_because && unsigned.redacted_because.sender;
|
||||
if (redactedBecauseUserId && redactedBecauseUserId !== mxEvent.getSender()) {
|
||||
const room = cli.getRoom(mxEvent.getRoomId());
|
||||
const sender = room && room.getMember(redactedBecauseUserId);
|
||||
text = _t("Message deleted by %(name)s", { name: sender ? sender.name : redactedBecauseUserId });
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="mx_RedactedBody" ref={ref}>
|
||||
{ text }
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
export default RedactedBody;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue