1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-11-07 10:46:24 +03:00

Merge remote-tracking branch 'origin/develop' into t3chguy/clear_notifications

This commit is contained in:
Travis Ralston
2019-11-27 19:27:52 -07:00
733 changed files with 50889 additions and 9584 deletions

View File

@@ -15,12 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import { _t } from '../../../languageHandler';
import React from 'react';
import createReactClass from 'create-react-class';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'AuthFooter',
render: function() {

View File

@@ -15,12 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const React = require('react');
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'AuthHeader',
render: function() {

View File

@@ -15,12 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const React = require('react');
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'AuthPage',
render: function() {

View File

@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
@@ -25,7 +24,7 @@ const DIV_ID = 'mx_recaptcha';
/**
* A pure UI component which displays a captcha form.
*/
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'CaptchaForm',
propTypes: {
@@ -90,7 +89,7 @@ module.exports = React.createClass({
+ "authentication");
}
console.log("Rendering to %s", divId);
console.info("Rendering to %s", divId);
this._captchaWidgetId = global.grecaptcha.render(divId, {
sitekey: publicKey,
callback: this.props.onCaptchaResponse,

View File

@@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
import sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
const COUNTRIES_BY_ISO2 = {};
for (const c of COUNTRIES) {
@@ -45,17 +46,25 @@ export default class CountryDropdown extends React.Component {
this._onOptionChange = this._onOptionChange.bind(this);
this._getShortOption = this._getShortOption.bind(this);
let defaultCountry = COUNTRIES[0];
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
if (defaultCountryCode) {
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
if (country) defaultCountry = country;
}
this.state = {
searchQuery: '',
defaultCountry,
};
}
componentWillMount() {
if (!this.props.value) {
// If no value is given, we start with the first
// If no value is given, we start with the default
// country selected, but our parent component
// doesn't know this, therefore we do this.
this.props.onOptionChange(COUNTRIES[0]);
this.props.onOptionChange(this.state.defaultCountry);
}
}
@@ -119,7 +128,7 @@ export default class CountryDropdown extends React.Component {
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating
const value = this.props.value || COUNTRIES[0].iso2;
const value = this.props.value || this.state.defaultCountry.iso2;
return <Dropdown className={this.props.className + " mx_CountryDropdown"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}

View File

@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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,9 +16,10 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'CustomServerDialog',
render: function() {
@@ -33,11 +35,6 @@ module.exports = React.createClass({
"allows you to use this app with an existing Matrix account on a " +
"different homeserver.",
)}</p>
<p>{_t(
"You can also set a custom identity server, but you won't be " +
"able to invite users by email address, or be invited by email " +
"address yourself.",
)}</p>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={true}>

View File

@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,6 +17,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import url from 'url';
import classnames from 'classnames';
@@ -57,13 +59,12 @@ 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.
* makeRegistrationUrl A function that makes a registration URL
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
*/
export const PasswordAuthEntry = React.createClass({
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
statics: {
@@ -81,40 +82,38 @@ export const PasswordAuthEntry = React.createClass({
getInitialState: function() {
return {
passwordValid: false,
password: "",
};
},
focus: function() {
if (this.refs.passwordField) {
this.refs.passwordField.focus();
}
},
_onSubmit: function(e) {
e.preventDefault();
if (this.props.busy) return;
this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE,
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
user: this.props.matrixClient.credentials.userId,
password: this.refs.passwordField.value,
identifier: {
type: "m.id.user",
user: this.props.matrixClient.credentials.userId,
},
password: this.state.password,
});
},
_onPasswordFieldChange: function(ev) {
// enable the submit button iff the password is non-empty
this.setState({
passwordValid: Boolean(this.refs.passwordField.value),
password: ev.target.value,
});
},
render: function() {
let passwordBoxClass = null;
if (this.props.errorText) {
passwordBoxClass = 'error';
}
const passwordBoxClass = classnames({
"error": this.props.errorText,
});
let submitButtonOrSpinner;
if (this.props.busy) {
@@ -124,7 +123,8 @@ export const PasswordAuthEntry = React.createClass({
submitButtonOrSpinner = (
<input type="submit"
className="mx_Dialog_primary"
disabled={!this.state.passwordValid}
disabled={!this.state.password}
value={_t("Continue")}
/>
);
}
@@ -138,17 +138,21 @@ export const PasswordAuthEntry = React.createClass({
);
}
const Field = sdk.getComponent('elements.Field');
return (
<div>
<p>{ _t("To continue, please enter your password.") }</p>
<form onSubmit={this._onSubmit}>
<label htmlFor="passwordField">{ _t("Password:") }</label>
<input
name="passwordField"
ref="passwordField"
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
<Field
id="mx_InteractiveAuthEntryComponents_password"
className={passwordBoxClass}
onChange={this._onPasswordFieldChange}
type="password"
name="passwordField"
label={_t('Password')}
autoFocus={true}
value={this.state.password}
onChange={this._onPasswordFieldChange}
/>
<div className="mx_button_row">
{ submitButtonOrSpinner }
@@ -160,7 +164,7 @@ export const PasswordAuthEntry = React.createClass({
},
});
export const RecaptchaAuthEntry = React.createClass({
export const RecaptchaAuthEntry = createReactClass({
displayName: 'RecaptchaAuthEntry',
statics: {
@@ -187,14 +191,24 @@ export const RecaptchaAuthEntry = React.createClass({
return <Loader />;
}
let errorText = this.props.errorText;
const CaptchaForm = sdk.getComponent("views.auth.CaptchaForm");
const sitePublicKey = this.props.stageParams.public_key;
let sitePublicKey;
if (!this.props.stageParams || !this.props.stageParams.public_key) {
errorText = _t(
"Missing captcha public key in homeserver configuration. Please report " +
"this to your homeserver administrator.",
);
} else {
sitePublicKey = this.props.stageParams.public_key;
}
let errorSection;
if (this.props.errorText) {
if (errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
{ errorText }
</div>
);
}
@@ -210,7 +224,7 @@ export const RecaptchaAuthEntry = React.createClass({
},
});
export const TermsAuthEntry = React.createClass({
export const TermsAuthEntry = createReactClass({
displayName: 'TermsAuthEntry',
statics: {
@@ -316,7 +330,7 @@ export const TermsAuthEntry = React.createClass({
checkboxes.push(
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onClick={() => this._togglePolicy(policy.id)} checked={checked} />
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
</label>,
);
@@ -349,7 +363,7 @@ export const TermsAuthEntry = React.createClass({
},
});
export const EmailIdentityAuthEntry = React.createClass({
export const EmailIdentityAuthEntry = createReactClass({
displayName: 'EmailIdentityAuthEntry',
statics: {
@@ -365,7 +379,6 @@ export const EmailIdentityAuthEntry = React.createClass({
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
makeRegistrationUrl: PropTypes.func.isRequired,
},
getInitialState: function() {
@@ -374,38 +387,6 @@ export const EmailIdentityAuthEntry = React.createClass({
};
},
componentWillMount: function() {
if (this.props.stageState.emailSid === null) {
this.setState({requestingToken: true});
this._requestEmailToken().catch((e) => {
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
}
},
/*
* Requests a verification token by email.
*/
_requestEmailToken: function() {
const nextLink = this.props.makeRegistrationUrl({
client_secret: this.props.clientSecret,
hs_url: this.props.matrixClient.getHomeserverUrl(),
is_url: this.props.matrixClient.getIdentityServerUrl(),
session_id: this.props.authSessionId,
});
return this.props.matrixClient.requestRegisterEmailToken(
this.props.inputs.emailAddress,
this.props.clientSecret,
1, // TODO: Multiple send attempts?
nextLink,
).then((result) => {
this.props.setEmailSid(result.sid);
});
},
render: function() {
if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner");
@@ -424,7 +405,7 @@ export const EmailIdentityAuthEntry = React.createClass({
},
});
export const MsisdnAuthEntry = React.createClass({
export const MsisdnAuthEntry = createReactClass({
displayName: 'MsisdnAuthEntry',
statics: {
@@ -450,6 +431,7 @@ export const MsisdnAuthEntry = React.createClass({
},
componentWillMount: function() {
this._submitUrl = null;
this._sid = null;
this._msisdn = null;
this._tokenBox = null;
@@ -459,7 +441,7 @@ export const MsisdnAuthEntry = React.createClass({
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
});
},
/*
@@ -472,6 +454,7 @@ export const MsisdnAuthEntry = React.createClass({
this.props.clientSecret,
1, // TODO: Multiple send attempts?
).then((result) => {
this._submitUrl = result.submit_url;
this._sid = result.sid;
this._msisdn = result.msisdn;
});
@@ -483,38 +466,56 @@ export const MsisdnAuthEntry = React.createClass({
});
},
_onFormSubmit: function(e) {
_onFormSubmit: async function(e) {
e.preventDefault();
if (this.state.token == '') return;
this.setState({
errorText: null,
});
this.setState({
errorText: null,
});
this.props.matrixClient.submitMsisdnToken(
this._sid, this.props.clientSecret, this.state.token,
).then((result) => {
if (result.success) {
const idServerParsedUrl = url.parse(
this.props.matrixClient.getIdentityServerUrl(),
try {
const requiresIdServerParam =
await this.props.matrixClient.doesServerRequireIdServerParam();
let result;
if (this._submitUrl) {
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
);
} else if (requiresIdServerParam) {
result = await this.props.matrixClient.submitMsisdnToken(
this._sid, this.props.clientSecret, this.state.token,
);
} else {
throw new Error("The registration with MSISDN flow is misconfigured");
}
if (result.success) {
const creds = {
sid: this._sid,
client_secret: this.props.clientSecret,
};
if (requiresIdServerParam) {
const idServerParsedUrl = url.parse(
this.props.matrixClient.getIdentityServerUrl(),
);
creds.id_server = idServerParsedUrl.host;
}
this.props.submitAuthDict({
type: MsisdnAuthEntry.LOGIN_TYPE,
threepid_creds: {
sid: this._sid,
client_secret: this.props.clientSecret,
id_server: idServerParsedUrl.host,
},
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
threepid_creds: creds,
threepidCreds: creds,
});
} else {
this.setState({
errorText: _t("Token incorrect"),
});
}
}).catch((e) => {
} catch (e) {
this.props.fail(e);
console.log("Failed to submit msisdn token");
}).done();
}
},
render: function() {
@@ -564,7 +565,7 @@ export const MsisdnAuthEntry = React.createClass({
},
});
export const FallbackAuthEntry = React.createClass({
export const FallbackAuthEntry = createReactClass({
displayName: 'FallbackAuthEntry',
propTypes: {

View File

@@ -15,91 +15,82 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
import ServerConfig from "./ServerConfig";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?
/*
* Configure the Modular server name.
*
* This is a variant of ServerConfig with only the HS field and different body
* text that is specific to the Modular case.
*/
export default class ModularServerConfig extends React.PureComponent {
static propTypes = {
onServerConfigChange: PropTypes.func,
export default class ModularServerConfig extends ServerConfig {
static propTypes = ServerConfig.propTypes;
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
// This component always uses the default IS URL and doesn't allow it
// to be changed. We still receive it as a prop here to simplify
// consumers by still passing the IS URL via onServerConfigChange.
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
}
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
delayTimeMs: 0,
}
constructor(props) {
super(props);
this.state = {
hsUrl: props.customHsUrl,
};
}
componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl) return;
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({
hsUrl: newProps.customHsUrl,
hsUrl,
isUrl,
busy: true,
errorText: "",
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: this.props.defaultIsUrl,
});
}
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.props.defaultIsUrl,
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
});
}
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
}
_waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) {
clearTimeout(existingTimeoutId);
return null;
}
return setTimeout(fn.bind(this), this.props.delayTimeMs);
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
}
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const submitButton = this.props.submitText
? <AccessibleButton
element="button"
type="submit"
className={this.props.submitClass}
onClick={this.onSubmit}
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
: null;
return (
<div className="mx_ServerConfig">
@@ -113,15 +104,18 @@ export default class ModularServerConfig extends React.PureComponent {
</a>,
},
)}
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Server Name")}
placeholder={this.props.defaultHsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
/>
</div>
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Server Name")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
/>
</div>
{submitButton}
</form>
</div>
);
}

View File

@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 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.
@@ -21,11 +22,30 @@ import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
/**
* A pure UI component which displays a username/password form.
*/
class PasswordLogin extends React.Component {
export default class PasswordLogin extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onEditServerDetailsClick: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
};
static defaultProps = {
onError: function() {},
onEditServerDetailsClick: null,
@@ -40,13 +60,12 @@ class PasswordLogin extends React.Component {
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
// This is optional and only set if we used a server name to determine
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about where to "sign in to".
hsName: null,
hsUrl: "",
disableSubmit: false,
}
};
static LOGIN_FIELD_EMAIL = "login_field_email";
static LOGIN_FIELD_MXID = "login_field_mxid";
static LOGIN_FIELD_PHONE = "login_field_phone";
constructor(props) {
super(props);
@@ -193,10 +212,7 @@ class PasswordLogin extends React.Component {
name="username" // make it a little easier for browser's remember-password
key="username_input"
type="text"
label={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("Username")}
label={_t("Username")}
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
@@ -242,6 +258,7 @@ class PasswordLogin extends React.Component {
render() {
const Field = sdk.getComponent('elements.Field');
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let forgotPasswordJsx;
@@ -258,31 +275,6 @@ class PasswordLogin extends React.Component {
</span>;
}
let signInToText = _t('Sign in to your Matrix account');
if (this.props.hsName) {
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.hsName,
});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
// ignore
}
}
let editLink = null;
if (this.props.onEditServerDetailsClick) {
editLink = <a className="mx_AuthBody_editServerDetails"
href="#" onClick={this.props.onEditServerDetailsClick}
>
{_t('Change')}
</a>;
}
const pwFieldClass = classNames({
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
@@ -295,7 +287,6 @@ class PasswordLogin extends React.Component {
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Field
className="mx_Login_type_dropdown"
id="mx_PasswordLogin_type"
element="select"
value={this.state.loginType}
@@ -326,10 +317,8 @@ class PasswordLogin extends React.Component {
return (
<div>
<h3>
{signInToText}
{editLink}
</h3>
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
<form onSubmit={this.onSubmitForm}>
{loginType}
{loginField}
@@ -353,27 +342,3 @@ class PasswordLogin extends React.Component {
);
}
}
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
hsName: PropTypes.string,
hsUrl: PropTypes.string,
disableSubmit: PropTypes.bool,
};
module.exports = PasswordLogin;

View File

@@ -1,7 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,6 +18,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Email from '../../../email';
@@ -26,6 +28,7 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number';
@@ -38,7 +41,7 @@ const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from of
/**
* A pure UI component which displays a registration form.
*/
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RegistrationForm',
propTypes: {
@@ -51,16 +54,15 @@ module.exports = React.createClass({
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
onEditServerDetailsClick: PropTypes.func,
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
// This is optional and only set if we used a server name to determine
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about "your account".
hsName: PropTypes.string,
hsUrl: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
canSubmit: PropTypes.bool,
serverRequiresIdServer: PropTypes.bool,
},
getDefaultProps: function() {
return {
onValidationChange: console.error,
canSubmit: true,
};
},
@@ -70,33 +72,47 @@ module.exports = React.createClass({
fieldValid: {},
// The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry,
username: "",
email: "",
phoneNumber: "",
password: "",
username: this.props.defaultUsername || "",
email: this.props.defaultEmail || "",
phoneNumber: this.props.defaultPhoneNumber || "",
password: this.props.defaultPassword || "",
passwordConfirm: "",
passwordComplexity: null,
passwordSafe: false,
};
},
onSubmit: async function(ev) {
ev.preventDefault();
if (!this.props.canSubmit) return;
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
if (!allFieldsValid) {
return;
}
const self = this;
if (this.state.email == '') {
if (this.state.email === '') {
const haveIs = Boolean(this.props.serverConfig.isUrl);
let desc;
if (this.props.serverRequiresIdServer && !haveIs) {
desc = _t(
"No identity server is configured so you cannot add an email address in order to " +
"reset your password in the future.",
);
} else {
desc = _t(
"If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?",
);
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?") }
</div>,
description: desc,
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
@@ -150,7 +166,11 @@ module.exports = React.createClass({
if (!field) {
continue;
}
field.validate({ allowEmpty: false });
// We must wait for these validations to finish before queueing
// up the setState below so our setState goes in the queue after
// all the setStates from these validate calls (that's how we
// know they've finished).
await field.validate({ allowEmpty: false });
}
// Validation and state updates are async, so we need to wait for them to complete
@@ -270,12 +290,23 @@ module.exports = React.createClass({
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
const safe = complexity.score >= PASSWORD_MIN_SCORE;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
this.setState({
passwordComplexity: complexity,
passwordSafe: safe,
});
return complexity.score >= PASSWORD_MIN_SCORE;
return allowUnsafe || safe;
},
valid: function() {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
if (!this.state.passwordSafe) {
return _t("Password is allowed, but unsafe");
}
return _t("Nice, strong password!");
},
valid: () => _t("Nice, strong password!"),
invalid: function() {
const complexity = this.state.passwordComplexity;
if (!complexity) {
@@ -367,7 +398,7 @@ module.exports = React.createClass({
},
validateUsernameRules: withValidation({
description: () => _t("Use letters, numbers, dashes and underscores only"),
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
rules: [
{
key: "required",
@@ -406,8 +437,32 @@ module.exports = React.createClass({
});
},
_showEmail() {
const haveIs = Boolean(this.props.serverConfig.isUrl);
if (
(this.props.serverRequiresIdServer && !haveIs) ||
!this._authStepIsUsed('m.login.email.identity')
) {
return false;
}
return true;
},
_showPhoneNumber() {
const threePidLogin = !SdkConfig.get().disable_3pid_login;
const haveIs = Boolean(this.props.serverConfig.isUrl);
if (
!threePidLogin ||
(this.props.serverRequiresIdServer && !haveIs) ||
!this._authStepIsUsed('m.login.msisdn')
) {
return false;
}
return true;
},
renderEmail() {
if (!this._authStepIsUsed('m.login.email.identity')) {
if (!this._showEmail()) {
return null;
}
const Field = sdk.getComponent('elements.Field');
@@ -419,7 +474,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_EMAIL] = field}
type="text"
label={emailPlaceholder}
defaultValue={this.props.defaultEmail}
value={this.state.email}
onChange={this.onEmailChange}
onValidate={this.onEmailValidate}
@@ -433,7 +487,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PASSWORD] = field}
type="password"
label={_t("Password")}
defaultValue={this.props.defaultPassword}
value={this.state.password}
onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate}
@@ -447,7 +500,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
type="password"
label={_t("Confirm")}
defaultValue={this.props.defaultPassword}
value={this.state.passwordConfirm}
onChange={this.onPasswordConfirmChange}
onValidate={this.onPasswordConfirmValidate}
@@ -455,8 +507,7 @@ module.exports = React.createClass({
},
renderPhoneNumber() {
const threePidLogin = !SdkConfig.get().disable_3pid_login;
if (!threePidLogin || !this._authStepIsUsed('m.login.msisdn')) {
if (!this._showPhoneNumber()) {
return null;
}
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
@@ -475,7 +526,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PHONE_NUMBER] = field}
type="text"
label={phoneLabel}
defaultValue={this.props.defaultPhoneNumber}
value={this.state.phoneNumber}
prefix={phoneCountry}
onChange={this.onPhoneNumberChange}
@@ -491,7 +541,6 @@ module.exports = React.createClass({
type="text"
autoFocus={true}
label={_t("Username")}
defaultValue={this.props.defaultUsername}
value={this.state.username}
onChange={this.onUsernameChange}
onValidate={this.onUsernameValidate}
@@ -499,20 +548,22 @@ module.exports = React.createClass({
},
render: function() {
let yourMatrixAccountText = _t('Create your Matrix account');
if (this.props.hsName) {
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.hsName,
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
// ignore
}
}
let editLink = null;
@@ -525,9 +576,38 @@ module.exports = React.createClass({
}
const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
);
let emailHelperText = null;
if (this._showEmail()) {
if (this._showPhoneNumber()) {
emailHelperText = <div>
{_t(
"Set an email for account recovery. " +
"Use email or phone to optionally be discoverable by existing contacts.",
)}
</div>;
} else {
emailHelperText = <div>
{_t(
"Set an email for account recovery. " +
"Use email to optionally be discoverable by existing contacts.",
)}
</div>;
}
}
const haveIs = Boolean(this.props.serverConfig.isUrl);
let noIsText = null;
if (this.props.serverRequiresIdServer && !haveIs) {
noIsText = <div>
{_t(
"No identity server is configured so you cannot add an email address in order to " +
"reset your password in the future.",
)}
</div>;
}
return (
<div>
<h3>
@@ -546,8 +626,8 @@ module.exports = React.createClass({
{this.renderEmail()}
{this.renderPhoneNumber()}
</div>
{_t("Use an email address to recover your account.") + " "}
{_t("Other users can invite you to rooms using your contact details.")}
{ emailHelperText }
{ noIsText }
{ registerButton }
</form>
</div>

View File

@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 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.
@@ -20,6 +21,11 @@ import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
import { createClient } from 'matrix-js-sdk/lib/matrix';
import classNames from 'classnames';
/*
* A pure UI component which displays the HS and IS to use.
@@ -27,82 +33,175 @@ import { _t } from '../../../languageHandler';
export default class ServerConfig extends React.PureComponent {
static propTypes = {
onServerConfigChange: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
// The current configuration that the user is expecting to change.
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
}
// Called after the component calls onServerConfigChange
onAfterSubmit: PropTypes.func,
// Optional text for the submit button. If falsey, no button will be shown.
submitText: PropTypes.string,
// Optional class for the submit button. Only applies if the submit button
// is to be rendered.
submitClass: PropTypes.string,
// Whether the flow this component is embedded in requires an identity
// server when the homeserver says it will need one. Default false.
showIdentityServerIfRequiredByHomeserver: PropTypes.bool,
};
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
customIsUrl: "",
delayTimeMs: 0,
}
};
constructor(props) {
super(props);
this.state = {
hsUrl: props.customHsUrl,
isUrl: props.customIsUrl,
busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
showIdentityServer: false,
};
}
componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl &&
newProps.customIsUrl === this.state.isUrl) return;
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
const result = this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
if (!result) {
return result;
}
// If the UI flow this component is embedded in requires an identity
// server when the homeserver says it will need one, check first and
// reveal this field if not already shown.
// XXX: This a backward compatibility path for homeservers that require
// an identity server to be passed during certain flows.
// See also https://github.com/matrix-org/synapse/pull/5868.
if (
this.props.showIdentityServerIfRequiredByHomeserver &&
!this.state.showIdentityServer &&
await this.isIdentityServerRequiredByHomeserver()
) {
this.setState({
showIdentityServer: true,
});
return null;
}
return result;
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({
hsUrl: defaultConfig.hsUrl,
isUrl: defaultConfig.isUrl,
busy: false,
errorText: "",
});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
hsUrl,
isUrl,
busy: true,
errorText: "",
});
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
if (!stateForError.isFatalError) {
this.setState({
busy: false,
});
// carry on anyway
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
this.props.onServerConfigChange(result);
return result;
} else {
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
return null;
}
}
}
async isIdentityServerRequiredByHomeserver() {
// XXX: We shouldn't have to create a whole new MatrixClient just to
// check if the homeserver requires an identity server... Should it be
// extracted to a static utils function...?
return createClient({
baseUrl: this.state.hsUrl,
}).doesServerRequireIdServerParam();
}
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
this.validateServer();
});
}
};
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
}
};
onIdentityServerBlur = (ev) => {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
this.validateServer();
});
}
};
onIdentityServerChange = (ev) => {
const isUrl = ev.target.value;
this.setState({ isUrl });
}
};
onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
const result = await this.validateServer();
if (!result) return; // Do not continue.
if (this.props.onAfterSubmit) {
this.props.onAfterSubmit();
}
};
_waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) {
@@ -114,35 +213,75 @@ export default class ServerConfig extends React.PureComponent {
showHelpPopup = () => {
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
};
_renderHomeserverSection() {
const Field = sdk.getComponent('elements.Field');
return <div>
{_t("Enter your custom homeserver URL <a>What does this mean?</a>", {}, {
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
{sub}
</a>,
})}
<Field id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
disabled={this.state.busy}
/>
</div>;
}
_renderIdentityServerSection() {
const Field = sdk.getComponent('elements.Field');
const classes = classNames({
"mx_ServerConfig_identityServer": true,
"mx_ServerConfig_identityServer_shown": this.state.showIdentityServer,
});
return <div className={classes}>
{_t("Enter your custom identity server URL <a>What does this mean?</a>", {}, {
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
{sub}
</a>,
})}
<Field id="mx_ServerConfig_isUrl"
label={_t("Identity Server URL")}
placeholder={this.props.serverConfig.isUrl}
value={this.state.isUrl || ''}
onBlur={this.onIdentityServerBlur}
onChange={this.onIdentityServerChange}
disabled={this.state.busy}
/>
</div>;
}
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const errorText = this.state.errorText
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
: null;
const submitButton = this.props.submitText
? <AccessibleButton
element="button"
type="submit"
className={this.props.submitClass}
onClick={this.onSubmit}
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
: null;
return (
<div className="mx_ServerConfig">
<h3>{_t("Other servers")}</h3>
{_t("Enter custom server URLs <a>What does this mean?</a>", {}, {
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
{ sub }
</a>,
})}
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")}
placeholder={this.props.defaultHsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
/>
<Field id="mx_ServerConfig_isUrl"
label={_t("Identity Server URL")}
placeholder={this.props.defaultIsUrl}
value={this.state.isUrl}
onBlur={this.onIdentityServerBlur}
onChange={this.onIdentityServerChange}
/>
</div>
{errorText}
{this._renderHomeserverSection()}
{this._renderIdentityServerSection()}
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
{submitButton}
</form>
</div>
);
}

View File

@@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
@@ -32,8 +34,12 @@ export const TYPES = {
label: () => _t('Free'),
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
description: () => _t('Join millions for free on the largest public server'),
hsUrl: 'https://matrix.org',
isUrl: 'https://vector.im',
serverConfig: makeType(ValidatedServerConfig, {
hsUrl: "https://matrix-client.matrix.org",
hsName: "matrix.org",
hsNameIsDifferent: false,
isUrl: "https://vector.im",
}),
},
PREMIUM: {
id: PREMIUM,
@@ -44,6 +50,7 @@ export const TYPES = {
{sub}
</a>,
}),
identityServerUrl: "https://vector.im",
},
ADVANCED: {
id: ADVANCED,
@@ -56,10 +63,11 @@ export const TYPES = {
},
};
export function getTypeFromHsUrl(hsUrl) {
export function getTypeFromServerConfig(config) {
const {hsUrl} = config;
if (!hsUrl) {
return null;
} else if (hsUrl === TYPES.FREE.hsUrl) {
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
return FREE;
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
// This is an unlikely case to reach, as Modular defaults to hiding the
@@ -76,7 +84,7 @@ export default class ServerTypeSelector extends React.PureComponent {
selected: PropTypes.string,
// Handler called when the selected type changes.
onChange: PropTypes.func.isRequired,
}
};
constructor(props) {
super(props);
@@ -106,7 +114,7 @@ export default class ServerTypeSelector extends React.PureComponent {
e.stopPropagation();
const type = e.currentTarget.dataset.id;
this.updateSelectedType(type);
}
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View File

@@ -0,0 +1,62 @@
/*
Copyright 2019 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 {_t} from "../../../languageHandler";
import sdk from "../../../index";
import PropTypes from "prop-types";
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
export default class SignInToText extends React.PureComponent {
static propTypes = {
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
onEditServerDetailsClick: PropTypes.func,
};
render() {
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
let editLink = null;
if (this.props.onEditServerDetailsClick) {
editLink = <a className="mx_AuthBody_editServerDetails"
href="#" onClick={this.props.onEditServerDetailsClick}
>
{_t('Change')}
</a>;
}
return <h3>
{signInToText}
{editLink}
</h3>;
}
}

View File

@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,12 +18,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'BaseAvatar',
propTypes: {
@@ -55,7 +57,7 @@ module.exports = React.createClass({
return this._getState(this.props);
},
componentWillMount() {
componentDidMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
},
@@ -104,9 +106,13 @@ module.exports = React.createClass({
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]
const urls = props.urls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
let urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
urls = props.urls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
}
}
let defaultImageUrl = null;
@@ -116,6 +122,10 @@ module.exports = React.createClass({
);
urls.push(defaultImageUrl); // lowest priority
}
// deduplicate URLs
urls = Array.from(new Set(urls));
return {
imageUrls: urls,
defaultImageUrl: defaultImageUrl,
@@ -133,40 +143,7 @@ module.exports = React.createClass({
}
},
/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
*/
_getInitialLetter: function(name) {
if (name.length < 1) {
return undefined;
}
let idx = 0;
const initial = name[0];
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
idx++;
}
// string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS).
let chars = 1;
const first = name.charCodeAt(idx);
// check if its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
const second = name.charCodeAt(idx+1);
if (second >= 0xDC00 && second <= 0xDFFF) {
chars++;
}
}
const firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const {
@@ -176,20 +153,20 @@ module.exports = React.createClass({
} = this.props;
if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = this._getInitialLetter(name);
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
>
{ initialLetter }
</EmojiText>
</span>
);
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} />
width={width} height={height} aria-hidden="true" />
);
if (onClick != null) {
return (

View File

@@ -16,10 +16,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
export default React.createClass({
export default createReactClass({
displayName: 'GroupAvatar',
propTypes: {

View File

@@ -14,15 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
const Avatar = require('../../../Avatar');
const sdk = require("../../../index");
const dispatcher = require("../../../dispatcher");
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MemberAvatar',
propTypes: {

View File

@@ -15,13 +15,14 @@ limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import sdk from "../../../index";
import DMRoomMap from '../../../utils/DMRoomMap';
import Avatar from '../../../Avatar';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RoomAvatar',
// Room may be left unset here, but if it is,
@@ -51,7 +52,7 @@ module.exports = React.createClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
},
@@ -89,7 +90,6 @@ module.exports = React.createClass({
props.resizeMethod,
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props), // lowest priority
].filter(function(url) {
return (url != null && url != "");
});
@@ -98,41 +98,14 @@ module.exports = React.createClass({
getRoomAvatarUrl: function(props) {
if (!props.room) return null;
return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
return Avatar.avatarUrlForRoom(
props.room,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
},
getOneToOneAvatar: function(props) {
const room = props.room;
if (!room) {
return null;
}
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {
otherMember = room.getMember(otherUserId);
} else {
// if the room is not marked as a 1:1, but only has max 2 members
// then still try to show any avatar (pref. other member)
otherMember = room.getAvatarFallbackMember();
}
if (otherMember) {
return otherMember.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
}
return null;
},
onRoomAvatarClick: function() {
const avatarUrl = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),

View File

@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
@@ -26,7 +24,7 @@ import PropTypes from 'prop-types';
export default class GenericElementContextMenu extends React.Component {
static PropTypes = {
static propTypes = {
element: PropTypes.element.isRequired,
// Function to be called when the parent window is resized
// This can be used to reposition or close the menu on resize and

View File

@@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
export default class GenericTextContextMenu extends React.Component {
static PropTypes = {
static propTypes = {
message: PropTypes.string.isRequired,
};

View File

@@ -1,5 +1,6 @@
/*
Copyright 2018 Vector Creations Ltd
Copyright 2019 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.
@@ -77,11 +78,12 @@ export default class GroupInviteTileContextMenu extends React.Component {
}
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div>
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
{ _t('Reject') }
</div>
</AccessibleButton>
</div>;
}
}

View File

@@ -1,6 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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.
@@ -17,6 +19,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {EventStatus} from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
@@ -29,7 +32,11 @@ import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
module.exports = React.createClass({
function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}
module.exports = createReactClass({
displayName: 'MessageContextMenu',
propTypes: {
@@ -90,11 +97,36 @@ module.exports = React.createClass({
this.closeMenu();
},
onResendEditClick: function() {
Resend.resend(this.props.mxEvent.replacingEvent());
this.closeMenu();
},
onResendRedactionClick: function() {
Resend.resend(this.props.mxEvent.localRedactionEvent());
this.closeMenu();
},
onResendReactionsClick: function() {
for (const reaction of this._getUnsentReactions()) {
Resend.resend(reaction);
}
this.closeMenu();
},
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onReportEventClick: function() {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent');
this.closeMenu();
},
onViewSourceClick: function() {
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
@@ -119,26 +151,54 @@ module.exports = React.createClass({
onRedactClick: function() {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
onFinished: (proceed) => {
onFinished: async (proceed) => {
if (!proceed) return;
const cli = MatrixClientPeg.get();
cli.redactEvent(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()).catch(function(e) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this.
try {
await cli.redactEvent(
this.props.mxEvent.getRoomId(),
this.props.mxEvent.getId(),
);
} catch (e) {
const code = e.errcode || e.statusCode;
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
title: _t('Error'),
description: _t('You cannot delete this message. (%(code)s)', {code}),
});
}).done();
// only show the dialog if failing for something other than a network error
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
// detached queue and we show the room status bar to allow retry
if (typeof code !== "undefined") {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this.
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
title: _t('Error'),
description: _t('You cannot delete this message. (%(code)s)', {code}),
});
}
}
},
}, 'mx_Dialog_confirmredact');
this.closeMenu();
},
onCancelSendClick: function() {
Resend.removeFromQueue(this.props.mxEvent);
const mxEvent = this.props.mxEvent;
const editEvent = mxEvent.replacingEvent();
const redactEvent = mxEvent.localRedactionEvent();
const pendingReactions = this._getPendingReactions();
if (editEvent && canCancel(editEvent.status)) {
Resend.removeFromQueue(editEvent);
}
if (redactEvent && canCancel(redactEvent.status)) {
Resend.removeFromQueue(redactEvent);
}
if (pendingReactions.length) {
for (const reaction of pendingReactions) {
Resend.removeFromQueue(reaction);
}
}
if (canCancel(mxEvent.status)) {
Resend.removeFromQueue(this.props.mxEvent);
}
this.closeMenu();
},
@@ -207,10 +267,46 @@ module.exports = React.createClass({
this.closeMenu();
},
_getReactions(filter) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId();
return room.getPendingEvents().filter(e => {
const relation = e.getRelation();
return relation &&
relation.rel_type === "m.annotation" &&
relation.event_id === eventId &&
filter(e);
});
},
_getPendingReactions() {
return this._getReactions(e => canCancel(e.status));
},
_getUnsentReactions() {
return this._getReactions(e => e.status === EventStatus.NOT_SENT);
},
render: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
const mxEvent = this.props.mxEvent;
const eventStatus = mxEvent.status;
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
const unsentReactionsCount = this._getUnsentReactions().length;
const pendingReactionsCount = this._getPendingReactions().length;
const allowCancel = canCancel(mxEvent.status) ||
canCancel(editStatus) ||
canCancel(redactStatus) ||
pendingReactionsCount !== 0;
let resendButton;
let resendEditButton;
let resendReactionsButton;
let resendRedactionButton;
let redactButton;
let cancelButton;
let forwardButton;
@@ -223,67 +319,92 @@ module.exports = React.createClass({
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
if (!mxEvent.isRedacted()) {
if (eventStatus === EventStatus.NOT_SENT) {
resendButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
{ _t('Resend') }
</AccessibleButton>
);
}
if (eventStatus === EventStatus.NOT_SENT) {
resendButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
{ _t('Resend') }
</div>
if (editStatus === EventStatus.NOT_SENT) {
resendEditButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
{ _t('Resend edit') }
</AccessibleButton>
);
}
if (unsentReactionsCount !== 0) {
resendReactionsButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
</AccessibleButton>
);
}
}
if (redactStatus === EventStatus.NOT_SENT) {
resendRedactionButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
{ _t('Resend removal') }
</AccessibleButton>
);
}
if (isSent && this.state.canRedact) {
redactButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
{ _t('Remove') }
</div>
</AccessibleButton>
);
}
if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) {
if (allowCancel) {
cancelButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
{ _t('Cancel Sending') }
</div>
</AccessibleButton>
);
}
if (isContentActionable(mxEvent)) {
forwardButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
{ _t('Forward Message') }
</div>
</AccessibleButton>
);
if (this.state.canPin) {
pinButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
</div>
</AccessibleButton>
);
}
}
const viewSourceButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
{ _t('View Source') }
</div>
</AccessibleButton>
);
if (mxEvent.getType() !== mxEvent.getWireType()) {
viewClearSourceButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
{ _t('View Decrypted Source') }
</div>
</AccessibleButton>
);
}
if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) {
unhidePreviewButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
{ _t('Unhide Preview') }
</div>
</AccessibleButton>
);
}
}
@@ -294,20 +415,19 @@ module.exports = React.createClass({
}
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = (
<div className="mx_MessageContextMenu_field">
<a href={permalink}
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>
<AccessibleButton className="mx_MessageContextMenu_field">
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</a>
</div>
</AccessibleButton>
);
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
quoteButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
{ _t('Quote') }
</div>
</AccessibleButton>
);
}
@@ -317,31 +437,52 @@ module.exports = React.createClass({
isUrlPermitted(mxEvent.event.content.external_url)
) {
externalURLButton = (
<div className="mx_MessageContextMenu_field">
<a href={mxEvent.event.content.external_url}
rel="noopener" target="_blank" onClick={this.closeMenu}>{ _t('Source URL') }</a>
</div>
<AccessibleButton className="mx_MessageContextMenu_field">
<a
href={mxEvent.event.content.external_url}
target="_blank"
rel="noopener"
onClick={this.closeMenu}
tabIndex={-1}
>
{ _t('Source URL') }
</a>
</AccessibleButton>
);
}
if (this.props.collapseReplyThread) {
collapseReplyThread = (
<div className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
{ _t('Collapse Reply Thread') }
</div>
</AccessibleButton>
);
}
let e2eInfo;
if (this.props.e2eInfoCallback) {
e2eInfo = <div className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
e2eInfo = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') }
</div>;
</AccessibleButton>
);
}
let reportEventButton;
if (mxEvent.getSender() !== me) {
reportEventButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
{ _t('Report Content') }
</AccessibleButton>
);
}
return (
<div className="mx_MessageContextMenu">
{ resendButton }
{ resendEditButton }
{ resendReactionsButton }
{ resendRedactionButton }
{ redactButton }
{ cancelButton }
{ forwardButton }
@@ -354,6 +495,7 @@ module.exports = React.createClass({
{ externalURLButton }
{ collapseReplyThread }
{ e2eInfo }
{ reportEventButton }
</div>
);
},

View File

@@ -1,6 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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,12 +17,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import Promise from 'bluebird';
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
@@ -30,8 +30,10 @@ import * as Rooms from '../../../Rooms';
import * as RoomNotifs from '../../../RoomNotifs';
import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
import {sleep} from "../../../utils/promise";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RoomTileContextMenu',
propTypes: {
@@ -60,7 +62,7 @@ module.exports = React.createClass({
_toggleTag: function(tagNameOn, tagNameOff) {
if (!MatrixClientPeg.get().isGuest()) {
Promise.delay(500).then(() => {
sleep(500).then(() => {
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
@@ -117,7 +119,7 @@ module.exports = React.createClass({
Rooms.guessAndSetDMRoom(
this.props.room, newIsDirectMessage,
).delay(500).finally(() => {
).then(sleep(500)).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
@@ -158,8 +160,12 @@ module.exports = React.createClass({
_onClickForget: function() {
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
MatrixClientPeg.get().forget(this.props.room.roomId).then(() => {
// Switch to another room view if we're currently viewing the
// historical room
if (RoomViewStore.getRoomId() === this.props.room.roomId) {
dis.dispatch({ action: 'view_next_room' });
}
}, function(err) {
const errCode = err.errcode || _td("unknown error code");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@@ -184,10 +190,10 @@ module.exports = React.createClass({
this.setState({
roomNotifState: newState,
});
RoomNotifs.setRoomNotifsState(roomId, newState).done(() => {
RoomNotifs.setRoomNotifsState(roomId, newState).then(() => {
// delay slightly so that the user can see their state change
// before closing the menu
return Promise.delay(500).then(() => {
return sleep(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {
@@ -222,6 +228,8 @@ module.exports = React.createClass({
},
_renderNotifMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const alertMeClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
@@ -244,29 +252,29 @@ module.exports = React.createClass({
return (
<div className="mx_RoomTileContextMenu">
<div className="mx_RoomTileContextMenu_notif_picker" >
<div className="mx_RoomTileContextMenu_notif_picker">
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" />
</div>
<div className={alertMeClasses} onClick={this._onClickAlertMe} >
<AccessibleButton className={alertMeClasses} onClick={this._onClickAlertMe}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off-copy.svg")} width="16" height="12" />
{ _t('All messages (noisy)') }
</div>
<div className={allNotifsClasses} onClick={this._onClickAllNotifs} >
</AccessibleButton>
<AccessibleButton className={allNotifsClasses} onClick={this._onClickAllNotifs}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off.svg")} width="16" height="12" />
{ _t('All messages') }
</div>
<div className={mentionsClasses} onClick={this._onClickMentions} >
</AccessibleButton>
<AccessibleButton className={mentionsClasses} onClick={this._onClickMentions}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-mentions.svg")} width="16" height="12" />
{ _t('Mentions only') }
</div>
<div className={muteNotifsClasses} onClick={this._onClickMute} >
</AccessibleButton>
<AccessibleButton className={muteNotifsClasses} onClick={this._onClickMute}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute.svg")} width="16" height="12" />
{ _t('Mute') }
</div>
</AccessibleButton>
</div>
);
},
@@ -282,12 +290,13 @@ module.exports = React.createClass({
},
_renderSettingsMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div>
<div className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
<AccessibleButton className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" />
{ _t('Settings') }
</div>
</AccessibleButton>
</div>
);
},
@@ -297,6 +306,8 @@ module.exports = React.createClass({
return null;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let leaveClickHandler = null;
let leaveText = null;
@@ -318,15 +329,17 @@ module.exports = React.createClass({
return (
<div>
<div className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} >
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
{ leaveText }
</div>
</AccessibleButton>
</div>
);
},
_renderRoomTagMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const favouriteClasses = classNames({
'mx_RoomTileContextMenu_tag_field': true,
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite,
@@ -347,21 +360,21 @@ module.exports = React.createClass({
return (
<div>
<div className={favouriteClasses} onClick={this._onClickFavourite} >
<AccessibleButton className={favouriteClasses} onClick={this._onClickFavourite} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_fave.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_fave_on.svg")} width="15" height="15" />
{ _t('Favourite') }
</div>
<div className={lowPriorityClasses} onClick={this._onClickLowPriority} >
</AccessibleButton>
<AccessibleButton className={lowPriorityClasses} onClick={this._onClickLowPriority} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_low.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_low_on.svg")} width="15" height="15" />
{ _t('Low Priority') }
</div>
<div className={dmClasses} onClick={this._onClickDM} >
</AccessibleButton>
<AccessibleButton className={dmClasses} onClick={this._onClickDM} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_person.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_person_on.svg")} width="15" height="15" />
{ _t('Direct Chat') }
</div>
</AccessibleButton>
</div>
);
},
@@ -369,25 +382,27 @@ module.exports = React.createClass({
render: function() {
const myMembership = this.props.room.getMyMembership();
// Can't set notif level or tags on non-join rooms
if (myMembership !== 'join') {
return <div>
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderSettingsMenu() }
</div>;
switch (myMembership) {
case 'join':
return <div>
{ this._renderNotifMenu() }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderRoomTagMenu() }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderSettingsMenu() }
</div>;
case 'invite':
return <div>
{ this._renderLeaveMenu(myMembership) }
</div>;
default:
return <div>
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderSettingsMenu() }
</div>;
}
return (
<div>
{ this._renderNotifMenu() }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderRoomTagMenu() }
<hr className="mx_RoomTileContextMenu_separator" />
{ this._renderSettingsMenu() }
</div>
);
},
});

View File

@@ -1,5 +1,6 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 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,12 +24,17 @@ import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from "../../../index";
export class TopLeftMenu extends React.Component {
static propTypes = {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
onFinished: PropTypes.func,
// Optional function to collect a reference to the container
// of this component directly.
containerRef: PropTypes.func,
};
constructor() {
@@ -52,6 +58,8 @@ export class TopLeftMenu extends React.Component {
}
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const isGuest = MatrixClientPeg.get().isGuest();
const hostingSignupLink = getHostingLink('user-context-menu');
@@ -61,44 +69,56 @@ export class TopLeftMenu extends React.Component {
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
let homePageSection = null;
let homePageItem = null;
if (this.hasHomePage()) {
homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
</ul>;
homePageItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
{_t("Home")}
</AccessibleButton>
);
}
let signInOutSection;
let signInOutItem;
if (isGuest) {
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
</ul>;
signInOutItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
{_t("Sign in")}
</AccessibleButton>
);
} else {
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
</ul>;
signInOutItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
{_t("Sign out")}
</AccessibleButton>
);
}
return <div className="mx_TopLeftMenu">
<div className="mx_TopLeftMenu_section_noIcon">
const settingsItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
{_t("Settings")}
</AccessibleButton>
);
return <div className="mx_TopLeftMenu" ref={this.props.containerRef}>
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText">{this.props.userId}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
{homePageSection}
<ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
{homePageItem}
{settingsItem}
{signInOutItem}
</ul>
{signInOutSection}
</div>;
}

View File

@@ -0,0 +1,134 @@
/*
Copyright 2019 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 sdk from '../../../index';
import {_t} from '../../../languageHandler';
export default class WidgetContextMenu extends React.Component {
static propTypes = {
onFinished: PropTypes.func,
// Callback for when the revoke button is clicked. Required.
onRevokeClicked: PropTypes.func.isRequired,
// Callback for when the snapshot button is clicked. Button not shown
// without a callback.
onSnapshotClicked: PropTypes.func,
// Callback for when the reload button is clicked. Button not shown
// without a callback.
onReloadClicked: PropTypes.func,
// Callback for when the edit button is clicked. Button not shown
// without a callback.
onEditClicked: PropTypes.func,
// Callback for when the delete button is clicked. Button not shown
// without a callback.
onDeleteClicked: PropTypes.func,
};
proxyClick(fn) {
fn();
if (this.props.onFinished) this.props.onFinished();
}
// XXX: It's annoying that our context menus require us to hit onFinished() to close :(
onEditClicked = () => {
this.proxyClick(this.props.onEditClicked);
};
onReloadClicked = () => {
this.proxyClick(this.props.onReloadClicked);
};
onSnapshotClicked = () => {
this.proxyClick(this.props.onSnapshotClicked);
};
onDeleteClicked = () => {
this.proxyClick(this.props.onDeleteClicked);
};
onRevokeClicked = () => {
this.proxyClick(this.props.onRevokeClicked);
};
render() {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const options = [];
if (this.props.onEditClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
{_t("Edit")}
</AccessibleButton>,
);
}
if (this.props.onReloadClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked}
key='reload'>
{_t("Reload")}
</AccessibleButton>,
);
}
if (this.props.onSnapshotClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked}
key='snap'>
{_t("Take picture")}
</AccessibleButton>,
);
}
if (this.props.onDeleteClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked}
key='delete'>
{_t("Remove for everyone")}
</AccessibleButton>,
);
}
// Push this last so it appears last. It's always present.
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
{_t("Remove for me")}
</AccessibleButton>,
);
// Put separators between the options
if (options.length > 1) {
const length = options.length;
for (let i = 0; i < length - 1; i++) {
const sep = <hr key={i} className="mx_WidgetContextMenu_separator" />;
// Insert backwards so the insertions don't affect our math on where to place them.
// We also use our cached length to avoid worrying about options.length changing
options.splice(length - 1 - i, 0, sep);
}
}
return <div className="mx_WidgetContextMenu">{options}</div>;
}
}

View File

@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'CreateRoomButton',
propTypes: {
onCreateRoom: PropTypes.func,

View File

@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const React = require('react');
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
const Presets = {
@@ -26,7 +25,7 @@ const Presets = {
Custom: "custom",
};
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: PropTypes.func,

View File

@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RoomAlias',
propTypes: {
// Specifying a homeserver will make magical things happen when you,

View File

@@ -1,6 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018, 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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.
@@ -17,13 +19,19 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Promise from 'bluebird';
import dis from '../../../dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from "../../../email";
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -35,7 +43,7 @@ const addressTypeName = {
};
module.exports = React.createClass({
module.exports = createReactClass({
displayName: "AddressPickerDialog",
propTypes: {
@@ -44,7 +52,7 @@ module.exports = React.createClass({
// Extra node inserted after picker input, dropdown and errors
extraNode: PropTypes.node,
value: PropTypes.string,
placeholder: PropTypes.string,
placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
roomId: PropTypes.string,
button: PropTypes.string,
focus: PropTypes.bool,
@@ -69,13 +77,18 @@ module.exports = React.createClass({
},
getInitialState: function() {
return {
error: false,
let validAddressTypes = this.props.validAddressTypes;
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) {
validAddressTypes = validAddressTypes.filter(type => type !== "email");
}
return {
// Whether to show an error message because of an invalid address
invalidAddressError: false,
// List of UserAddressType objects representing
// the list of addresses we're going to invite
selectedList: [],
// Whether a search is ongoing
busy: false,
// An error message generated during the user directory search
@@ -87,6 +100,9 @@ module.exports = React.createClass({
// List of UserAddressType objects representing the set of
// auto-completion results for the current search query.
suggestedList: [],
// List of address types initialised from props, but may change while the
// dialog is open and represents the supported list of address types at this time.
validAddressTypes,
};
},
@@ -97,12 +113,21 @@ module.exports = React.createClass({
}
},
getPlaceholder() {
const { placeholder } = this.props;
if (typeof placeholder === "string") {
return placeholder;
}
// Otherwise it's a function, as checked by prop types.
return placeholder(this.state.validAddressTypes);
},
onButtonClick: function() {
let selectedList = this.state.selectedList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local selectedList
if (this.refs.textinput.value !== '') {
selectedList = this._addInputToList();
selectedList = this._addAddressesToList([this.refs.textinput.value]);
if (selectedList === null) return;
}
this.props.onFinished(true, selectedList);
@@ -140,12 +165,12 @@ module.exports = React.createClass({
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
this._addInputToList();
this._addAddressesToList([this.refs.textinput.value]);
}
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
e.stopPropagation();
e.preventDefault();
this._addInputToList();
this._addAddressesToList([this.refs.textinput.value]);
}
},
@@ -205,7 +230,7 @@ module.exports = React.createClass({
onSelected: function(index) {
const selectedList = this.state.selectedList.slice();
selectedList.push(this.state.suggestedList[index]);
selectedList.push(this._getFilteredSuggestions()[index]);
this.setState({
selectedList,
suggestedList: [],
@@ -241,7 +266,7 @@ module.exports = React.createClass({
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
}).done(() => {
}).then(() => {
this.setState({
busy: false,
});
@@ -354,7 +379,7 @@ module.exports = React.createClass({
// Do a local search immediately
this._doLocalSearch(query);
}
}).done(() => {
}).then(() => {
this.setState({
busy: false,
});
@@ -430,7 +455,7 @@ module.exports = React.createClass({
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (this.props.validAddressTypes.includes(addrType)) {
if (this.state.validAddressTypes.includes(addrType)) {
if (addrType === 'email' && !Email.looksValid(query)) {
this.setState({searchError: _t("That doesn't look like a valid email address")});
return;
@@ -442,56 +467,62 @@ module.exports = React.createClass({
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType === 'email') {
this._lookupThreepid(addrType, query).done();
this._lookupThreepid(addrType, query);
}
}
this.setState({
suggestedList,
error: false,
invalidAddressError: false,
}, () => {
if (this.addressSelector) this.addressSelector.moveSelectionTop();
});
},
_addInputToList: function() {
const addressText = this.refs.textinput.value.trim();
const addrType = getAddressType(addressText);
const addrObj = {
addressType: addrType,
address: addressText,
isKnown: false,
};
if (!this.props.validAddressTypes.includes(addrType)) {
this.setState({ error: true });
return null;
} else if (addrType === 'mx-user-id') {
const user = MatrixClientPeg.get().getUser(addrObj.address);
if (user) {
addrObj.displayName = user.displayName;
addrObj.avatarMxc = user.avatarUrl;
addrObj.isKnown = true;
}
} else if (addrType === 'mx-room-id') {
const room = MatrixClientPeg.get().getRoom(addrObj.address);
if (room) {
addrObj.displayName = room.name;
addrObj.avatarMxc = room.avatarUrl;
addrObj.isKnown = true;
}
}
_addAddressesToList: function(addressTexts) {
const selectedList = this.state.selectedList.slice();
selectedList.push(addrObj);
let hasError = false;
addressTexts.forEach((addressText) => {
addressText = addressText.trim();
const addrType = getAddressType(addressText);
const addrObj = {
addressType: addrType,
address: addressText,
isKnown: false,
};
if (!this.state.validAddressTypes.includes(addrType)) {
hasError = true;
} else if (addrType === 'mx-user-id') {
const user = MatrixClientPeg.get().getUser(addrObj.address);
if (user) {
addrObj.displayName = user.displayName;
addrObj.avatarMxc = user.avatarUrl;
addrObj.isKnown = true;
}
} else if (addrType === 'mx-room-id') {
const room = MatrixClientPeg.get().getRoom(addrObj.address);
if (room) {
addrObj.displayName = room.name;
addrObj.avatarMxc = room.avatarUrl;
addrObj.isKnown = true;
}
}
selectedList.push(addrObj);
});
this.setState({
selectedList,
suggestedList: [],
query: "",
invalidAddressError: hasError ? true : this.state.invalidAddressError,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return selectedList;
return hasError ? null : selectedList;
},
_lookupThreepid: function(medium, address) {
_lookupThreepid: async function(medium, address) {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
@@ -502,36 +533,44 @@ module.exports = React.createClass({
};
// wait a bit to let the user finish typing
return Promise.delay(500).then(() => {
if (cancelled) return null;
return MatrixClientPeg.get().lookupThreePid(medium, address);
}).then((res) => {
if (res === null || !res.mxid) return null;
await sleep(500);
if (cancelled) return null;
try {
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
if (cancelled) return null;
return MatrixClientPeg.get().getProfileInfo(res.mxid);
}).then((res) => {
if (res === null) return null;
if (cancelled) return null;
const lookup = await MatrixClientPeg.get().lookupThreePid(
medium,
address,
undefined /* callback */,
identityAccessToken,
);
if (cancelled || lookup === null || !lookup.mxid) return null;
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
if (cancelled || profile === null) return null;
this.setState({
suggestedList: [{
// a UserAddressType
addressType: medium,
address: address,
displayName: res.displayname,
avatarMxc: res.avatar_url,
displayName: profile.displayname,
avatarMxc: profile.avatar_url,
isKnown: true,
}],
});
});
} catch (e) {
console.error(e);
this.setState({
searchError: _t('Something went wrong!'),
});
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
_getFilteredSuggestions: function() {
// map addressType => set of addresses to avoid O(n*m) operation
const selectedAddresses = {};
this.state.selectedList.forEach(({address, addressType}) => {
@@ -540,9 +579,50 @@ module.exports = React.createClass({
});
// Filter out any addresses in the above already selected addresses (matching both type and address)
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
return this.state.suggestedList.filter(({address, addressType}) => {
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
});
},
_onPaste: function(e) {
// Prevent the text being pasted into the textarea
e.preventDefault();
const text = e.clipboardData.getData("text");
// Process it as a list of addresses to add instead
this._addAddressesToList(text.split(/[\s,]+/));
},
onUseDefaultIdentityServerClick(e) {
e.preventDefault();
// Update the IS in account data. Actually using it may trigger terms.
// eslint-disable-next-line react-hooks/rules-of-hooks
useDefaultIdentityServer();
// Add email as a valid address type.
const { validAddressTypes } = this.state;
validAddressTypes.push('email');
this.setState({ validAddressTypes });
},
onManageSettingsClick(e) {
e.preventDefault();
dis.dispatch({ action: 'view_user_settings' });
this.onCancel();
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
let inputLabel;
if (this.props.description) {
inputLabel = <div className="mx_AddressPickerDialog_label">
<label htmlFor="textinput">{this.props.description}</label>
</div>;
}
const query = [];
// create the invite list
@@ -562,22 +642,26 @@ module.exports = React.createClass({
// Add the query at the end
query.push(
<textarea key={this.state.selectedList.length}
<textarea
key={this.state.selectedList.length}
onPaste={this._onPaste}
rows="1"
id="textinput"
ref="textinput"
className="mx_AddressPickerDialog_input"
onChange={this.onQueryChanged}
placeholder={this.props.placeholder}
placeholder={this.getPlaceholder()}
defaultValue={this.props.value}
autoFocus={this.props.focus}>
</textarea>,
);
const filteredSuggestedList = this._getFilteredSuggestions();
let error;
let addressSelector;
if (this.state.error) {
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
if (this.state.invalidAddressError) {
const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t]));
error = <div className="mx_AddressPickerDialog_error">
{ _t("You have entered an invalid address.") }
<br />
@@ -600,17 +684,45 @@ module.exports = React.createClass({
);
}
let identityServer;
// If picker cannot currently accept e-mail but should be able to
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes('email')
&& this.props.validAddressTypes.includes('email')) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) {
identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
"Use an identity server to invite by email. " +
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
"or manage in <settings>Settings</settings>.",
{
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
},
{
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
},
)}</div>;
} else {
identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
"Use an identity server to invite by email. " +
"Manage in <settings>Settings</settings>.",
{}, {
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
},
)}</div>;
}
}
return (
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished} title={this.props.title}>
<div className="mx_AddressPickerDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>
{inputLabel}
<div className="mx_Dialog_content">
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
{ error }
{ addressSelector }
{ this.props.extraNode }
{ identityServer }
</div>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onButtonClick}

View File

@@ -16,12 +16,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
export default React.createClass({
export default createReactClass({
propTypes: {
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
onInviteAnyways: PropTypes.func.isRequired,

View File

@@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import FocusTrap from 'focus-trap-react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@@ -25,6 +26,7 @@ import { MatrixClient } from 'matrix-js-sdk';
import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
/**
* Basic container for modal dialogs.
@@ -32,7 +34,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
export default React.createClass({
export default createReactClass({
displayName: 'BaseDialog',
propTypes: {
@@ -113,8 +115,9 @@ export default React.createClass({
render: function() {
let cancelButton;
if (this.props.hasCancel) {
cancelButton = <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton">
</AccessibleButton>;
cancelButton = (
<AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("Close dialog")} />
);
}
return (

View File

@@ -1,6 +1,8 @@
/*
Copyright 2017 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,6 +18,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
@@ -50,6 +53,13 @@ export default class BugReportDialog extends React.Component {
}
_onSubmit(ev) {
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
this.setState({
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
});
return;
}
const userText =
(this.state.text.length > 0 ? this.state.text + '\n\n': '') + 'Issue: ' +
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
@@ -62,6 +72,7 @@ export default class BugReportDialog extends React.Component {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
label: this.props.label,
}).then(() => {
if (!this._unmounted) {
this.props.onFinished(false);
@@ -93,7 +104,7 @@ export default class BugReportDialog extends React.Component {
this.setState({ issueUrl: ev.target.value });
}
_onSendLogsChange(ev) {
_onSendLogsChange(ev) {
this.setState({ sendLogs: ev.target.checked });
}
@@ -165,6 +176,7 @@ 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")}
@@ -193,5 +205,5 @@ export default class BugReportDialog extends React.Component {
}
BugReportDialog.propTypes = {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
};

View File

@@ -1,5 +1,6 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,6 +16,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import request from 'browser-request';
import { _t } from '../../../languageHandler';
@@ -99,7 +101,7 @@ export default class ChangelogDialog extends React.Component {
}
ChangelogDialog.propTypes = {
version: React.PropTypes.string.isRequired,
newVersion: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
version: PropTypes.string.isRequired,
newVersion: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};

View File

@@ -1,198 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
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 PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
import Unread from '../../../Unread';
import classNames from 'classnames';
export default class ChatCreateOrReuseDialog extends React.Component {
constructor(props) {
super(props);
this.onFinished = this.onFinished.bind(this);
this.onRoomTileClick = this.onRoomTileClick.bind(this);
this.state = {
tiles: [],
profile: {
displayName: null,
avatarUrl: null,
},
profileError: null,
};
}
componentWillMount() {
const client = MatrixClientPeg.get();
const dmRoomMap = new DMRoomMap(client);
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.userId);
const RoomTile = sdk.getComponent("rooms.RoomTile");
const tiles = [];
for (const roomId of dmRooms) {
const room = client.getRoom(roomId);
if (room) {
const isInvite = room.getMyMembership() === "invite";
const highlight = room.getUnreadNotificationCount('highlight') > 0 || isInvite;
tiles.push(
<RoomTile key={room.roomId} room={room}
transparent={true}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight}
isInvite={isInvite}
onClick={this.onRoomTileClick}
/>,
);
}
}
this.setState({
tiles: tiles,
});
if (tiles.length === 0) {
this.setState({
busyProfile: true,
});
MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
const profile = {
displayName: resp.displayname,
avatarUrl: null,
};
if (resp.avatar_url) {
profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
resp.avatar_url, 48, 48, "crop",
);
}
this.setState({
busyProfile: false,
profile: profile,
});
}, (err) => {
console.error(
'Unable to get profile for user ' + this.props.userId + ':',
err,
);
this.setState({
busyProfile: false,
profileError: err,
});
});
}
}
onRoomTileClick(roomId) {
this.props.onExistingRoomSelected(roomId);
}
onFinished() {
this.props.onFinished(false);
}
render() {
let title = '';
let content = null;
if (this.state.tiles.length > 0) {
// Show the existing rooms with a "+" to add a new dm
title = _t('Create a new chat or reuse an existing one');
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
});
const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.props.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src={require("../../../../res/img/create-big.svg")} width="26" height="26" />
</div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>;
content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
{ startNewChat }
</div>
</div>;
} else {
// Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting');
let profile = null;
if (this.state.busyProfile) {
profile = <Spinner />;
} else if (this.state.profileError) {
profile = <div className="error" role="alert">
Unable to load profile information for { this.props.userId }
</div>;
} else {
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
<BaseAvatar
name={this.state.profile.displayName || this.props.userId}
url={this.state.profile.avatarUrl}
width={48} height={48}
/>
<div className="mx_ChatCreateOrReuseDialog_profile_name">
{ this.state.profile.displayName || this.props.userId }
</div>
</div>;
}
content = <div>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<DialogButtons primaryButton={_t('Start Chatting')}
onPrimaryButtonClick={this.props.onNewDMClick} focus={true} />
</div>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={this.onFinished}
title={title}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>
);
}
}
ChatCreateOrReuseDialog.propTypes = {
userId: PropTypes.string.isRequired,
// Called when clicking outside of the dialog
onFinished: PropTypes.func.isRequired,
onNewDMClick: PropTypes.func.isRequired,
onExistingRoomSelected: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,90 @@
/*
Copyright 2019 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 sdk from '../../../index';
import { _t } from '../../../languageHandler';
/*
* A dialog for confirming a redaction.
* Also shows a spinner (and possible error) while the redaction is ongoing,
* and only closes the dialog when the redaction is done or failed.
*
* This is done to prevent the edit history dialog racing with the redaction:
* if this dialog closes and the MessageEditHistoryDialog is shown again,
* it will fetch the relations again, which will race with the ongoing /redact request.
* which will cause the edit to appear unredacted.
*
* To avoid this, we keep the dialog open as long as /redact is in progress.
*/
export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isRedacting: false,
redactionErrorCode: null,
};
}
onParentFinished = async (proceed) => {
if (proceed) {
this.setState({isRedacting: true});
try {
await this.props.redact();
this.props.onFinished(true);
} catch (error) {
const code = error.errcode || error.statusCode;
if (typeof code !== "undefined") {
this.setState({redactionErrorCode: code});
} else {
this.props.onFinished(true);
}
}
} else {
this.props.onFinished(false);
}
};
render() {
if (this.state.isRedacting) {
if (this.state.redactionErrorCode) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const code = this.state.redactionErrorCode;
return (
<ErrorDialog
onFinished={this.props.onFinished}
title={_t('Error')}
description={_t('You cannot delete this message. (%(code)s)', {code})}
/>
);
} else {
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
const Spinner = sdk.getComponent('elements.Spinner');
return (
<BaseDialog
onFinished={this.props.onFinished}
hasCancel={false}
title={_t("Removing…")}>
<Spinner />
</BaseDialog>
);
}
} else {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
return <ConfirmRedactDialog onFinished={this.onParentFinished} />;
}
}
}

View File

@@ -15,13 +15,14 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
/*
* A dialog for confirming a redaction.
*/
export default React.createClass({
export default createReactClass({
displayName: 'ConfirmRedactDialog',
render: function() {

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
@@ -29,7 +30,7 @@ import { GroupMemberType } from '../../../groups';
* to make it obvious what is going to happen.
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
export default React.createClass({
export default createReactClass({
displayName: 'ConfirmUserActionDialog',
propTypes: {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
@@ -49,10 +50,10 @@ export default React.createClass({
onFinished: PropTypes.func.isRequired,
},
defaultProps: {
getDefaultProps: () => ({
danger: false,
askReason: false,
},
}),
componentWillMount: function() {
this._reasonField = null;

View File

@@ -0,0 +1,61 @@
/*
Copyright 2019 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 {_t} from "../../../languageHandler";
import sdk from "../../../index";
export default class ConfirmWipeDeviceDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onConfirm = () => {
this.props.onFinished(true);
};
_onDecline = () => {
this.props.onFinished(false);
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Clear all data on this device?")}>
<div className='mx_ConfirmWipeDeviceDialog_content'>
<p>
{_t(
"Clearing all data from this device is permanent. Encrypted messages will be lost " +
"unless their keys have been backed up.",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("Clear all data")}
onPrimaryButtonClick={this._onConfirm}
primaryButtonClass="danger"
cancelButton={_t("Cancel")}
onCancel={this._onDecline}
/>
</BaseDialog>
);
}
}

View File

@@ -15,13 +15,14 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
export default React.createClass({
export default createReactClass({
displayName: 'CreateGroupDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
@@ -92,7 +93,7 @@ export default React.createClass({
this.setState({createError: e});
}).finally(() => {
this.setState({creating: false});
}).done();
});
},
_onCancel: function() {

View File

@@ -15,58 +15,187 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
export default React.createClass({
export default createReactClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
},
componentWillMount: function() {
getInitialState() {
const config = SdkConfig.get();
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
this.defaultNoFederate = config.default_federate === false;
return {
isPublic: false,
name: "",
topic: "",
alias: "",
detailsOpen: false,
noFederate: config.default_federate === false,
nameIsValid: false,
};
},
onOk: function() {
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
_roomCreateOptions() {
const createOpts = {};
createOpts.name = this.state.name;
if (this.state.isPublic) {
createOpts.visibility = "public";
createOpts.preset = "public_chat";
// to prevent createRoom from enabling guest access
createOpts['initial_state'] = [];
const {alias} = this.state;
const localPart = alias.substr(1, alias.indexOf(":") - 1);
createOpts['room_alias_name'] = localPart;
}
if (this.state.topic) {
createOpts.topic = this.state.topic;
}
if (this.state.noFederate) {
createOpts.creation_content = {'m.federate': false};
}
return createOpts;
},
componentDidMount() {
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
// move focus to first field when showing dialog
this._nameFieldRef.focus();
},
componentWillUnmount() {
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
},
_onKeyDown: function(event) {
if (event.key === Key.ENTER) {
this.onOk();
event.preventDefault();
event.stopPropagation();
}
},
onOk: async function() {
const activeElement = document.activeElement;
if (activeElement) {
activeElement.blur();
}
await this._nameFieldRef.validate({allowEmpty: false});
if (this._aliasFieldRef) {
await this._aliasFieldRef.validate({allowEmpty: false});
}
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve));
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
this.props.onFinished(true, this._roomCreateOptions());
} else {
let field;
if (!this.state.nameIsValid) {
field = this._nameFieldRef;
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
field = this._aliasFieldRef;
}
if (field) {
field.focus();
field.validate({ allowEmpty: false, focused: true });
}
}
},
onCancel: function() {
this.props.onFinished(false);
},
onNameChange(ev) {
this.setState({name: ev.target.value});
},
onTopicChange(ev) {
this.setState({topic: ev.target.value});
},
onPublicChange(isPublic) {
this.setState({isPublic});
},
onAliasChange(alias) {
this.setState({alias});
},
onDetailsToggled(ev) {
this.setState({detailsOpen: ev.target.open});
},
onNoFederateChange(noFederate) {
this.setState({noFederate});
},
collectDetailsRef(ref) {
this._detailsRef = ref;
},
async onNameValidate(fieldState) {
const result = await this._validateRoomName(fieldState);
this.setState({nameIsValid: result.valid});
return result;
},
_validateRoomName: withValidation({
rules: [
{
key: "required",
test: async ({ value }) => !!value,
invalid: () => _t("Please enter a name for the room"),
},
],
}),
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field');
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let privateLabel;
let publicLabel;
let aliasField;
if (this.state.isPublic) {
publicLabel = (<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} />
</div>
);
} else {
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
title={_t('Create Room')}
title={title}
>
<form onSubmit={this.onOk}>
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
<div className="mx_CreateRoomDialog_input_container">
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
</div>
<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} />
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ privateLabel }
{ publicLabel }
{ 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>
<LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
</details>
</div>
</form>

View File

@@ -36,7 +36,7 @@ export default class DeactivateAccountDialog extends React.Component {
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
this.state = {
confirmButtonEnabled: false,
password: "",
busy: false,
shouldErase: false,
errStr: null,
@@ -45,7 +45,7 @@ export default class DeactivateAccountDialog extends React.Component {
_onPasswordFieldChange(ev) {
this.setState({
confirmButtonEnabled: Boolean(ev.target.value),
password: ev.target.value,
});
}
@@ -63,14 +63,20 @@ export default class DeactivateAccountDialog extends React.Component {
// 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,
password: this._passwordField.value,
identifier: {
type: "m.id.user",
user: MatrixClientPeg.get().credentials.userId,
},
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) {
if (err.httpStatus === 401 || err.httpStatus === 403) {
errStr = _t('Incorrect password');
Velocity(this._passwordField, "callout.shake", 300);
}
@@ -83,7 +89,7 @@ export default class DeactivateAccountDialog extends React.Component {
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(false);
this.props.onFinished(true);
}
_onCancel() {
@@ -104,7 +110,7 @@ export default class DeactivateAccountDialog extends React.Component {
}
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
const okEnabled = this.state.password && !this.state.busy;
let cancelButton = null;
if (!this.state.busy) {
@@ -113,6 +119,8 @@ export default class DeactivateAccountDialog extends React.Component {
</button>;
}
const Field = sdk.getComponent('elements.Field');
return (
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
@@ -167,10 +175,12 @@ export default class DeactivateAccountDialog extends React.Component {
</p>
<p>{ _t("To continue, please enter your password:") }</p>
<input
<Field
id="mx_DeactivateAccountDialog_password"
type="password"
placeholder={_t("password")}
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
ref={(e) => {this._passwordField = e;}}
className={passwordBoxClass}
/>

View File

@@ -2,6 +2,7 @@
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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,10 @@ import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import DMRoomMap from '../../../utils/DMRoomMap';
import createRoom from "../../../createRoom";
import dis from "../../../dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
@@ -85,25 +90,37 @@ export default class DeviceVerifyDialog extends React.Component {
this.props.onFinished(confirm);
}
_onSasRequestClick = () => {
_onSasRequestClick = async () => {
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
});
this._verifier = MatrixClientPeg.get().beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
this._verifier.on('show_sas', this._onVerifierShowSas);
this._verifier.verify().then(() => {
const client = MatrixClientPeg.get();
const verifyingOwnDevice = this.props.userId === client.getUserId();
try {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
this._verifier = await client.requestVerificationDM(
this.props.userId, roomId, [verificationMethods.SAS],
);
} else {
this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
}
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
}).catch((e) => {
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
});
}
}
_onSasMatchesClick = () => {
@@ -241,6 +258,16 @@ export default class DeviceVerifyDialog extends React.Component {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("To verify that this device can be trusted, please check that the key you see " +
"in User Settings on that device matches the key below:");
} else {
text = _t("To verify that this device can be trusted, please contact its owner using some other " +
"means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " +
"for this device matches the key below:");
}
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
const body = (
<div>
@@ -250,10 +277,7 @@ export default class DeviceVerifyDialog extends React.Component {
{_t("Use two-way text verification")}
</AccessibleButton>
<p>
{ _t("To verify that this device can be trusted, please contact its " +
"owner using some other means (e.g. in person or a phone call) " +
"and ask them whether the key they see in their User Settings " +
"for this device matches the key below:") }
{ text }
</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
@@ -291,3 +315,30 @@ export default class DeviceVerifyDialog extends React.Component {
}
}
async function ensureDMExistsAndOpen(userId) {
const client = MatrixClientPeg.get();
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
if (r && r.getMyMembership() === "join") {
const member = r.getMember(userId);
return member && (member.membership === "invite" || member.membership === "join");
}
return false;
});
let roomId;
if (suitableDMRooms.length) {
const room = suitableDMRooms[0];
roomId = room.roomId;
} else {
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
}
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({
action: 'view_room',
room_id: roomId,
should_peek: false,
});
return roomId;
}

View File

@@ -128,8 +128,10 @@ class SendCustomEvent extends GenericEditor {
return <div>
<div className="mx_DevTools_content">
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
<div className="mx_DevTools_eventTypeStateKeyGroup">
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
</div>
<br />

View File

@@ -26,11 +26,12 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
displayName: 'ErrorDialog',
propTypes: {
title: PropTypes.string,

View File

@@ -34,9 +34,15 @@ export default class IncomingSasDialog extends React.Component {
constructor(props) {
super(props);
let phase = PHASE_START;
if (this.props.verifier.cancelled) {
console.log("Verifier was cancelled in the background.");
phase = PHASE_CANCELLED;
}
this._showSasEvent = null;
this.state = {
phase: PHASE_START,
phase: phase,
sasVerified: false,
opponentProfile: null,
opponentProfileError: null,

View File

@@ -17,23 +17,28 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classNames from "classnames";
export default React.createClass({
export default createReactClass({
displayName: 'InfoDialog',
propTypes: {
className: PropTypes.string,
title: PropTypes.string,
description: PropTypes.node,
button: PropTypes.string,
onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool,
},
getDefaultProps: function() {
return {
title: '',
description: '',
hasCloseButton: false,
};
},
@@ -48,9 +53,9 @@ export default React.createClass({
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
title={this.props.title}
contentId='mx_Dialog_content'
hasCancel={false}
hasCancel={this.props.hasCloseButton}
>
<div className="mx_Dialog_content" id="mx_Dialog_content">
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
{ this.props.description }
</div>
<DialogButtons primaryButton={this.props.button || _t('OK')}

View File

@@ -0,0 +1,57 @@
/*
Copyright 2019 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 {_t} from "../../../languageHandler";
import sdk from "../../../index";
import dis from '../../../dispatcher';
export default class IntegrationsDisabledDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished();
};
_onOpenSettingsClick = () => {
this.props.onFinished();
dis.dispatch({action: "view_user_settings"});
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_IntegrationsDisabledDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Integrations are disabled")}>
<div className='mx_IntegrationsDisabledDialog_content'>
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
</div>
<DialogButtons
primaryButton={_t("Settings")}
onPrimaryButtonClick={this._onOpenSettingsClick}
cancelButton={_t("OK")}
onCancel={this._onAcknowledgeClick}
/>
</BaseDialog>
);
}
}

View File

@@ -0,0 +1,55 @@
/*
Copyright 2019 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 {_t} from "../../../languageHandler";
import sdk from "../../../index";
export default class IntegrationsImpossibleDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished();
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_IntegrationsImpossibleDialog' hasCancel={false}
onFinished={this.props.onFinished}
title={_t("Integrations not allowed")}>
<div className='mx_IntegrationsImpossibleDialog_content'>
<p>
{_t(
"Your Riot doesn't allow you to use an Integration Manager to do this. " +
"Please contact an admin.",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={this._onAcknowledgeClick}
hasCancel={false}
/>
</BaseDialog>
);
}
}

View File

@@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
@@ -23,7 +24,7 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
export default React.createClass({
export default createReactClass({
displayName: 'InteractiveAuthDialog',
propTypes: {

View File

@@ -16,6 +16,7 @@ limitations under the License.
import Modal from '../../../Modal';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
@@ -29,7 +30,7 @@ import { _t, _td } from '../../../languageHandler';
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default React.createClass({
export default createReactClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
@@ -77,7 +78,7 @@ export default React.createClass({
true,
);
}
}).done();
});
},
componentWillUnmount: function() {

View File

@@ -20,11 +20,12 @@ import 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 = {
onFinished: function() {},
}
};
constructor() {
super();
@@ -34,9 +35,11 @@ export default class LogoutDialog extends React.Component {
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
const shouldLoadBackupStatus = !MatrixClientPeg.get().getKeyBackupEnabled();
const lowBandwidth = SettingsStore.getValue("lowBandwidth");
const shouldLoadBackupStatus = !lowBandwidth && !MatrixClientPeg.get().getKeyBackupEnabled();
this.state = {
shouldLoadBackupStatus: shouldLoadBackupStatus,
loading: shouldLoadBackupStatus,
backupInfo: null,
error: null,
@@ -110,17 +113,17 @@ export default class LogoutDialog extends React.Component {
}
render() {
const description = <div>
<p>{_t(
"Encrypted messages are secured with end-to-end encryption. " +
"Only you and the recipient(s) have the keys to read these messages.",
)}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>;
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
if (this.state.shouldLoadBackupStatus) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const description = <div>
<p>{_t(
"Encrypted messages are secured with end-to-end encryption. " +
"Only you and the recipient(s) have the keys to read these messages.",
)}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>;
let dialogContent;
if (this.state.loading) {
const Spinner = sdk.getComponent('views.elements.Spinner');

View File

@@ -0,0 +1,171 @@
/*
Copyright 2019 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 { _t } from '../../../languageHandler';
import sdk from "../../../index";
import {wantsDateSeparator} from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore';
export default class MessageEditHistoryDialog extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
originalEvent: null,
error: null,
events: [],
nextBatch: null,
isLoading: true,
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
};
}
loadMoreEdits = async (backwards) => {
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
// bail out on backwards as we only paginate in one direction
return false;
}
const opts = {from: this.state.nextBatch};
const roomId = this.props.mxEvent.getRoomId();
const eventId = this.props.mxEvent.getId();
const client = MatrixClientPeg.get();
let result;
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;});
try {
result = await client.relations(
roomId, eventId, "m.replace", "m.room.message", opts);
} catch (error) {
// log if the server returned an error
if (error.errcode) {
console.error("fetching /relations failed with error", error);
}
this.setState({error}, () => reject(error));
return promise;
}
const newEvents = result.events;
this._locallyRedactEventsIfNeeded(newEvents);
this.setState({
originalEvent: this.state.originalEvent || result.originalEvent,
events: this.state.events.concat(newEvents),
nextBatch: result.nextBatch,
isLoading: false,
}, () => {
const hasMoreResults = !!this.state.nextBatch;
resolve(hasMoreResults);
});
return promise;
}
_locallyRedactEventsIfNeeded(newEvents) {
const roomId = this.props.mxEvent.getRoomId();
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
const pendingEvents = room.getPendingEvents();
for (const e of newEvents) {
const pendingRedaction = pendingEvents.find(pe => {
return pe.getType() === "m.room.redaction" && pe.getAssociatedId() === e.getId();
});
if (pendingRedaction) {
e.markLocallyRedacted(pendingRedaction);
}
}
}
componentDidMount() {
this.loadMoreEdits();
}
_renderEdits() {
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const nodes = [];
let lastEvent;
let allEvents = this.state.events;
// append original event when we've done last pagination
if (this.state.originalEvent && !this.state.nextBatch) {
allEvents = allEvents.concat(this.state.originalEvent);
}
const baseEventId = this.props.mxEvent.getId();
allEvents.forEach((e, i) => {
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
}
const isBaseEvent = e.getId() === baseEventId;
nodes.push((
<EditHistoryMessage
key={e.getId()}
previousEdit={!isBaseEvent ? allEvents[i + 1] : null}
isBaseEvent={isBaseEvent}
mxEvent={e}
isTwelveHour={this.state.isTwelveHour}
/>));
lastEvent = e;
});
return nodes;
}
render() {
let content;
if (this.state.error) {
const {error} = this.state;
if (error.errcode === "M_UNRECOGNIZED") {
content = (<p className="mx_MessageEditHistoryDialog_error">
{_t("Your homeserver doesn't seem to support this feature.")}
</p>);
} else if (error.errcode) {
// some kind of error from the homeserver
content = (<p className="mx_MessageEditHistoryDialog_error">
{_t("Something went wrong!")}
</p>);
} else {
content = (<p className="mx_MessageEditHistoryDialog_error">
{_t("Cannot reach homeserver")}
<br />
{_t("Ensure you have a stable internet connection, or get in touch with the server admin")}
</p>);
}
} else if (this.state.isLoading) {
const Spinner = sdk.getComponent("elements.Spinner");
content = <Spinner />;
} else {
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
content = (<ScrollPanel
className="mx_MessageEditHistoryDialog_scrollPanel"
onFillRequest={ this.loadMoreEdits }
stickyBottom={false}
startAtBottom={false}
>
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
</ScrollPanel>);
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true}
onFinished={this.props.onFinished} title={_t("Message edits")}>
{content}
</BaseDialog>
);
}
}

View File

@@ -16,11 +16,12 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
displayName: 'QuestionDialog',
propTypes: {
title: PropTypes.string,

View File

@@ -0,0 +1,137 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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, {PureComponent} from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import PropTypes from "prop-types";
import {MatrixEvent} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
/*
* A dialog for reporting an event.
*/
export default class ReportEventDialog extends PureComponent {
static propTypes = {
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
this.state = {
reason: "",
busy: false,
err: null,
};
}
_onReasonChange = ({target: {value: reason}}) => {
this.setState({ reason });
};
_onCancel = () => {
this.props.onFinished(false);
};
_onSubmit = async () => {
if (!this.state.reason || !this.state.reason.trim()) {
this.setState({
err: _t("Please fill why you're reporting."),
});
return;
}
this.setState({
busy: true,
err: null,
});
try {
const ev = this.props.mxEvent;
await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
this.props.onFinished(true);
} catch (e) {
this.setState({
busy: false,
err: e.message,
});
}
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Loader = sdk.getComponent('elements.Spinner');
const Field = sdk.getComponent('elements.Field');
let error = null;
if (this.state.err) {
error = <div className="error">
{this.state.err}
</div>;
}
let progress = null;
if (this.state.busy) {
progress = (
<div className="progress">
<Loader />
</div>
);
}
return (
<BaseDialog
className="mx_BugReportDialog"
onFinished={this.props.onFinished}
title={_t('Report Content to Your Homeserver Administrator')}
contentId='mx_ReportEventDialog'
>
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
<p>
{
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
"your homeserver. If messages in this room are encrypted, your homeserver " +
"administrator will not be able to read the message text or view any files or images.")
}
</p>
<Field
id="mx_ReportEventDialog_reason"
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
rows={5}
onChange={this._onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
</div>
<DialogButtons
primaryButton={_t("Send report")}
onPrimaryButtonClick={this._onSubmit}
focus={true}
onCancel={this._onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View File

@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,8 +23,10 @@ import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsT
import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import dis from "../../../dispatcher";
export default class RoomSettingsDialog extends React.Component {
static propTypes = {
@@ -31,6 +34,22 @@ export default class RoomSettingsDialog extends React.Component {
onFinished: PropTypes.func.isRequired,
};
componentWillMount() {
this._dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
_onAction = (payload) => {
// When room changes below us, close the room settings
// whilst the modal is open this can only be triggered when someone hits Leave Room
if (payload.action === 'view_next_room') {
this.props.onFinished();
}
};
_getTabs() {
const tabs = [];
@@ -49,6 +68,11 @@ export default class RoomSettingsDialog extends React.Component {
"mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
_td("Notifications"),
"mx_RoomSettingsDialog_rolesIcon",
<NotificationSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",

View File

@@ -15,13 +15,14 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
displayName: 'RoomUpgradeDialog',
propTypes: {
@@ -92,7 +93,7 @@ export default React.createClass({
<p>
{_t(
"Upgrading this room requires closing down the current " +
"instance of the room and creating a new room it its place. " +
"instance of the room and creating a new room in its place. " +
"To give room members the best possible experience, we will:",
)}
</p>

View File

@@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
@@ -23,7 +24,7 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
displayName: 'SessionRestoreErrorDialog',
propTypes: {

View File

@@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Email from '../../../email';
@@ -29,7 +30,7 @@ import Modal from '../../../Modal';
*
* On success, `onFinished(true)` is called.
*/
export default React.createClass({
export default createReactClass({
displayName: 'SetEmailDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
@@ -61,9 +62,7 @@ export default React.createClass({
return;
}
this._addThreepid = new AddThreepid();
// we always bind emails when registering, so let's do the
// same here.
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
this._addThreepid.addEmailAddress(emailAddress).then(() => {
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
title: _t("Verification Pending"),
description: _t(
@@ -97,7 +96,7 @@ export default React.createClass({
},
verifyEmailAddress: function() {
this._addThreepid.checkEmailLinkClicked().done(() => {
this._addThreepid.checkEmailLinkClicked().then(() => {
this.props.onFinished(true);
}, (err) => {
this.setState({emailBusy: false});

View File

@@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
@@ -34,7 +34,7 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
export default createReactClass({
displayName: 'SetMxIdDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,

View File

@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,6 +17,8 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
@@ -60,10 +63,10 @@ const WarmFuzzy = function(props) {
*
* On success, `onFinished()` when finished
*/
export default React.createClass({
export default createReactClass({
displayName: 'SetPasswordDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {

View File

@@ -20,7 +20,7 @@ import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react';
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../matrix-to";
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextualMenu from "../../structures/ContextualMenu";
const socials = [
@@ -114,7 +114,8 @@ export default class ShareDialog extends React.Component {
top: y,
message: successful ? _t('Copied!') : _t('Failed to copy'),
}, false);
e.target.onmouseleave = close;
// Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close;
}
onLinkSpecificEventCheckboxClick() {
@@ -131,6 +132,12 @@ export default class ShareDialog extends React.Component {
}
}
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;
let matrixToUrl;
@@ -146,7 +153,7 @@ export default class ShareDialog extends React.Component {
<input type="checkbox"
id="mx_ShareDialog_checkbox"
checked={this.state.linkSpecificEvent}
onClick={this.onLinkSpecificEventCheckboxClick} />
onChange={this.onLinkSpecificEventCheckboxClick} />
<label htmlFor="mx_ShareDialog_checkbox">
{ _t('Link to most recent message') }
</label>

View File

@@ -0,0 +1,63 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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 {_t} from "../../../languageHandler";
import {CommandCategories, CommandMap} from "../../../SlashCommands";
import sdk from "../../../index";
export default ({onFinished}) => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
const categories = {};
Object.values(CommandMap).forEach(cmd => {
if (!categories[cmd.category]) {
categories[cmd.category] = [];
}
categories[cmd.category].push(cmd);
});
const body = Object.values(CommandCategories).filter(c => categories[c]).map((category) => {
const rows = [
<tr key={"_category_" + category} className="mx_SlashCommandHelpDialog_headerRow">
<td colSpan={3}>
<h2>{_t(category)}</h2>
</td>
</tr>,
];
categories[category].forEach(cmd => {
rows.push(<tr key={cmd.command}>
<td><strong>{cmd.command}</strong></td>
<td>{cmd.args}</td>
<td>{cmd.description}</td>
</tr>);
});
return rows;
});
return <InfoDialog
className="mx_SlashCommandHelpDialog"
title={_t("Command Help")}
description={<table>
<tbody>
{body}
</tbody>
</table>}
hasCloseButton={true}
onFinished={onFinished} />;
};

View File

@@ -0,0 +1,172 @@
/*
Copyright 2019 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 {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {Room} from "matrix-js-sdk";
import sdk from '../../../index';
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
import classNames from 'classnames';
import ScalarMessaging from "../../../ScalarMessaging";
export default class TabbedIntegrationManagerDialog extends React.Component {
static propTypes = {
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
/**
* Optional room where the integration manager should be open to
*/
room: PropTypes.instanceOf(Room),
/**
* Optional screen to open on the integration manager
*/
screen: PropTypes.string,
/**
* Optional integration ID to open in the integration manager
*/
integrationId: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
managers: IntegrationManagers.sharedInstance().getOrderedManagers(),
busy: true,
currentIndex: 0,
currentConnected: false,
currentLoading: true,
currentScalarClient: null,
};
}
componentDidMount(): void {
this.openManager(0, true);
}
openManager = async (i: number, force = false) => {
if (i === this.state.currentIndex && !force) return;
const manager = this.state.managers[i];
const client = manager.getScalarClient();
this.setState({
busy: true,
currentIndex: i,
currentLoading: true,
currentConnected: false,
currentScalarClient: client,
});
ScalarMessaging.setOpenManagerUrl(manager.uiUrl);
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
// To avoid visual glitching of two modals stacking briefly, we customise the
// terms dialog sizing when it will appear for the integration manager so that
// it gets the same basic size as the IM's own modal.
return dialogTermsInteractionCallback(
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager',
);
});
try {
await client.connect();
if (!client.hasCredentials()) {
this.setState({
busy: false,
currentLoading: false,
currentConnected: false,
});
} else {
this.setState({
busy: false,
currentLoading: false,
currentConnected: true,
});
}
} catch (e) {
if (e instanceof TermsNotSignedError) {
return;
}
console.error(e);
this.setState({
busy: false,
currentLoading: false,
currentConnected: false,
});
}
};
_renderTabs() {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
return this.state.managers.map((m, i) => {
const classes = classNames({
'mx_TabbedIntegrationManagerDialog_tab': true,
'mx_TabbedIntegrationManagerDialog_currentTab': this.state.currentIndex === i,
});
return (
<AccessibleButton
className={classes}
onClick={() => this.openManager(i)}
key={`tab_${i}`}
disabled={this.state.busy}
>
{m.name}
</AccessibleButton>
);
});
}
_renderTab() {
const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
let uiUrl = null;
if (this.state.currentScalarClient) {
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
this.props.room,
this.props.screen,
this.props.integrationId,
);
}
return <IntegrationManager
configured={true}
loading={this.state.currentLoading}
connected={this.state.currentConnected}
url={uiUrl}
onFinished={() => {/* no-op */}}
/>;
}
render() {
return (
<div className='mx_TabbedIntegrationManagerDialog_container'>
<div className='mx_TabbedIntegrationManagerDialog_tabs'>
{this._renderTabs()}
</div>
<div className='mx_TabbedIntegrationManagerDialog_currentManager'>
{this._renderTab()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,205 @@
/*
Copyright 2019 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 url from 'url';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t, pickBestLanguage } from '../../../languageHandler';
import Matrix from 'matrix-js-sdk';
class TermsCheckbox extends React.PureComponent {
static propTypes = {
onChange: PropTypes.func.isRequired,
url: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
}
onChange = (ev) => {
this.props.onChange(this.props.url, ev.target.checked);
}
render() {
return <input type="checkbox"
onChange={this.onChange}
checked={this.props.checked}
/>;
}
}
export default class TermsDialog extends React.PureComponent {
static propTypes = {
/**
* Array of [Service, policies] pairs, where policies is the response from the
* /terms endpoint for that service
*/
policiesAndServicePairs: PropTypes.array.isRequired,
/**
* urls that the user has already agreed to
*/
agreedUrls: PropTypes.arrayOf(PropTypes.string),
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
}
constructor(props) {
super();
this.state = {
// url -> boolean
agreedUrls: {},
};
for (const url of props.agreedUrls) {
this.state.agreedUrls[url] = true;
}
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onNextClick = () => {
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
}
_nameForServiceType(serviceType, host) {
switch (serviceType) {
case Matrix.SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>;
case Matrix.SERVICE_TYPES.IM:
return <div>{_t("Integration Manager")}<br />({host})</div>;
}
}
_summaryForServiceType(serviceType) {
switch (serviceType) {
case Matrix.SERVICE_TYPES.IS:
return <div>
{_t("Find others by phone or email")}
<br />
{_t("Be found by phone or email")}
</div>;
case Matrix.SERVICE_TYPES.IM:
return <div>
{_t("Use bots, bridges, widgets and sticker packs")}
</div>;
}
}
_onTermsCheckboxChange = (url, checked) => {
this.setState({
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
});
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const rows = [];
for (const policiesAndService of this.props.policiesAndServicePairs) {
const parsedBaseUrl = url.parse(policiesAndService.service.baseUrl);
const policyValues = Object.values(policiesAndService.policies);
for (let i = 0; i < policyValues.length; ++i) {
const termDoc = policyValues[i];
const termsLang = pickBestLanguage(Object.keys(termDoc).filter((k) => k !== 'version'));
let serviceName;
let summary;
if (i === 0) {
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
summary = this._summaryForServiceType(
policiesAndService.service.serviceType,
);
}
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}>
<span className="mx_TermsDialog_link" />
</a></td>
<td><TermsCheckbox
url={termDoc[termsLang].url}
onChange={this._onTermsCheckboxChange}
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
/></td>
</tr>);
}
}
// if all the documents for at least one service have been checked, we can enable
// the submit button
let enableSubmit = false;
for (const policiesAndService of this.props.policiesAndServicePairs) {
let docsAgreedForService = 0;
for (const terms of Object.values(policiesAndService.policies)) {
let docAgreed = false;
for (const lang of Object.keys(terms)) {
if (lang === 'version') continue;
if (this.state.agreedUrls[terms[lang].url]) {
docAgreed = true;
break;
}
}
if (docAgreed) {
++docsAgreedForService;
}
}
if (docsAgreedForService === Object.keys(policiesAndService.policies).length) {
enableSubmit = true;
break;
}
}
return (
<BaseDialog
fixedWidth={false}
onFinished={this._onCancelClick}
title={_t("Terms of Service")}
contentId='mx_Dialog_content'
hasCancel={false}
>
<div id='mx_Dialog_content'>
<p>{_t("To continue you need to accept the terms of this service.")}</p>
<table className="mx_TermsDialog_termsTable"><tbody>
<tr className="mx_TermsDialog_termsTableHeader">
<th>{_t("Service")}</th>
<th>{_t("Summary")}</th>
<th>{_t("Document")}</th>
<th>{_t("Accept")}</th>
</tr>
{rows}
</tbody></table>
</div>
<DialogButtons primaryButton={_t('Next')}
hasCancel={true}
onCancel={this._onCancelClick}
onPrimaryButtonClick={this._onNextClick}
primaryDisabled={!enableSubmit}
/>
</BaseDialog>
);
}
}

View File

@@ -15,10 +15,11 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
export default React.createClass({
export default createReactClass({
displayName: 'TextInputDialog',
propTypes: {
title: PropTypes.string,

View File

@@ -16,11 +16,10 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { markAllDevicesKnown } from '../../../cryptodevices';
@@ -67,7 +66,7 @@ UnknownDeviceList.propTypes = {
};
export default React.createClass({
export default createReactClass({
displayName: 'UnknownDeviceDialog',
propTypes: {

View File

@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,6 +19,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import filesize from "filesize";
export default class UploadConfirmDialog extends React.Component {
static propTypes = {
@@ -49,6 +51,10 @@ export default class UploadConfirmDialog extends React.Component {
this.props.onFinished(true);
}
_onUploadAllClick = () => {
this.props.onFinished(true, true);
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@@ -71,7 +77,7 @@ export default class UploadConfirmDialog extends React.Component {
preview = <div className="mx_UploadConfirmDialog_previewOuter">
<div className="mx_UploadConfirmDialog_previewInner">
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
<div>{this.props.file.name}</div>
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
</div>
</div>;
} else {
@@ -80,11 +86,18 @@ export default class UploadConfirmDialog extends React.Component {
<img className="mx_UploadConfirmDialog_fileIcon"
src={require("../../../../res/img/files.png")}
/>
{this.props.file.name}
{this.props.file.name} ({filesize(this.props.file.size)})
</div>
</div>;
}
let uploadAllButton;
if (this.props.currentIndex + 1 < this.props.totalFiles) {
uploadAllButton = <button onClick={this._onUploadAllClick}>
{_t("Upload all")}
</button>;
}
return (
<BaseDialog className='mx_UploadConfirmDialog'
fixedWidth={false}
@@ -100,7 +113,9 @@ export default class UploadConfirmDialog extends React.Component {
hasCancel={false}
onPrimaryButtonClick={this._onUploadClick}
focus={true}
/>
>
{uploadAllButton}
</DialogButtons>
</BaseDialog>
);
}

View File

@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 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.
@@ -28,19 +29,42 @@ import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"),
};
}
componentDidMount(): void {
this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
}
componentWillUnmount(): void {
SettingsStore.unwatchSetting(this._mjolnirWatcher);
}
_mjolnirChanged(settingName, roomId, atLevel, newValue) {
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
this.setState({mjolnirEnabled: newValue});
}
_getTabs() {
const tabs = [];
tabs.push(new Tab(
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab />,
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
tabs.push(new Tab(
_td("Flair"),
@@ -67,13 +91,20 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab />,
));
if (SettingsStore.getLabsFeatures().length > 0) {
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
<LabsUserSettingsTab />,
));
}
if (this.state.mjolnirEnabled) {
tabs.push(new Tab(
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />,
));
}
tabs.push(new Tab(
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import Modal from '../../../../Modal';
@@ -22,6 +23,7 @@ import Modal from '../../../../Modal';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../../languageHandler';
import {Key} from "../../../../Keyboard";
const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1;
@@ -29,7 +31,7 @@ const RESTORE_TYPE_RECOVERYKEY = 1;
/**
* Dialog for restoring e2e keys from a backup and the user's recovery key
*/
export default React.createClass({
export default createReactClass({
getInitialState: function() {
return {
backupInfo: null,
@@ -135,13 +137,13 @@ export default React.createClass({
},
_onPassPhraseKeyPress: function(e) {
if (e.key === "Enter") {
if (e.key === Key.ENTER) {
this._onPassPhraseNext();
}
},
_onRecoveryKeyKeyPress: function(e) {
if (e.key === "Enter" && this.state.recoveryKeyValid) {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
},

View File

@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
@@ -37,7 +39,7 @@ export default class NetworkDropdown extends React.Component {
this.inputTextBox = null;
const server = MatrixClientPeg.getHomeServerName();
const server = MatrixClientPeg.getHomeserverName();
this.state = {
expanded: false,
selectedServer: server,
@@ -138,8 +140,8 @@ export default class NetworkDropdown extends React.Component {
servers = servers.concat(roomDirectory.servers);
}
if (!servers.includes(MatrixClientPeg.getHomeServerName())) {
servers.unshift(MatrixClientPeg.getHomeServerName());
if (!servers.includes(MatrixClientPeg.getHomeserverName())) {
servers.unshift(MatrixClientPeg.getHomeserverName());
}
// For our own HS, we can use the instance_ids given in the third party protocols
@@ -148,7 +150,7 @@ export default class NetworkDropdown extends React.Component {
// we can only show the default room list.
for (const server of servers) {
options.push(this._makeMenuOption(server, null, true));
if (server === MatrixClientPeg.getHomeServerName()) {
if (server === MatrixClientPeg.getHomeserverName()) {
options.push(this._makeMenuOption(server, null, false));
if (this.props.protocols) {
for (const proto of Object.keys(this.props.protocols)) {
@@ -241,10 +243,10 @@ export default class NetworkDropdown extends React.Component {
}
NetworkDropdown.propTypes = {
onOptionChange: React.PropTypes.func.isRequired,
protocols: React.PropTypes.object,
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: React.PropTypes.object,
config: PropTypes.object,
};
NetworkDropdown.defaultProps = {

View File

@@ -63,10 +63,11 @@ export default function AccessibleButton(props) {
};
}
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";
restProps.className = (restProps.className ? restProps.className + " " : "") +
"mx_AccessibleButton";
// Pass through the ref - used for keyboard shortcut access to some buttons
restProps.ref = restProps.inputRef;
delete restProps.inputRef;
restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton";
if (kind) {
// We apply a hasKind class to maintain backwards compatibility with
@@ -76,6 +77,7 @@ export default function AccessibleButton(props) {
if (disabled) {
restProps.className += " mx_AccessibleButton_disabled";
restProps["aria-disabled"] = true;
}
return React.createElement(element, restProps, children);
@@ -89,18 +91,30 @@ export default function AccessibleButton(props) {
*/
AccessibleButton.propTypes = {
children: PropTypes.node,
inputRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
element: PropTypes.string,
onClick: PropTypes.func.isRequired,
// The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options.
kind: PropTypes.string,
// The ARIA role
role: PropTypes.string,
// The tabIndex
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.bool,
};
AccessibleButton.defaultProps = {
element: 'div',
role: 'button',
tabIndex: "0",
};
AccessibleButton.displayName = "AccessibleButton";

View File

@@ -0,0 +1,64 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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 AccessibleButton from "./AccessibleButton";
import sdk from "../../../index";
export default class AccessibleTooltipButton extends React.PureComponent {
static propTypes = {
...AccessibleButton.propTypes,
// The tooltip to render on hover
title: PropTypes.string.isRequired,
};
state = {
hover: false,
};
onMouseOver = () => {
this.setState({
hover: true,
});
};
onMouseOut = () => {
this.setState({
hover: false,
});
};
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {title, ...props} = this.props;
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName="mx_AccessibleTooltipButton_tooltip"
label={title}
/> : <div />;
return (
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
{ tip }
</AccessibleButton>
);
}
}

View File

@@ -16,12 +16,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import Analytics from '../../../Analytics';
export default React.createClass({
export default createReactClass({
displayName: 'RoleButton',
propTypes: {

View File

@@ -15,15 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress';
export default React.createClass({
export default createReactClass({
displayName: 'AddressSelector',
propTypes: {

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
@@ -24,7 +25,7 @@ import { _t } from '../../../languageHandler';
import { UserAddressType } from '../../../UserAddress.js';
export default React.createClass({
export default createReactClass({
displayName: 'AddressTile',
propTypes: {

View File

@@ -1,76 +1,144 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 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 url from 'url';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import WidgetUtils from "../../../utils/WidgetUtils";
import MatrixClientPeg from "../../../MatrixClientPeg";
export default class AppPermission extends React.Component {
static propTypes = {
url: PropTypes.string.isRequired,
creatorUserId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
isRoomEncrypted: PropTypes.bool,
};
static defaultProps = {
onPermissionGranted: () => {},
};
constructor(props) {
super(props);
const curlBase = this.getCurlBase();
this.state = { curlBase: curlBase};
// The first step is to pick apart the widget so we can render information about it
const urlInfo = this.parseWidgetUrl();
// The second step is to find the user's profile so we can show it on the prompt
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
let roomMember;
if (room) roomMember = room.getMember(this.props.creatorUserId);
// Set all this into the initial state
this.state = {
...urlInfo,
roomMember,
};
}
// Return string representation of content URL without query parameters
getCurlBase() {
const wurl = url.parse(this.props.url);
let curl;
let curlString;
parseWidgetUrl() {
const widgetUrl = url.parse(this.props.url);
const params = new URLSearchParams(widgetUrl.search);
const searchParams = new URLSearchParams(wurl.search);
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
curl = url.parse(searchParams.get('url'));
if (curl) {
curl.search = curl.query = "";
curlString = curl.format();
}
// HACK: We're relying on the query params when we should be relying on the widget's `data`.
// This is a workaround for Scalar.
if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) {
const unwrappedUrl = url.parse(params.get('url'));
return {
widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname,
isWrapped: true,
};
} else {
return {
widgetDomain: widgetUrl.host || widgetUrl.hostname,
isWrapped: false,
};
}
if (!curl && wurl) {
wurl.search = wurl.query = "";
curlString = wurl.format();
}
return curlString;
}
render() {
let e2eWarningText;
if (this.props.isRoomEncrypted) {
e2eWarningText =
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
}
const cookieWarning =
<span className='mx_AppPermissionWarningTextLabel'>
{ _t('Warning: This widget might use cookies.') }
</span>;
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip");
const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId;
const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId;
const avatar = this.state.roomMember
? <MemberAvatar member={this.state.roomMember} width={38} height={38} />
: <BaseAvatar name={this.props.creatorUserId} width={38} height={38} />;
const warningTooltipText = (
<div>
{_t("Any of the following data may be shared:")}
<ul>
<li>{_t("Your display name")}</li>
<li>{_t("Your avatar URL")}</li>
<li>{_t("Your user ID")}</li>
<li>{_t("Your theme")}</li>
<li>{_t("Riot URL")}</li>
<li>{_t("Room ID")}</li>
<li>{_t("Widget ID")}</li>
</ul>
</div>
);
const warningTooltip = (
<TextWithTooltip tooltip={warningTooltipText} tooltipClass='mx_AppPermissionWarning_tooltip mx_Tooltip_dark'>
<span className='mx_AppPermissionWarning_helpIcon' />
</TextWithTooltip>
);
// Due to i18n limitations, we can't dedupe the code for variables in these two messages.
const warning = this.state.isWrapped
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip})
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip});
const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null;
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/feather-customised/warning-triangle.svg")} alt={_t('Warning!')} />
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
{_t("Widget added by")}
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
{ e2eWarningText }
{ cookieWarning }
<div className='mx_AppPermissionWarning_row'>
{avatar}
<h4 className='mx_AppPermissionWarning_bolder'>{displayName}</h4>
<div className='mx_AppPermissionWarning_smallText'>{userId}</div>
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{warning}
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{_t("This widget may use cookies.")}&nbsp;{encryptionWarning}
</div>
<div className='mx_AppPermissionWarning_row'>
<AccessibleButton kind='primary_sm' onClick={this.props.onPermissionGranted}>
{_t("Continue")}
</AccessibleButton>
</div>
<input
className='mx_AppPermissionButton'
type='button'
value={_t('Allow')}
onClick={this.props.onPermissionGranted}
/>
</div>
);
}
}
AppPermission.propTypes = {
isRoomEncrypted: PropTypes.bool,
url: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
};
AppPermission.defaultProps = {
isRoomEncrypted: false,
onPermissionGranted: function() {},
};

View File

@@ -1,6 +1,7 @@
/**
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,14 +16,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import url from 'url';
import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import AccessibleButton from './AccessibleButton';
import Modal from '../../../Modal';
@@ -35,6 +33,9 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {createMenu} from "../../structures/ContextualMenu";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@@ -52,7 +53,7 @@ export default class AppTile extends React.Component {
this._onLoaded = this._onLoaded.bind(this);
this._onEditClick = this._onEditClick.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
this._onCancelClick = this._onCancelClick.bind(this);
this._onRevokeClicked = this._onRevokeClicked.bind(this);
this._onSnapshotClick = this._onSnapshotClick.bind(this);
this.onClickMenuBar = this.onClickMenuBar.bind(this);
this._onMinimiseClick = this._onMinimiseClick.bind(this);
@@ -69,8 +70,11 @@ export default class AppTile extends React.Component {
* @return {Object} Updated component state to be set with setState
*/
_getNewState(newProps) {
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
// 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];
};
const PersistedElement = sdk.getComponent("elements.PersistedElement");
return {
@@ -78,10 +82,9 @@ export default class AppTile extends React.Component {
// True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
widgetUrl: this._addWurlParams(newProps.url),
widgetPermissionId: widgetPermissionId,
// 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: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
error: null,
deleting: false,
widgetPageTitle: newProps.widgetPageTitle,
@@ -139,7 +142,10 @@ export default class AppTile extends React.Component {
}
componentWillMount() {
this.setScalarToken();
// 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() {
@@ -153,7 +159,7 @@ export default class AppTile extends React.Component {
// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
ActiveWidgetStore.destroyPersistentWidget();
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
@@ -164,8 +170,6 @@ export default class AppTile extends React.Component {
* Component initialisation is only complete when this function has resolved
*/
setScalarToken() {
this.setState({initialising: true});
if (!WidgetUtils.isScalarUrl(this.props.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({
@@ -176,18 +180,42 @@ export default class AppTile extends React.Component {
return;
}
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
console.warn("No integration manager - not setting scalar token", url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
initialising: false,
});
return;
}
// TODO: Pick the right manager for the widget
const defaultManager = managers.getPrimaryManager();
if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
console.warn('Non-scalar manager, not setting scalar token!', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
initialising: false,
});
return;
}
// Fetch the token before loading the iframe as we need it to mangle the URL
if (!this._scalarClient) {
this._scalarClient = new ScalarAuthClient();
this._scalarClient = defaultManager.getScalarClient();
}
this._scalarClient.getScalarToken().done((token) => {
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 params = qs.parse(u.query);
if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token);
// u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
// u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
u.search = undefined;
u.query = params;
}
@@ -214,11 +242,20 @@ export default class AppTile extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.url !== this.props.url) {
this._getNewState(nextProps);
this.setScalarToken();
} else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) {
this.setState({
loading: true,
});
// 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 (this.props.waitForIframeLoad) {
this.setState({
loading: true,
});
}
// Fetch IM token now that we're showing if we already have permission to load
if (this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
this.setState({
widgetPageTitle: nextProps.widgetPageTitle,
@@ -235,28 +272,29 @@ export default class AppTile extends React.Component {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
}
_onEditClick(e) {
_onEditClick() {
console.log("Edit widget ID ", this.props.id);
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
this._scalarClient.connect().done(() => {
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
this.props.room, 'type_' + this.props.type, this.props.id);
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
}, (err) => {
this.setState({
error: err.message,
});
console.error('Error ensuring a valid scalar_token exists', err);
});
// TODO: Open the right manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
'type_' + this.props.type,
this.props.id,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
'type_' + this.props.type,
this.props.id,
);
}
}
}
_onSnapshotClick(e) {
_onSnapshotClick() {
console.warn("Requesting widget snapshot");
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
.catch((err) => {
@@ -323,13 +361,9 @@ export default class AppTile extends React.Component {
}
}
_onCancelClick() {
if (this.props.onDeleteClick) {
this.props.onDeleteClick();
} else {
console.log("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
}
_onRevokeClicked() {
console.info("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
}
/**
@@ -411,22 +445,38 @@ export default class AppTile extends React.Component {
});
}
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
_grantWidgetPermission() {
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
localStorage.setItem(this.state.widgetPermissionId, true);
this.setState({hasPermissionToLoad: true});
const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = true;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: true});
// Fetch a token for the integration manager, now that we're allowed to
this.setScalarToken();
}).catch(err => {
console.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
}
_revokeWidgetPermission() {
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
localStorage.removeItem(this.state.widgetPermissionId);
this.setState({hasPermissionToLoad: false});
const roomId = this.props.room.roomId;
console.info("Revoking permission for widget to load: " + this.props.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = false;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: false});
// Force the widget to be non-persistent
ActiveWidgetStore.destroyPersistentWidget();
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}).catch(err => {
console.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
}
formatAppTileName() {
@@ -491,18 +541,59 @@ export default class AppTile extends React.Component {
}
}
_onPopoutWidgetClick(e) {
_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');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
}
_onReloadWidgetClick(e) {
_onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions
this.refs.appFrame.src = this.refs.appFrame.src;
}
_getMenuOptions(ev) {
// TODO: This block of code gets copy/pasted a lot. We should make that happen less.
const menuOptions = {};
const buttonRect = ev.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const buttonLeft = buttonRect.left + window.pageXOffset;
const buttonTop = buttonRect.top + window.pageYOffset;
// Align the right edge of the menu to the left edge of the button
menuOptions.right = window.innerWidth - buttonLeft;
// Align the menu vertically on whichever side of the button has more
// space available.
if (buttonTop < window.innerHeight / 2) {
menuOptions.top = buttonTop;
} else {
menuOptions.bottom = window.innerHeight - buttonTop;
}
return menuOptions;
}
_onContextMenuClick = (ev) => {
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
const menuOptions = {
...this._getMenuOptions(ev),
// A revoke handler is always required
onRevokeClicked: this._onRevokeClicked,
};
const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._scalarClient && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
if (showEditButton) menuOptions.onEditClicked = this._onEditClick;
if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick;
if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick;
if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick;
createMenu(WidgetContextMenu, menuOptions);
};
render() {
let appTileBody;
@@ -512,7 +603,7 @@ export default class AppTile extends React.Component {
}
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to prgramatically remove the sandbox attribute, but
// because that would allow the iframe to programmatically remove the sandbox attribute, but
// this would only be for content hosted on the same origin as the riot client: anything
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
@@ -531,13 +622,26 @@ export default class AppTile extends React.Component {
<MessageSpinner msg='Loading...' />
</div>
);
if (this.state.initialising) {
if (!this.state.hasPermissionToLoad) {
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
url={this.state.widgetUrl}
isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
} else if (this.state.initialising) {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
{ loadingElement }
</div>
);
} else if (this.state.hasPermissionToLoad == true) {
} else {
if (this.isMixedContent()) {
appTileBody = (
<div className={appTileBodyClass}>
@@ -557,13 +661,12 @@ export default class AppTile extends React.Component {
allow={iframeFeatures}
ref="appFrame"
src={this._getSafeUrl()}
allowFullScreen="true"
allowFullScreen={true}
sandbox={sandboxFlags}
onLoad={this._onLoaded}
></iframe>
onLoad={this._onLoaded} />
</div>
);
// if the widget would be allowed to remian on screen, we must put it in
// if the widget would be allowed to remain on screen, we must put it in
// a PersistedElement from the get-go, otherwise the iframe will be
// re-mounted later when we do.
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
@@ -577,27 +680,9 @@ export default class AppTile extends React.Component {
</div>;
}
}
} else {
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
isRoomEncrypted={isRoomEncrypted}
url={this.state.widgetUrl}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
}
}
// editing is done in scalar
const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._scalarClient && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showCancelButton = (this.props.showCancel === undefined || this.props.showCancel) && !showDeleteButton;
// Picture snapshot - only show button when apps are maximised.
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
const showMinimiseButton = this.props.showMinimise && this.props.show;
const showMaximiseButton = this.props.showMinimise && !this.props.show;
@@ -636,41 +721,17 @@ export default class AppTile extends React.Component {
{ this.props.showTitle && this._getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Reload widget */ }
{ this.props.showReload && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_reload"
title={_t('Reload widget')}
onClick={this._onReloadWidgetClick}
/> }
{ /* Popout widget */ }
{ this.props.showPopout && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick}
/> }
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_snapshot"
title={_t('Picture')}
onClick={this._onSnapshotClick}
/> }
{ /* Edit widget */ }
{ showEditButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_edit"
title={_t('Edit')}
onClick={this._onEditClick}
/> }
{ /* Delete widget */ }
{ showDeleteButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_delete"
title={_t('Delete widget')}
onClick={this._onDeleteClick}
/> }
{ /* Cancel widget */ }
{ showCancelButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_cancel"
title={_t('Revoke widget access')}
onClick={this._onCancelClick}
{ /* Context menu */ }
{ <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
title={_t('More options')}
onClick={this._onContextMenuClick}
/> }
</span>
</div> }
@@ -684,6 +745,7 @@ AppTile.displayName ='AppTile';
AppTile.propTypes = {
id: PropTypes.string.isRequired,
eventId: PropTypes.string, // required for room widgets
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,

View File

@@ -16,12 +16,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
displayName: 'DeviceVerifyButtons',
propTypes: {

View File

@@ -17,12 +17,13 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
/**
* Basic container for buttons in modal dialogs.
*/
module.exports = React.createClass({
module.exports = createReactClass({
displayName: "DialogButtons",
propTypes: {

View File

@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -48,7 +49,7 @@ class MenuOption extends React.Component {
});
return <div className={optClasses}
onClick={this._onClick} onKeyPress={this._onKeyPress}
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
>
{ this.props.children }
@@ -58,7 +59,7 @@ class MenuOption extends React.Component {
MenuOption.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(React.PropTypes.node),
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
highlighted: PropTypes.bool,

View File

@@ -88,6 +88,7 @@ export class EditableItem extends React.Component {
export default class EditableItemList extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.string).isRequired,
itemsLabel: PropTypes.string,
noItemsLabel: PropTypes.string,
@@ -119,12 +120,10 @@ export default class EditableItemList extends React.Component {
_renderNewItemField() {
return (
<form onSubmit={this._onItemAdded} autoComplete={false}
<form onSubmit={this._onItemAdded} autoComplete="off"
noValidate={true} className="mx_EditableItemList_newItem">
<Field id="newEmailAddress" label={this.props.placeholder}
type="text" autoComplete="off" value={this.props.newItem}
onChange={this._onNewItemChanged}
/>
<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">
{_t("Add")}
</AccessibleButton>
@@ -135,11 +134,11 @@ export default class EditableItemList extends React.Component {
render() {
const editableItems = this.props.items.map((item, index) => {
if (!this.props.canRemove) {
return <li>{item}</li>;
return <li key={item}>{item}</li>;
}
return <EditableItem
key={index}
key={item}
index={index}
value={item}
onRemove={this._onItemRemoved}

View File

@@ -17,8 +17,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {Key} from "../../../Keyboard";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'EditableText',
propTypes: {
@@ -132,7 +134,7 @@ module.exports = React.createClass({
this.showPlaceholder(false);
}
if (ev.key === "Enter") {
if (ev.key === Key.ENTER) {
ev.stopPropagation();
ev.preventDefault();
}
@@ -149,9 +151,9 @@ module.exports = React.createClass({
this.value = ev.target.textContent;
}
if (ev.key === "Enter") {
if (ev.key === Key.ENTER) {
this.onFinish(ev);
} else if (ev.key === "Escape") {
} else if (ev.key === Key.ESCAPE) {
this.cancelEdit();
}
@@ -183,7 +185,7 @@ module.exports = React.createClass({
onFinish: function(ev, shouldSubmit) {
const self = this;
const submit = (ev.key === "Enter") || shouldSubmit;
const submit = (ev.key === Key.ENTER) || shouldSubmit;
this.setState({
phase: this.Phases.Display,
}, () => {

View File

@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Promise from 'bluebird';
/**
* A component which wraps an EditableText, with a spinner while updates take
@@ -51,7 +50,7 @@ export default class EditableTextContainer extends React.Component {
this.setState({busy: true});
this.props.getInitialValue().done(
this.props.getInitialValue().then(
(result) => {
if (this._unmounted) { return; }
this.setState({
@@ -83,7 +82,7 @@ export default class EditableTextContainer extends React.Component {
errorString: null,
});
this.props.onSubmit(value).done(
this.props.onSubmit(value).then(
() => {
if (this._unmounted) { return; }
this.setState({

View File

@@ -1,43 +0,0 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 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 PropTypes from 'prop-types';
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) {
const {element, children, addAlt, ...restProps} = props;
// fast path: simple regex to detect strings that don't contain
// emoji and just return them
if (containsEmoji(children)) {
restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt);
return React.createElement(element, restProps);
} else {
return React.createElement(element, restProps, children);
}
}
EmojiText.propTypes = {
element: PropTypes.string,
children: PropTypes.string.isRequired,
};
EmojiText.defaultProps = {
element: 'span',
addAlt: true,
};

View File

@@ -0,0 +1,106 @@
/*
Copyright 2019 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 sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
/**
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
export default class ErrorBoundary extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
error: null,
};
}
static getDerivedStateFromError(error) {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
componentDidCatch(error, { componentStack }) {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
console.error(
"The above error occured while React was rendering the following components:",
componentStack,
);
}
_onClearCacheAndReload = () => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload();
});
};
_onBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash',
});
};
render() {
if (this.state.error) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new";
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
<p>{_t(
"Please <newIssueLink>create a new issue</newIssueLink> " +
"on GitHub so that we can investigate this bug.", {}, {
newIssueLink: (sub) => {
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
},
},
)}</p>
<p>{_t(
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)}</p>
<AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")}
</AccessibleButton>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
</div>;
}
return this.props.children;
}
}

View File

@@ -0,0 +1,95 @@
/*
Copyright 2019 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, {useEffect} from 'react';
import PropTypes from 'prop-types';
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import {MatrixEvent, RoomMember} from "matrix-js-sdk";
import {useStateToggle} from "../../../hooks/useStateToggle";
const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
// Whenever expanded changes call onToggle
useEffect(() => {
if (onToggle) {
onToggle();
}
}, [expanded]); // eslint-disable-line react-hooks/exhaustive-deps
const eventIds = events.map((e) => e.getId()).join(',');
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
{ children }
</div>
);
}
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">&nbsp;</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>
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_EventListSummary_avatars" onClick={toggleExpanded}>
{ avatars }
</span>
<span className="mx_TextualEvent mx_EventListSummary_summary">
{ summaryText }
</span>
</div>
</div>
</div>
);
};
EventListSummary.propTypes = {
// An array of member events to summarise
events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
// An array of EventTiles to render when expanded
children: PropTypes.arrayOf(PropTypes.element).isRequired,
// The minimum number of events needed to trigger summarisation
threshold: PropTypes.number,
// Called when the event list expansion is toggled
onToggle: PropTypes.func,
// Whether or not to begin with state.expanded=true
startExpanded: PropTypes.bool,
// 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,
};
export default EventListSummary;

View File

@@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';
import { throttle } from 'lodash';
import { debounce } from 'lodash';
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@@ -41,11 +41,23 @@ export default class Field extends React.PureComponent {
value: PropTypes.string.isRequired,
// Optional component to include inside the field before the input.
prefix: PropTypes.node,
// Optional component to include inside the field after the input.
postfix: PropTypes.node,
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
// to the user.
onValidate: PropTypes.func,
// If specified, overrides the value returned by onValidate.
flagInvalid: PropTypes.bool,
// If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed.
tooltipContent: PropTypes.node,
// If specified alongside tooltipContent, the class name to apply to the
// tooltip itself.
tooltipClassName: PropTypes.string,
// If specified, an additional class name to apply to the field container
className: PropTypes.string,
// All other props pass through to the <input>.
};
@@ -118,14 +130,25 @@ export default class Field extends React.PureComponent {
}
}
validateOnChange = throttle(() => {
/*
* This was changed from throttle to debounce: this is more traditional for
* form validation since it means that the validation doesn't happen at all
* until the user stops typing for a bit (debounce defaults to not running on
* the leading edge). If we're doing an HTTP hit on each validation, we have more
* incentive to prevent validating input that's very unlikely to be valid.
* We may find that we actually want different behaviour for registration
* fields, in which case we can add some options to control it.
*/
validateOnChange = debounce(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
render() {
const { element, prefix, onValidate, children, ...inputProps } = this.props;
const {
element, prefix, postfix, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
const inputElement = element || "input";
@@ -144,24 +167,32 @@ export default class Field extends React.PureComponent {
if (prefix) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
}
let postfixContainer = null;
if (postfix) {
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
}
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefix,
mx_Field_valid: onValidate && this.state.valid === true,
mx_Field_invalid: onValidate && this.state.valid === false,
mx_Field_invalid: hasValidationFlag
? flagInvalid
: onValidate && this.state.valid === false,
});
// Handle displaying feedback on validity
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.feedback) {
tooltip = <Tooltip
tooltipClassName="mx_Field_tooltip"
let fieldTooltip;
if (tooltipContent || this.state.feedback) {
const addlClassName = tooltipClassName ? tooltipClassName : '';
fieldTooltip = <Tooltip
tooltipClassName={`mx_Field_tooltip ${addlClassName}`}
visible={this.state.feedbackVisible}
label={this.state.feedback}
label={tooltipContent || this.state.feedback}
/>;
}
@@ -169,7 +200,8 @@ export default class Field extends React.PureComponent {
{prefixContainer}
{fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label>
{tooltip}
{postfixContainer}
{fieldTooltip}
</div>;
}
}

View File

@@ -74,15 +74,15 @@ export default class Flair extends React.Component {
};
}
componentWillUnmount() {
this._unmounted = true;
}
componentWillMount() {
componentDidMount() {
this._unmounted = false;
this._generateAvatars(this.props.groups);
}
componentWillUnmount() {
this._unmounted = true;
}
componentWillReceiveProps(newProps) {
this._generateAvatars(newProps.groups);
}
@@ -134,9 +134,6 @@ Flair.propTypes = {
groups: PropTypes.arrayOf(PropTypes.string),
};
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
// this.context.matrixClient everywhere instead of this.props.matrixClient.
// See https://github.com/vector-im/riot-web/issues/4951.
Flair.contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};

View File

@@ -0,0 +1,28 @@
/*
Copyright 2019 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 AccessibleButton from "./AccessibleButton";
export default function FormButton(props) {
const {className, label, kind, ...restProps} = props;
const newClassName = (className || "") + " mx_FormButton";
const allProps = Object.assign({}, restProps,
{className: newClassName, kind: kind || "primary", children: [label]});
return React.createElement(AccessibleButton, allProps);
}
FormButton.propTypes = AccessibleButton.propTypes;

View File

@@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
const GroupsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton className="mx_GroupsButton" action="view_my_groups"
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
label={_t("Communities")}
size={props.size}
tooltip={true}

View File

@@ -0,0 +1,34 @@
/*
Copyright 2019 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 AccessibleButton from "./AccessibleButton";
export default function IconButton(props) {
const {icon, className, ...restProps} = props;
let newClassName = (className || "") + " mx_IconButton";
newClassName = newClassName + " mx_IconButton_icon_" + icon;
const allProps = Object.assign({}, restProps, {className: newClassName});
return React.createElement(AccessibleButton, allProps);
}
IconButton.propTypes = Object.assign({
icon: PropTypes.string,
}, AccessibleButton.propTypes);

View File

@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,7 +17,8 @@ limitations under the License.
'use strict';
const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg');
@@ -29,19 +31,19 @@ import { _t } from '../../../languageHandler';
export default class ImageView extends React.Component {
static propTypes = {
src: React.PropTypes.string.isRequired, // the source of the image being displayed
name: React.PropTypes.string, // the main title ('name') for the image
link: React.PropTypes.string, // the link (if any) applied to the name of the image
width: React.PropTypes.number, // width of the image src in pixels
height: React.PropTypes.number, // height of the image src in pixels
fileSize: React.PropTypes.number, // size of the image src in bytes
onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
src: PropTypes.string.isRequired, // the source of the image being displayed
name: PropTypes.string, // the main title ('name') for the image
link: PropTypes.string, // the link (if any) applied to the name of the image
width: PropTypes.number, // width of the image src in pixels
height: PropTypes.number, // height of the image src in pixels
fileSize: PropTypes.number, // size of the image src in bytes
onFinished: PropTypes.func.isRequired, // callback when the lightbox is dismissed
// the event (if any) that the Image is displaying. Used for event-specific stuff like
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
// properties above, which let us use lightboxes to display images which aren't associated
// with events.
mxEvent: React.PropTypes.object,
mxEvent: PropTypes.object,
};
constructor(props) {
@@ -60,7 +62,7 @@ export default class ImageView extends React.Component {
}
onKeyDown = (ev) => {
if (ev.keyCode == 27) { // escape
if (ev.keyCode === 27) { // escape
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
@@ -72,7 +74,6 @@ export default class ImageView extends React.Component {
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
onFinished: (proceed) => {
if (!proceed) return;
const self = this;
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
).catch(function(e) {
@@ -83,7 +84,7 @@ export default class ImageView extends React.Component {
title: _t('Error'),
description: _t('You cannot delete this image. (%(code)s)', {code: code}),
});
}).done();
});
},
});
};
@@ -153,32 +154,38 @@ export default class ImageView extends React.Component {
size = filesize(this.props.fileSize);
}
let size_res;
let sizeRes;
if (size && res) {
size_res = size + ", " + res;
sizeRes = size + ", " + res;
} else {
size_res = size || res;
sizeRes = size || res;
}
let mayRedact = false;
const showEventMeta = !!this.props.mxEvent;
let eventMeta;
if (showEventMeta) {
// Figure out the sender, defaulting to mxid
let sender = this.props.mxEvent.getSender();
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
if (room) {
mayRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId);
const member = room.getMember(sender);
if (member) sender = member.name;
}
eventMeta = (<div className="mx_ImageView_metadata">
{ _t('Uploaded on %(date)s by %(user)s', {date: formatDate(new Date(this.props.mxEvent.getTs())), user: sender}) }
{ _t('Uploaded on %(date)s by %(user)s', {
date: formatDate(new Date(this.props.mxEvent.getTs())),
user: sender,
}) }
</div>);
}
let eventRedact;
if (showEventMeta) {
if (mayRedact) {
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
{ _t('Remove') }
</div>);
@@ -195,13 +202,13 @@ export default class ImageView extends React.Component {
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
<div className="mx_ImageView_labelWrapper">
<div className="mx_ImageView_label">
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" onClick={ this.rotateCounterClockwise }>
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" title={_t("Rotate Left")} onClick={ this.rotateCounterClockwise }>
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_rotateClockwise" onClick={ this.rotateClockwise }>
<AccessibleButton className="mx_ImageView_rotateClockwise" title={_t("Rotate Right")} onClick={ this.rotateClockwise }>
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }>
<AccessibleButton className="mx_ImageView_cancel" title={_t("Close")} onClick={ this.props.onFinished }>
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
</AccessibleButton>
<div className="mx_ImageView_shim">
@@ -213,7 +220,7 @@ export default class ImageView extends React.Component {
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
<div className="mx_ImageView_download">
{ _t('Download this file') }<br />
<span className="mx_ImageView_size">{ size_res }</span>
<span className="mx_ImageView_size">{ sizeRes }</span>
</div>
</a>
{ eventRedact }

View File

@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const React = require('react');
import React from "react";
import createReactClass from 'create-react-class';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'InlineSpinner',
render: function() {

View File

@@ -0,0 +1,336 @@
/*
Copyright 2019 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 ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
// If the distance from tooltip to window edge is below this value, the tooltip
// will flip around to the other side of the target.
const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20;
function getOrCreateContainer() {
let container = document.getElementById(InteractiveTooltipContainerId);
if (!container) {
container = document.createElement("div");
container.id = InteractiveTooltipContainerId;
document.body.appendChild(container);
}
return container;
}
function isInRect(x, y, rect) {
const { top, right, bottom, left } = rect;
return x >= left && x <= right && y >= top && y <= bottom;
}
/**
* Returns the positive slope of the diagonal of the rect.
*
* @param {DOMRect} rect
* @return {integer}
*/
function getDiagonalSlope(rect) {
const { top, right, bottom, left } = rect;
return (bottom - top) / (right - left);
}
function isInUpperLeftHalf(x, y, rect) {
const { bottom, left } = rect;
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left));
}
function isInLowerRightHalf(x, y, rect) {
const { bottom, left } = rect;
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left));
}
function isInUpperRightHalf(x, y, rect) {
const { top, left } = rect;
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left));
}
function isInLowerLeftHalf(x, y, rect) {
const { top, left } = rect;
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left));
}
/*
* This style of tooltip takes a "target" element as its child and centers the
* tooltip along one edge of the target.
*/
export default class InteractiveTooltip extends React.Component {
propTypes: {
// Content to show in the tooltip
content: PropTypes.node.isRequired,
// Function to call when visibility of the tooltip changes
onVisibilityChange: PropTypes.func,
// flag to forcefully hide this tooltip
forceHidden: PropTypes.bool,
};
constructor() {
super();
this.state = {
contentRect: null,
visible: false,
};
}
componentDidUpdate() {
// Whenever this passthrough component updates, also render the tooltip
// in a separate DOM tree. This allows the tooltip content to participate
// the normal React rendering cycle: when this component re-renders, the
// tooltip content re-renders.
// Once we upgrade to React 16, this could be done a bit more naturally
// using the portals feature instead.
this.renderTooltip();
}
componentWillUnmount() {
document.removeEventListener("mousemove", this.onMouseMove);
}
collectContentRect = (element) => {
// We don't need to clean up when unmounting, so ignore
if (!element) return;
this.setState({
contentRect: element.getBoundingClientRect(),
});
}
collectTarget = (element) => {
this.target = element;
}
canTooltipFitAboveTarget() {
const { contentRect } = this.state;
const targetRect = this.target.getBoundingClientRect();
const targetTop = targetRect.top + window.pageYOffset;
return (
!contentRect ||
(targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)
);
}
onMouseMove = (ev) => {
const { clientX: x, clientY: y } = ev;
const { contentRect } = this.state;
const targetRect = this.target.getBoundingClientRect();
// When moving the mouse from the target to the tooltip, we create a
// safe area that includes the tooltip, the target, and the trapezoid
// ABCD between them:
// ┌───────────┐
// │ │
// │ │
// A └───E───F───┘ B
// V
// ┌─┐
// │ │
// C└─┘D
//
// As long as the mouse remains inside the safe area, the tooltip will
// stay open.
const buffer = 50;
if (isInRect(x, y, targetRect)) {
return;
}
if (this.canTooltipFitAboveTarget()) {
const contentRectWithBuffer = {
top: contentRect.top - buffer,
right: contentRect.right + buffer,
bottom: contentRect.bottom,
left: contentRect.left - buffer,
};
const trapezoidLeft = {
top: contentRect.bottom,
right: targetRect.left,
bottom: targetRect.bottom,
left: contentRect.left - buffer,
};
const trapezoidCenter = {
top: contentRect.bottom,
right: targetRect.right,
bottom: targetRect.bottom,
left: targetRect.left,
};
const trapezoidRight = {
top: contentRect.bottom,
right: contentRect.right + buffer,
bottom: targetRect.bottom,
left: targetRect.right,
};
if (
isInRect(x, y, contentRectWithBuffer) ||
isInUpperRightHalf(x, y, trapezoidLeft) ||
isInRect(x, y, trapezoidCenter) ||
isInUpperLeftHalf(x, y, trapezoidRight)
) {
return;
}
} else {
const contentRectWithBuffer = {
top: contentRect.top,
right: contentRect.right + buffer,
bottom: contentRect.bottom + buffer,
left: contentRect.left - buffer,
};
const trapezoidLeft = {
top: targetRect.top,
right: targetRect.left,
bottom: contentRect.top,
left: contentRect.left - buffer,
};
const trapezoidCenter = {
top: targetRect.top,
right: targetRect.right,
bottom: contentRect.top,
left: targetRect.left,
};
const trapezoidRight = {
top: targetRect.top,
right: contentRect.right + buffer,
bottom: contentRect.top,
left: targetRect.right,
};
if (
isInRect(x, y, contentRectWithBuffer) ||
isInLowerRightHalf(x, y, trapezoidLeft) ||
isInRect(x, y, trapezoidCenter) ||
isInLowerLeftHalf(x, y, trapezoidRight)
) {
return;
}
}
this.hideTooltip();
}
onTargetMouseOver = (ev) => {
this.showTooltip();
}
showTooltip() {
// Don't enter visible state if we haven't collected the target yet
if (!this.target) {
return;
}
this.setState({
visible: true,
});
if (this.props.onVisibilityChange) {
this.props.onVisibilityChange(true);
}
document.addEventListener("mousemove", this.onMouseMove);
}
hideTooltip() {
this.setState({
visible: false,
});
if (this.props.onVisibilityChange) {
this.props.onVisibilityChange(false);
}
document.removeEventListener("mousemove", this.onMouseMove);
}
renderTooltip() {
const { contentRect, visible } = this.state;
if (this.props.forceHidden === true || !visible) {
ReactDOM.render(null, getOrCreateContainer());
return null;
}
const targetRect = this.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const targetLeft = targetRect.left + window.pageXOffset;
const targetBottom = targetRect.bottom + window.pageYOffset;
const targetTop = targetRect.top + window.pageYOffset;
// Place the tooltip above the target by default. If we find that the
// tooltip content would extend past the safe area towards the window
// edge, flip around to below the target.
const position = {};
let chevronFace = null;
if (this.canTooltipFitAboveTarget()) {
position.bottom = window.innerHeight - targetTop;
chevronFace = "bottom";
} else {
position.top = targetBottom;
chevronFace = "top";
}
// Center the tooltip horizontally with the target's center.
position.left = targetLeft + targetRect.width / 2;
const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
const menuClasses = classNames({
'mx_InteractiveTooltip': true,
'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top',
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom',
});
const menuStyle = {};
if (contentRect) {
menuStyle.left = `-${contentRect.width / 2}px`;
}
const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
<div className={menuClasses}
style={menuStyle}
ref={this.collectContentRect}
>
{chevron}
{this.props.content}
</div>
</div>;
ReactDOM.render(tooltip, getOrCreateContainer());
}
render() {
// We use `cloneElement` here to append some props to the child content
// without using a wrapper element which could disrupt layout.
return React.cloneElement(this.props.children, {
ref: this.collectTarget,
onMouseOver: this.onTargetMouseOver,
});
}
}

View File

@@ -42,7 +42,7 @@ export default class LabelledToggleSwitch extends React.Component {
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
let secondPart = <ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
onChange={this.props.onChange} />;
onChange={this.props.onChange} aria-label={this.props.label} />;
if (this.props.toggleInFront) {
const temp = firstPart;

View File

@@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component {
this.setState({langs});
}).catch(() => {
this.setState({langs: ['en']});
}).done();
});
if (!this.props.value) {
// If no value is given, we start with the first

View File

@@ -15,9 +15,7 @@ limitations under the License.
*/
import React from "react";
const OVERFLOW_ITEMS = 20;
const OVERFLOW_MARGIN = 5;
import PropTypes from 'prop-types';
class ItemRange {
constructor(topCount, renderCount, bottomCount) {
@@ -27,11 +25,22 @@ class ItemRange {
}
contains(range) {
// don't contain empty ranges
// as it will prevent clearing the list
// once it is scrolled far enough out of view
if (!range.renderCount && this.renderCount) {
return false;
}
return range.topCount >= this.topCount &&
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
}
expand(amount) {
// don't expand ranges that won't render anything
if (this.renderCount === 0) {
return this;
}
const topGrow = Math.min(amount, this.topCount);
const bottomGrow = Math.min(amount, this.bottomCount);
return new ItemRange(
@@ -40,52 +49,80 @@ class ItemRange {
this.bottomCount - bottomGrow,
);
}
totalSize() {
return this.topCount + this.renderCount + this.bottomCount;
}
}
export default class LazyRenderList extends React.Component {
constructor(props) {
super(props);
const renderRange = LazyRenderList.getVisibleRangeFromProps(props).expand(OVERFLOW_ITEMS);
this.state = {renderRange};
static getDerivedStateFromProps(props, state) {
const range = LazyRenderList.getVisibleRangeFromProps(props);
const intersectRange = range.expand(props.overflowMargin);
const renderRange = range.expand(props.overflowItems);
const listHasChangedSize = !!state && renderRange.totalSize() !== state.renderRange.totalSize();
// only update render Range if the list has shrunk/grown and we need to adjust padding OR
// if the new range + overflowMargin isn't contained by the old anymore
if (listHasChangedSize || !state || !state.renderRange.contains(intersectRange)) {
return {renderRange};
}
return null;
}
static getVisibleRangeFromProps(props) {
const {items, itemHeight, scrollTop, height} = props;
const length = items ? items.length : 0;
const topCount = Math.max(0, Math.floor(scrollTop / itemHeight));
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
const itemsAfterTop = length - topCount;
const renderCount = Math.min(Math.ceil(height / itemHeight), itemsAfterTop);
const visibleItems = height !== 0 ? Math.ceil(height / itemHeight) : 0;
const renderCount = Math.min(visibleItems, itemsAfterTop);
const bottomCount = itemsAfterTop - renderCount;
return new ItemRange(topCount, renderCount, bottomCount);
}
componentWillReceiveProps(props) {
const state = this.state;
const range = LazyRenderList.getVisibleRangeFromProps(props);
const intersectRange = range.expand(OVERFLOW_MARGIN);
const prevSize = this.props.items ? this.props.items.length : 0;
const listHasChangedSize = props.items.length !== prevSize;
// only update renderRange if the list has shrunk/grown and we need to adjust padding or
// if the new range isn't contained by the old anymore
if (listHasChangedSize || !state.renderRange || !state.renderRange.contains(intersectRange)) {
this.setState({renderRange: range.expand(OVERFLOW_ITEMS)});
}
}
render() {
const {itemHeight, items, renderItem} = this.props;
const {renderRange} = this.state;
const paddingTop = renderRange.topCount * itemHeight;
const paddingBottom = renderRange.bottomCount * itemHeight;
const {topCount, renderCount, bottomCount} = renderRange;
const paddingTop = topCount * itemHeight;
const paddingBottom = bottomCount * itemHeight;
const renderedItems = (items || []).slice(
renderRange.topCount,
renderRange.topCount + renderRange.renderCount,
topCount,
topCount + renderCount,
);
return (<div style={{paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px`}}>
{ renderedItems.map(renderItem) }
</div>);
const element = this.props.element || "div";
const elementProps = {
"style": {paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px`},
"className": this.props.className,
};
return React.createElement(element, elementProps, renderedItems.map(renderItem));
}
}
LazyRenderList.defaultProps = {
overflowItems: 20,
overflowMargin: 5,
};
LazyRenderList.propTypes = {
// height in pixels of the component returned by `renderItem`
itemHeight: PropTypes.number.isRequired,
// function to turn an element of `items` into a react component
renderItem: PropTypes.func.isRequired,
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
scrollTop: PropTypes.number.isRequired,
// the height of the viewport this content is scrolled in
height: PropTypes.number.isRequired,
// all items for the list. These should not be react components, see `renderItem`.
items: PropTypes.array,
// the amount of items to scroll before causing a rerender,
// should typically be less than `overflowItems` unless applying
// margins in the parent component when using multiple LazyRenderList in one viewport.
// use 0 to only rerender when items will come into view.
overflowMargin: PropTypes.number,
// the amount of items to add at the top and bottom to render,
// so not every scroll of causes a rerender.
overflowItems: PropTypes.number,
};

View File

@@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 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.
@@ -17,95 +18,40 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import classNames from 'classnames';
import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import Modal from "../../../Modal";
import { _t } from '../../../languageHandler';
import AccessibleButton from './AccessibleButton';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
export default class ManageIntegsButton extends React.Component {
constructor(props) {
super(props);
this.state = {
scalarError: null,
};
this.onManageIntegrations = this.onManageIntegrations.bind(this);
}
componentWillMount() {
ScalarMessaging.startListening();
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
}, (err) => {
this.setState({scalarError: err});
console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
});
}
}
componentWillUnmount() {
ScalarMessaging.stopListening();
}
onManageIntegrations(ev) {
onManageIntegrations = (ev) => {
ev.preventDefault();
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
return;
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
managers.openAll(this.props.room);
} else {
managers.getPrimaryManager().open(this.props.room);
}
}
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
this.scalarClient.connect().done(() => {
Modal.createDialog(IntegrationsManager, {
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
null,
}, "mx_IntegrationsManager");
}, (err) => {
this.setState({scalarError: err});
console.error('Error ensuring a valid scalar_token exists', err);
});
}
};
render() {
let integrationsButton = <div />;
let integrationsWarningTriangle = <div />;
let integrationsErrorPopup = <div />;
if (this.scalarClient !== null) {
const integrationsButtonClasses = classNames({
mx_RoomHeader_button: true,
mx_RoomHeader_manageIntegsButton: true,
mx_ManageIntegsButton_error: !!this.state.scalarError,
});
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
integrationsWarningTriangle = <img
src={require("../../../../res/img/warning.svg")}
title={_t('Integrations Error')}
width="17"
/>;
// Popup shown when hovering over integrationsButton_error (via CSS)
integrationsErrorPopup = (
<span className="mx_ManageIntegsButton_errorPopup">
{ _t('Could not connect to the integration server') }
</span>
);
}
if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = (
<AccessibleButton className={integrationsButtonClasses}
<AccessibleButton
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
title={_t("Manage Integrations")}
onClick={this.onManageIntegrations}
title={_t('Manage Integrations')}
>
{ integrationsWarningTriangle }
{ integrationsErrorPopup }
</AccessibleButton>
/>
);
}

View File

@@ -1,5 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,18 +15,21 @@ 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 sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import sdk from "../../../index";
import {MatrixEvent} from "matrix-js-sdk";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MemberEventListSummary',
propTypes: {
// An array of member events to summarise
events: PropTypes.array.isRequired,
events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
// An array of EventTiles to render when expanded
children: PropTypes.array.isRequired,
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
@@ -39,12 +44,6 @@ module.exports = React.createClass({
startExpanded: PropTypes.bool,
},
getInitialState: function() {
return {
expanded: Boolean(this.props.startExpanded),
};
},
getDefaultProps: function() {
return {
summaryLength: 1,
@@ -53,37 +52,27 @@ module.exports = React.createClass({
};
},
shouldComponentUpdate: function(nextProps, nextState) {
shouldComponentUpdate: function(nextProps) {
// Update if
// - The number of summarised events has changed
// - or if the summary is currently expanded
// - or if the summary is about to toggle to become collapsed
// - or if there are fewEvents, meaning the child eventTiles are shown as-is
return (
nextProps.events.length !== this.props.events.length ||
this.state.expanded || nextState.expanded ||
nextProps.events.length < this.props.threshold
);
},
_toggleSummary: function() {
this.setState({
expanded: !this.state.expanded,
});
this.props.onToggle();
},
/**
* Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where
* Generate the text for users aggregated by their transition sequences (`eventAggregates`) where
* the sequences are ordered by `orderedTransitionSequences`.
* @param {object[]} eventAggregates a map of transition sequence to array of user display names
* or user IDs.
* @param {string[]} orderedTransitionSequences an array which is some ordering of
* `Object.keys(eventAggregates)`.
* @returns {ReactElement} a single <span> containing the textual summary of the aggregated
* events that occurred.
* @returns {string} the textual summary of the aggregated events that occurred.
*/
_renderSummary: function(eventAggregates, orderedTransitionSequences) {
_generateSummary: function(eventAggregates, orderedTransitionSequences) {
const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions];
const nameList = this._renderNameList(userNames);
@@ -105,7 +94,7 @@ module.exports = React.createClass({
);
});
const desc = this._renderCommaSeparatedList(descs);
const desc = formatCommaSeparatedList(descs);
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
});
@@ -114,15 +103,7 @@ module.exports = React.createClass({
return null;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
<EmojiText>
{ summaries.join(", ") }
</EmojiText>
</span>
);
return summaries.join(", ");
},
/**
@@ -132,7 +113,7 @@ module.exports = React.createClass({
* included before "and [n] others".
*/
_renderNameList: function(users) {
return this._renderCommaSeparatedList(users, this.props.summaryLength);
return formatCommaSeparatedList(users, this.props.summaryLength);
},
/**
@@ -208,7 +189,7 @@ module.exports = React.createClass({
* For a certain transition, t, describe what happened to the users that
* underwent the transition.
* @param {string} t the transition type.
* @param {integer} userCount number of usernames
* @param {number} userCount number of usernames
* @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written Human Readable equivalent of the transition.
*/
@@ -278,53 +259,16 @@ module.exports = React.createClass({
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: repeats });
break;
case "no_change":
res = (userCount > 1)
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count: repeats });
break;
}
return res;
},
/**
* Constructs a written English string representing `items`, with an optional limit on
* the number of items included in the result. If specified and if the length of
*`items` is greater than the limit, the string "and n others" will be appended onto
* the result.
* If `items` is empty, returns the empty string. If there is only one item, return
* it.
* @param {string[]} items the items to construct a string from.
* @param {number?} itemLimit the number by which to limit the list.
* @returns {string} a string constructed by joining `items` with a comma between each
* item, but with the last item appended as " and [lastItem]".
*/
_renderCommaSeparatedList(items, itemLimit) {
const remaining = itemLimit === undefined ? 0 : Math.max(
items.length - itemLimit, 0,
);
if (items.length === 0) {
return "";
} else if (items.length === 1) {
return items[0];
} else if (remaining > 0) {
items = items.slice(0, itemLimit);
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
} else {
const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
}
},
_renderAvatars: function(roomMembers) {
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
return (
<MemberAvatar key={m.userId} member={m} width={14} height={14} />
);
});
return (
<span className="mx_MemberEventListSummary_avatars" onClick={this._toggleSummary}>
{ avatars }
</span>
);
},
_getTransitionSequence: function(events) {
return events.map(this._getTransition);
},
@@ -338,6 +282,11 @@ module.exports = React.createClass({
* if a transition is not recognised.
*/
_getTransition: function(e) {
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
// Handle 3pid invites the same as invites so they get bundled together
return 'invited';
}
switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited';
case 'ban': return 'banned';
@@ -351,7 +300,7 @@ module.exports = React.createClass({
return 'changed_avatar';
}
// console.log("MELS ignoring duplicate membership join event");
return null;
return 'no_change';
} else {
return 'joined';
}
@@ -365,8 +314,8 @@ module.exports = React.createClass({
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return 'invite_withdrawal';
case 'ban': return 'unbanned';
case 'join': return 'kicked';
default: return 'left';
// sender is not target and made the target leave, if not from invite/ban then this is a kick
default: return 'kicked';
}
default: return null;
}
@@ -415,22 +364,6 @@ module.exports = React.createClass({
render: function() {
const eventsToRender = this.props.events;
const eventIds = eventsToRender.map((e) => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents;
let expandedEvents = null;
if (expanded) {
expandedEvents = this.props.children;
}
if (fewEvents) {
return (
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{ expandedEvents }
</div>
);
}
// Map user IDs to an array of objects:
const userEvents = {
@@ -452,9 +385,17 @@ module.exports = React.createClass({
userEvents[userId] = [];
if (e.target) avatarMembers.push(e.target);
}
let displayName = userId;
if (e.getType() === 'm.room.third_party_invite') {
displayName = e.getContent().display_name;
} else if (e.target) {
displayName = e.target.name;
}
userEvents[userId].push({
mxEvent: e,
displayName: (e.target ? e.target.name : null) || userId,
displayName,
index: index,
});
});
@@ -466,30 +407,14 @@ module.exports = React.createClass({
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
);
let summaryContainer = null;
if (!expanded) {
summaryContainer = (
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
{ this._renderAvatars(avatarMembers) }
{ this._renderSummary(aggregate.names, orderedTransitionSequences) }
</div>
</div>
);
}
const toggleButton = (
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
{ expanded ? _t('collapse') : _t('expand') }
</div>
);
return (
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{ toggleButton }
{ summaryContainer }
{ expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null }
{ expandedEvents }
</div>
);
const EventListSummary = sdk.getComponent("views.elements.EventListSummary");
return <EventListSummary
events={this.props.events}
threshold={this.props.threshold}
onToggle={this.props.onToggle}
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={avatarMembers}
summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />;
},
});

View File

@@ -15,8 +15,9 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MessageSpinner',
render: function() {

View File

@@ -15,13 +15,14 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import RoomViewStore from '../../../stores/RoomViewStore';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'PersistentApp',
getInitialState: function() {
@@ -66,13 +67,15 @@ module.exports = React.createClass({
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
});
const app = WidgetUtils.makeAppConfig(
appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(),
);
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
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}

Some files were not shown because too many files have changed in this diff Show More