Merge branches 'develop' and 't3chguy/report_event' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/report_event

 Conflicts:
	src/i18n/strings/en_EN.json
This commit is contained in:
Michael Telatynski 2019-09-12 12:45:16 +01:00
commit 9a15d4cfc1
305 changed files with 8157 additions and 1832 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: {

View file

@ -15,9 +15,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() {

View file

@ -17,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';
@ -63,7 +64,7 @@ import SettingsStore from "../../../settings/SettingsStore";
* focus: set the input focus appropriately in the form.
*/
export const PasswordAuthEntry = React.createClass({
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
statics: {
@ -162,7 +163,7 @@ export const PasswordAuthEntry = React.createClass({
},
});
export const RecaptchaAuthEntry = React.createClass({
export const RecaptchaAuthEntry = createReactClass({
displayName: 'RecaptchaAuthEntry',
statics: {
@ -212,7 +213,7 @@ export const RecaptchaAuthEntry = React.createClass({
},
});
export const TermsAuthEntry = React.createClass({
export const TermsAuthEntry = createReactClass({
displayName: 'TermsAuthEntry',
statics: {
@ -351,7 +352,7 @@ export const TermsAuthEntry = React.createClass({
},
});
export const EmailIdentityAuthEntry = React.createClass({
export const EmailIdentityAuthEntry = createReactClass({
displayName: 'EmailIdentityAuthEntry',
statics: {
@ -393,7 +394,7 @@ export const EmailIdentityAuthEntry = React.createClass({
},
});
export const MsisdnAuthEntry = React.createClass({
export const MsisdnAuthEntry = createReactClass({
displayName: 'MsisdnAuthEntry',
statics: {
@ -540,7 +541,7 @@ export const MsisdnAuthEntry = React.createClass({
},
});
export const FallbackAuthEntry = React.createClass({
export const FallbackAuthEntry = createReactClass({
displayName: 'FallbackAuthEntry',
propTypes: {

View file

@ -15,13 +15,13 @@ 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';
@ -33,49 +33,8 @@ const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_
* 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,
// 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,
};
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
delayTimeMs: 0,
};
constructor(props) {
super(props);
this.state = {
busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
};
}
componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
export default class ModularServerConfig extends ServerConfig {
static propTypes = ServerConfig.propTypes;
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
@ -120,35 +79,6 @@ export default class ModularServerConfig extends React.PureComponent {
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
}
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.validateServer();
});
};
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
};
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) {
clearTimeout(existingTimeoutId);
}
return setTimeout(fn.bind(this), this.props.delayTimeMs);
}
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View file

@ -31,6 +31,7 @@ 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,
@ -257,6 +258,7 @@ export default class PasswordLogin extends React.Component {
render() {
const Field = sdk.getComponent('elements.Field');
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let forgotPasswordJsx;
@ -273,33 +275,6 @@ export default class PasswordLogin extends React.Component {
</span>;
}
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>;
}
const pwFieldClass = classNames({
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
@ -342,10 +317,8 @@ export default 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}

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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';
@ -39,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: {
@ -54,6 +56,7 @@ module.exports = React.createClass({
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
canSubmit: PropTypes.bool,
serverRequiresIdServer: PropTypes.bool,
},
getDefaultProps: function() {
@ -69,10 +72,10 @@ 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,
@ -90,7 +93,7 @@ module.exports = React.createClass({
}
const self = this;
if (this.state.email == '') {
if (this.state.email === '') {
const haveIs = Boolean(this.props.serverConfig.isUrl);
let desc;
@ -436,7 +439,17 @@ module.exports = React.createClass({
_showEmail() {
const haveIs = Boolean(this.props.serverConfig.isUrl);
if (!haveIs || !this._authStepIsUsed('m.login.email.identity')) {
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);
const haveRequiredIs = this.props.serverRequiresIdServer && !haveIs;
if (!threePidLogin || haveRequiredIs || !this._authStepIsUsed('m.login.msisdn')) {
return false;
}
return true;
@ -455,7 +468,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}
@ -469,7 +481,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}
@ -483,7 +494,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}
@ -491,9 +501,7 @@ module.exports = React.createClass({
},
renderPhoneNumber() {
const threePidLogin = !SdkConfig.get().disable_3pid_login;
const haveIs = Boolean(this.props.serverConfig.isUrl);
if (!threePidLogin || !haveIs || !this._authStepIsUsed('m.login.msisdn')) {
if (!this._showPhoneNumber()) {
return null;
}
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
@ -512,7 +520,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}
@ -528,7 +535,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}
@ -567,11 +573,24 @@ module.exports = React.createClass({
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
);
const emailHelperText = this._showEmail() ? <div>
{_t("Use an email address to recover your account.") + " "}
{_t("Other users can invite you to rooms using your contact details.")}
</div> : null;
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);
const noIsText = haveIs ? null : <div>
{_t(

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.
@ -23,6 +24,8 @@ 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.
@ -46,6 +49,10 @@ export default class ServerConfig extends React.PureComponent {
// 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 = {
@ -61,6 +68,7 @@ export default class ServerConfig extends React.PureComponent {
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
showIdentityServer: false,
};
}
@ -75,14 +83,41 @@ export default class ServerConfig extends React.PureComponent {
// 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, this.state.isUrl);
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({busy: false, errorText: ""});
this.setState({
hsUrl: defaultConfig.hsUrl,
isUrl: defaultConfig.isUrl,
busy: false,
errorText: "",
});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
@ -126,6 +161,15 @@ export default class ServerConfig extends React.PureComponent {
}
}
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.validateServer();
@ -171,8 +215,49 @@ export default class ServerConfig extends React.PureComponent {
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
};
render() {
_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 AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const errorText = this.state.errorText
@ -191,31 +276,10 @@ export default class ServerConfig extends React.PureComponent {
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>,
})}
{errorText}
{this._renderHomeserverSection()}
{this._renderIdentityServerSection()}
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
<div className="mx_ServerConfig_fields">
<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}
/>
<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>
{submitButton}
</form>
</div>

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,13 +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: {
@ -121,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,

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 Avatar from '../../../Avatar';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RoomAvatar',
// Room may be left unset here, but if it is,

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

@ -18,6 +18,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';
@ -34,7 +35,7 @@ function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MessageContextMenu',
propTypes: {

View file

@ -18,8 +18,9 @@ limitations under the License.
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';
@ -31,7 +32,7 @@ import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RoomTileContextMenu',
propTypes: {

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

@ -19,15 +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 dis from '../../../dispatcher';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -39,7 +43,7 @@ const addressTypeName = {
};
module.exports = React.createClass({
module.exports = createReactClass({
displayName: "AddressPickerDialog",
propTypes: {
@ -48,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,
@ -90,6 +94,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.
validAddressTypes: this.props.validAddressTypes,
};
},
@ -100,6 +107,15 @@ 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
@ -433,7 +449,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;
@ -469,7 +485,7 @@ module.exports = React.createClass({
isKnown: false,
};
if (!this.props.validAddressTypes.includes(addrType)) {
if (!this.state.validAddressTypes.includes(addrType)) {
hasError = true;
} else if (addrType === 'mx-user-id') {
const user = MatrixClientPeg.get().getUser(addrObj.address);
@ -570,12 +586,37 @@ module.exports = React.createClass({
this._addAddressesToList(text.split(/[\s,]+/));
},
onUseDefaultIdentityServerClick(e) {
e.preventDefault();
// Update the IS in account data. Actually using it may trigger terms.
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
if (this.state.selectedList.length > 0) {
@ -602,7 +643,7 @@ module.exports = React.createClass({
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>,
@ -613,7 +654,7 @@ module.exports = React.createClass({
let error;
let addressSelector;
if (this.state.invalidAddressError) {
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t]));
error = <div className="mx_AddressPickerDialog_error">
{ _t("You have entered an invalid address.") }
<br />
@ -636,17 +677,43 @@ module.exports = React.createClass({
);
}
let identityServer;
if (this.props.pickerType === 'user' && !this.state.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';
@ -32,7 +33,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: {

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'

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,

View file

@ -15,12 +15,13 @@ 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 { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,

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.
@ -241,6 +242,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 +261,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>

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

@ -17,12 +17,13 @@ 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,

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,

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

@ -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: {

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,

View file

@ -17,6 +17,7 @@ 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 +35,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

@ -17,6 +17,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 { _t } from '../../../languageHandler';
@ -62,7 +63,7 @@ const WarmFuzzy = function(props) {
*
* On success, `onFinished()` when finished
*/
export default React.createClass({
export default createReactClass({
displayName: 'SetPasswordDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,

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 integrations manager so that
// it gets the same basic size as the IM's own modal.
return dialogTermsInteractionCallback(
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager',
);
});
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 IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
let uiUrl = null;
if (this.state.currentScalarClient) {
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
this.props.room,
this.props.screen,
this.props.integrationId,
);
}
return <IntegrationsManager
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

@ -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

@ -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';
@ -29,7 +30,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,

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,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,7 +33,8 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import { showIntegrationsManager } from '../../../integrations/integrations';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -157,7 +156,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);
}
@ -178,9 +177,22 @@ 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
// Fetch the token before loading the iframe as we need it to mangle the URL
if (!this._scalarClient) {
this._scalarClient = new ScalarAuthClient();
this._scalarClient = managers.getPrimaryManager().getScalarClient();
}
this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param if not already present
@ -189,7 +201,7 @@ export default class AppTile extends React.Component {
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;
}
@ -251,11 +263,20 @@ export default class AppTile extends React.Component {
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
showIntegrationsManager({
room: this.props.room,
screen: 'type_' + this.props.type,
integrationId: this.props.id,
});
// 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,
);
}
}
}
@ -429,7 +450,7 @@ export default class AppTile extends React.Component {
this.setState({hasPermissionToLoad: false});
// Force the widget to be non-persistent
ActiveWidgetStore.destroyPersistentWidget();
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
@ -575,11 +596,10 @@ export default class AppTile extends React.Component {
src={this._getSafeUrl()}
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')) {

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

@ -17,8 +17,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'EditableText',
propTypes: {

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;
@ -46,6 +46,14 @@ export default class Field extends React.PureComponent {
// 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,
// All other props pass through to the <input>.
};
@ -118,14 +126,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, onValidate, children, tooltipContent, flagInvalid,
tooltipClassName, ...inputProps} = this.props;
const inputElement = element || "input";
@ -145,23 +164,27 @@ export default class Field extends React.PureComponent {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
}
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
// 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 +192,7 @@ export default class Field extends React.PureComponent {
{prefixContainer}
{fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label>
{tooltip}
{fieldTooltip}
</div>;
}
}

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

@ -18,9 +18,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import { _t } from '../../../languageHandler';
import { showIntegrationsManager } from '../../../integrations/integrations';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
export default class ManageIntegsButton extends React.Component {
constructor(props) {
@ -30,12 +30,21 @@ export default class ManageIntegsButton extends React.Component {
onManageIntegrations = (ev) => {
ev.preventDefault();
showIntegrationsManager({ room: this.props.room });
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);
}
}
};
render() {
let integrationsButton = <div />;
if (ScalarAuthClient.isPossible()) {
if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = (
<AccessibleButton

View file

@ -18,11 +18,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MemberEventListSummary',
propTypes: {

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() {

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import classNames from 'classnames';
@ -31,7 +32,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
const Pill = React.createClass({
const Pill = createReactClass({
statics: {
isPillUrl: (url) => {
return !!REGEX_MATRIXTO.exec(url);

View file

@ -14,15 +14,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 * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
import Field from "./Field";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'PowerSelector',
propTypes: {

View file

@ -14,12 +14,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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'ProgressBar',
propTypes: {
value: PropTypes.number,

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 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.
@ -176,6 +177,9 @@ export default class ReplyThread extends React.Component {
componentWillMount() {
this.unmounted = false;
this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId());
this.room.on("Room.redaction", this.onRoomRedaction);
// same event handler as Room.redaction as for both we just do forceUpdate
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
this.initialize();
}
@ -185,8 +189,21 @@ export default class ReplyThread extends React.Component {
componentWillUnmount() {
this.unmounted = true;
if (this.room) {
this.room.removeListener("Room.redaction", this.onRoomRedaction);
this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
}
}
onRoomRedaction = (ev, room) => {
if (this.unmounted) return;
// If one of the events we are rendering gets redacted, force a re-render
if (this.state.events.some(event => event.getId() === ev.getId())) {
this.forceUpdate();
}
};
async initialize() {
const {parentEv} = this.props;
// at time of making this component we checked that props.parentEv has a parentEventId
@ -298,11 +315,13 @@ export default class ReplyThread extends React.Component {
return <blockquote className="mx_ReplyThread" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev}
tileShape="reply"
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
<EventTile
mxEvent={ev}
tileShape="reply"
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
</blockquote>;
});

View file

@ -16,11 +16,12 @@ limitations under the License.
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
import ToggleSwitch from "./ToggleSwitch";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'SettingsFlag',
propTypes: {
name: PropTypes.string.isRequired,

View file

@ -14,11 +14,10 @@ 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';
const React = require('react');
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'Spinner',
render: function() {

View file

@ -0,0 +1,51 @@
/*
Copyright 2019 Sorunome
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';
export default class Spoiler extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
};
}
toggleVisible(e) {
if (!this.state.visible) {
// we are un-blurring, we don't want this click to propagate to potential child pills
e.preventDefault();
e.stopPropagation();
}
this.setState({ visible: !this.state.visible });
}
render() {
const reason = this.props.reason ? (
<span className="mx_EventTile_spoiler_reason">{"(" + this.props.reason + ")"}</span>
) : null;
// react doesn't allow appending a DOM node as child.
// as such, we pass the this.props.contentHtml instead and then set the raw
// HTML content. This is secure as the contents have already been parsed previously
return (
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}>
{ reason }
&nbsp;
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
</span>
);
}
}

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 { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
@ -34,7 +35,7 @@ import TagOrderStore from '../../../stores/TagOrderStore';
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
export default React.createClass({
export default createReactClass({
displayName: 'TagTile',
propTypes: {

View file

@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const React = require('react');
const ReactDOM = require("react-dom");
import React from 'react';
import PropTypes from 'prop-types';
const Tinter = require("../../../Tinter");
import createReactClass from 'create-react-class';
import Tinter from "../../../Tinter";
var TintableSvg = React.createClass({
const TintableSvg = createReactClass({
displayName: 'TintableSvg',
propTypes: {

View file

@ -20,12 +20,13 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
import classNames from 'classnames';
const MIN_TOOLTIP_HEIGHT = 25;
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'Tooltip',
propTypes: {

View file

@ -16,9 +16,10 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'TooltipButton',
getInitialState: function() {

View file

@ -17,9 +17,10 @@ limitations under the License.
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: 'TruncatedList',
propTypes: {

View file

@ -14,13 +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: 'UserSelector',
propTypes: {

View file

@ -14,14 +14,13 @@ 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 { _t } from '../../../languageHandler';
import Notifier from '../../../Notifier';
import AccessibleButton from '../../../components/views/elements/AccessibleButton';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MatrixToolbar',
hideToolbar: function() {

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 sdk from '../../../index';
import Modal from '../../../Modal';
import PlatformPeg from '../../../PlatformPeg';
@ -31,7 +32,7 @@ function checkVersion(ver) {
return parts.length == 5 && parts[1] == 'react' && parts[3] == 'js';
}
export default React.createClass({
export default createReactClass({
propTypes: {
version: PropTypes.string.isRequired,
newVersion: PropTypes.string.isRequired,

View file

@ -16,11 +16,12 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
onUpdateClicked: function() {
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);

View file

@ -16,11 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import { _td } from '../../../languageHandler';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
export default React.createClass({
export default createReactClass({
propTypes: {
// 'hard' if the logged in user has been locked out, 'soft' if they haven't
kind: PropTypes.string,

View file

@ -16,11 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import PlatformPeg from '../../../PlatformPeg';
import AccessibleButton from '../../../components/views/elements/AccessibleButton';
export default React.createClass({
export default createReactClass({
propTypes: {
status: PropTypes.string.isRequired,
// Currently for error detail but will be usable for download progress

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 { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
@ -25,7 +26,7 @@ import classNames from 'classnames';
import MatrixClientPeg from "../../../MatrixClientPeg";
import {createMenu} from "../../structures/ContextualMenu";
export default React.createClass({
export default createReactClass({
displayName: 'GroupInviteTile',
propTypes: {

View file

@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
@ -26,7 +27,7 @@ import { GroupMemberType } from '../../../groups';
import GroupStore from '../../../stores/GroupStore';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'GroupMemberInfo',
contextTypes: {

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import dis from '../../../dispatcher';
@ -27,7 +28,7 @@ import RightPanel from '../../structures/RightPanel';
const INITIAL_LOAD_NUM_MEMBERS = 30;
export default React.createClass({
export default createReactClass({
displayName: 'GroupMemberList',
propTypes: {

View file

@ -18,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 sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupMemberType } from '../../../groups';
export default React.createClass({
export default createReactClass({
displayName: 'GroupMemberTile',
propTypes: {

View file

@ -16,11 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import GroupStore from '../../../stores/GroupStore';
import ToggleSwitch from "../elements/ToggleSwitch";
export default React.createClass({
export default createReactClass({
displayName: 'GroupPublicityToggle',
propTypes: {

View file

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
@ -23,7 +24,7 @@ import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import GroupStore from '../../../stores/GroupStore';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'GroupRoomInfo',
contextTypes: {

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStore from '../../../stores/GroupStore';
@ -24,7 +25,7 @@ import TintableSvg from '../elements/TintableSvg';
const INITIAL_LOAD_NUM_ROOMS = 30;
export default React.createClass({
export default createReactClass({
propTypes: {
groupId: PropTypes.string.isRequired,
},

View file

@ -15,13 +15,14 @@ limitations under the License.
*/
import React from 'react';
import {MatrixClient} from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups';
const GroupRoomTile = React.createClass({
const GroupRoomTile = createReactClass({
displayName: 'GroupRoomTile',
propTypes: {

View file

@ -16,6 +16,7 @@ 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 { Draggable, Droppable } from 'react-beautiful-dnd';
import sdk from '../../../index';
@ -24,7 +25,7 @@ import FlairStore from '../../../stores/FlairStore';
function nop() {}
const GroupTile = React.createClass({
const GroupTile = createReactClass({
displayName: 'GroupTile',
propTypes: {

View file

@ -16,11 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
export default React.createClass({
export default createReactClass({
displayName: 'GroupUserSettings',
contextTypes: {

View file

@ -15,10 +15,9 @@ 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 filesize from 'filesize';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
@ -195,7 +194,7 @@ function computedStyle(element) {
return cssText;
}
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MFileBody',
getInitialState: function() {

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018, 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.
@ -264,6 +264,7 @@ export default class MImageBody extends React.Component {
decryptedBlob = blob;
return URL.createObjectURL(blob);
}).then((contentUrl) => {
if (this.unmounted) return;
this.setState({
decryptedUrl: contentUrl,
decryptedThumbnailUrl: thumbnailUrl,
@ -271,6 +272,7 @@ export default class MImageBody extends React.Component {
});
});
}).catch((err) => {
if (this.unmounted) return;
console.warn("Unable to decrypt attachment: ", err);
// Set a placeholder image when we can't decrypt the image.
this.setState({

View file

@ -14,10 +14,9 @@ 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 MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile } from '../../../utils/DecryptFile';
@ -25,7 +24,7 @@ import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MVideoBody',
propTypes: {

View file

@ -14,13 +14,12 @@ 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';
const sdk = require('../../../index');
import createReactClass from 'create-react-class';
import sdk from '../../../index';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'MessageEvent',
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.
@ -16,14 +17,14 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RoomAvatarEvent',
propTypes: {
@ -31,12 +32,21 @@ module.exports = React.createClass({
mxEvent: PropTypes.object.isRequired,
},
onAvatarClick: function(name) {
const httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
onAvatarClick: function() {
const cli = MatrixClientPeg.get();
const ev = this.props.mxEvent;
const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
senderDisplayName: ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(),
roomName: room ? room.name : '',
});
const ImageView = sdk.getComponent("elements.ImageView");
const params = {
src: httpUrl,
name: name,
name: text,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
@ -44,29 +54,22 @@ module.exports = React.createClass({
render: function() {
const ev = this.props.mxEvent;
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
senderDisplayName: senderDisplayName,
roomName: room ? room.name : '',
});
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return (
<div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) }
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
</div>
);
}
const url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
ev.getContent().url,
Math.ceil(14 * window.devicePixelRatio),
Math.ceil(14 * window.devicePixelRatio),
'crop',
);
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
// Provide all arguments to RoomAvatar via oobData because the avatar is historic
const oobData = {
avatarUrl: ev.getContent().url,
name: room ? room.name : "",
};
return (
<div className="mx_RoomAvatarEvent">
@ -75,8 +78,8 @@ module.exports = React.createClass({
{
'img': () =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick.bind(this, name)}>
<BaseAvatar width={14} height={14} url={url} name={name} />
onClick={this.onAvatarClick}>
<RoomAvatar width={14} height={14} oobData={oobData} />
</AccessibleButton>,
})
}

View file

@ -16,13 +16,14 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
import { RoomPermalinkCreator } from '../../../matrix-to';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'RoomCreate',
propTypes: {

View file

@ -14,17 +14,16 @@
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
export default React.createClass({
export default createReactClass({
displayName: 'SenderProfile',
propTypes: {
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing

View file

@ -16,18 +16,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils';
import {formatDate} from '../../../DateUtils';
import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import * as ContextualMenu from '../../structures/ContextualMenu';
@ -35,8 +32,9 @@ import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread";
import {host as matrixtoHost} from '../../../matrix-to';
import {pillifyLinks} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'TextualBody',
propTypes: {
@ -95,6 +93,8 @@ module.exports = React.createClass({
},
_applyFormatting() {
this.activateSpoilers(this.refs.content.children);
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
@ -183,6 +183,34 @@ module.exports = React.createClass({
}
},
activateSpoilers: function(nodes) {
let node = nodes[0];
while (node) {
if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
const spoilerContainer = document.createElement('span');
const reason = node.getAttribute("data-mx-spoiler");
const Spoiler = sdk.getComponent('elements.Spoiler');
node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
const spoiler = <Spoiler
reason={reason}
contentHtml={node.outerHTML}
/>;
ReactDOM.render(spoiler, spoilerContainer);
node.parentNode.replaceChild(spoilerContainer, node);
node = spoilerContainer;
}
if (node.childNodes && node.childNodes.length) {
this.activateSpoilers(node.childNodes);
}
node = node.nextSibling;
}
},
findLinks: function(nodes) {
let links = [];
@ -318,12 +346,19 @@ module.exports = React.createClass({
// which requires the user to click through and THEN we can open the link in a new tab because
// the window.open command occurs in the same stack frame as the onClick callback.
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
return;
}
// Go fetch a scalar token
const scalarClient = new ScalarAuthClient();
const integrationManager = managers.getPrimaryManager();
const scalarClient = integrationManager.getScalarClient();
scalarClient.connect().then(() => {
const completeUrl = scalarClient.getStarterLink(starterLink);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const integrationsUrl = SdkConfig.get().integrations_ui_url;
const integrationsUrl = integrationManager.uiUrl;
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
title: _t("Add an Integration"),
description:

View file

@ -14,14 +14,13 @@ 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 TextForEvent = require('../../../TextForEvent');
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'TextualEvent',
propTypes: {

View file

@ -14,12 +14,11 @@ 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 { _t } from '../../../languageHandler';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'UnknownBody',
render: function() {

View file

@ -13,15 +13,13 @@ 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 Promise from 'bluebird';
const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
const sdk = require('../../../index');
const Tinter = require('../../../Tinter');
const MatrixClientPeg = require("../../../MatrixClientPeg");
const Modal = require("../../../Modal");
import Tinter from '../../../Tinter';
import dis from '../../../dispatcher';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
@ -43,7 +41,7 @@ const ROOM_COLORS = [
// has a high possibility of being used in the nearish future.
// Ref: https://github.com/vector-im/riot-web/issues/8421
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'ColorSettings',
propTypes: {

View file

@ -16,16 +16,17 @@ 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';
const sdk = require("../../../index");
import createReactClass from 'create-react-class';
import sdk from "../../../index";
import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import dis from "../../../dispatcher";
import MatrixClientPeg from "../../../MatrixClientPeg";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'UrlPreviewSettings',
propTypes: {

View file

@ -15,10 +15,9 @@ 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 MatrixClientPeg from '../../../MatrixClientPeg';
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
@ -29,12 +28,13 @@ import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
import { showIntegrationsManager } from '../../../integrations/integrations';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'AppsDrawer',
propTypes: {
@ -128,10 +128,11 @@ module.exports = React.createClass({
},
_launchManageIntegrations: function() {
showIntegrationsManager({
room: this.props.room,
screen: 'add_integ',
});
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll();
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
}
},
onClickAddWidget: function(e) {

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 MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
@ -28,7 +29,7 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'AuxPanel',
propTypes: {

View file

@ -14,25 +14,69 @@ 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 classNames from 'classnames';
import React from 'react';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import HistoryManager from '../../../editor/history';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
import {setSelection} from '../../../editor/caret';
import {
formatRangeAsQuote,
formatRangeAsCode,
toggleInlineFormat,
replaceRangeAndMoveCaret,
} from '../../../editor/operations';
import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom';
import Autocomplete from '../rooms/Autocomplete';
import {autoCompleteCreator} from '../../../editor/parts';
import {parsePlainTextMessage} from '../../../editor/deserialize';
import {renderModel} from '../../../editor/render';
import {Room} from 'matrix-js-sdk';
import TypingStore from "../../../stores/TypingStore";
import EMOJIBASE from 'emojibase-data/en/compact.json';
import SettingsStore from "../../../settings/SettingsStore";
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import sdk from '../../../index';
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
function ctrlShortcutLabel(key) {
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
}
function cloneSelection(selection) {
return {
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset,
isCollapsed: selection.isCollapsed,
rangeCount: selection.rangeCount,
type: selection.type,
};
}
function selectionEquals(a: Selection, b: Selection): boolean {
return a.anchorNode === b.anchorNode &&
a.anchorOffset === b.anchorOffset &&
a.focusNode === b.focusNode &&
a.focusOffset === b.focusOffset &&
a.isCollapsed === b.isCollapsed &&
a.rangeCount === b.rangeCount &&
a.type === b.type;
}
export default class BasicMessageEditor extends React.Component {
static propTypes = {
onChange: PropTypes.func,
model: PropTypes.instanceOf(EditorModel).isRequired,
room: PropTypes.instanceOf(Room).isRequired,
placeholder: PropTypes.string,
label: PropTypes.string, // the aria label
initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js
};
constructor(props, context) {
@ -42,23 +86,111 @@ export default class BasicMessageEditor extends React.Component {
};
this._editorRef = null;
this._autocompleteRef = null;
this._formatBarRef = null;
this._modifiedFlag = false;
this._isIMEComposing = false;
this._hasTextSelected = false;
}
_updateEditorState = (caret, inputType, diff) => {
_replaceEmoticon = (caretPosition, inputType, diff) => {
const {model} = this.props;
const range = model.startRange(caretPosition);
// expand range max 8 characters backwards from caretPosition,
// as a space to look for an emoticon
let n = 8;
range.expandBackwardsWhile((index, offset) => {
const part = model.parts[index];
n -= 1;
return n >= 0 && (part.type === "plain" || part.type === "pill-candidate");
});
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
if (emoticonMatch) {
const query = emoticonMatch[1].toLowerCase().replace("-", "");
const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false);
if (data) {
const {partCreator} = model;
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
// we need the range to only comprise of the emoticon
// because we'll replace the whole range with an emoji,
// so move the start forward to the start of the emoticon.
// Take + 1 because index is reported without the possible preceding space.
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
// this returns the amount of added/removed characters during the replace
// so the caret position can be adjusted.
return range.replace([partCreator.plain(data.unicode + " ")]);
}
}
}
_updateEditorState = (selection, inputType, diff) => {
renderModel(this._editorRef, this.props.model);
if (caret) {
if (selection) { // set the caret/selection
try {
setCaretPosition(this._editorRef, this.props.model, caret);
setSelection(this._editorRef, this.props.model, selection);
} catch (err) {
console.error(err);
}
// if caret selection is a range, take the end position
const position = selection.end || selection;
this._setLastCaretFromPosition(position);
}
if (this.props.placeholder) {
const {isEmpty} = this.props.model;
if (isEmpty) {
this._showPlaceholder();
} else {
this._hidePlaceholder();
}
}
this.setState({autoComplete: this.props.model.autoComplete});
this.historyManager.tryPush(this.props.model, caret, inputType, diff);
this.historyManager.tryPush(this.props.model, selection, inputType, diff);
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty);
if (this.props.onChange) {
this.props.onChange();
}
}
_showPlaceholder() {
this._editorRef.style.setProperty("--placeholder", `'${this.props.placeholder}'`);
this._editorRef.classList.add("mx_BasicMessageComposer_inputEmpty");
}
_hidePlaceholder() {
this._editorRef.classList.remove("mx_BasicMessageComposer_inputEmpty");
this._editorRef.style.removeProperty("--placeholder");
}
_onCompositionStart = (event) => {
this._isIMEComposing = true;
// even if the model is empty, the composition text shouldn't be mixed with the placeholder
this._hidePlaceholder();
}
_onCompositionEnd = (event) => {
this._isIMEComposing = false;
// some browsers (chromium) don't fire an input event after ending a composition
// so trigger a model update after the composition is done by calling the input handler
this._onInput({inputType: "insertCompositionText"});
}
_onPaste = (event) => {
const {model} = this.props;
const {partCreator} = model;
const text = event.clipboardData.getData("text/plain");
if (text) {
const range = getRangeForSelection(this._editorRef, model, document.getSelection());
const parts = parsePlainTextMessage(text, partCreator);
replaceRangeAndMoveCaret(range, parts);
event.preventDefault();
}
}
_onInput = (event) => {
// ignore any input while doing IME compositions
if (this._isIMEComposing) {
return;
}
this._modifiedFlag = true;
const sel = document.getSelection();
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
@ -71,24 +203,104 @@ export default class BasicMessageEditor extends React.Component {
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
caret.offset += textToInsert.length;
this.props.model.update(newText, inputType, caret);
this._modifiedFlag = true;
}
_isCaretAtStart() {
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === 0;
// this is used later to see if we need to recalculate the caret
// on selectionchange. If it is just a consequence of typing
// we don't need to. But if the user is navigating the caret without input
// we need to recalculate it, to be able to know where to insert content after
// losing focus
_setLastCaretFromPosition(position) {
const {model} = this.props;
this._isCaretAtEnd = position.isAtEnd(model);
this._lastCaret = position.asOffset(model);
this._lastSelection = cloneSelection(document.getSelection());
}
_isCaretAtEnd() {
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === text.length;
_refreshLastCaretIfNeeded() {
// XXX: needed when going up and down in editing messages ... not sure why yet
// because the editors should stop doing this when when blurred ...
// maybe it's on focus and the _editorRef isn't available yet or something.
if (!this._editorRef) {
return;
}
const selection = document.getSelection();
if (!this._lastSelection || !selectionEquals(this._lastSelection, selection)) {
this._lastSelection = cloneSelection(selection);
const {caret, text} = getCaretOffsetAndText(this._editorRef, selection);
this._lastCaret = caret;
this._isCaretAtEnd = caret.offset === text.length;
}
return this._lastCaret;
}
clearUndoHistory() {
this.historyManager.clear();
}
getCaret() {
return this._lastCaret;
}
isSelectionCollapsed() {
return !this._lastSelection || this._lastSelection.isCollapsed;
}
isCaretAtStart() {
return this.getCaret().offset === 0;
}
isCaretAtEnd() {
return this._isCaretAtEnd;
}
_onBlur = () => {
document.removeEventListener("selectionchange", this._onSelectionChange);
}
_onFocus = () => {
document.addEventListener("selectionchange", this._onSelectionChange);
// force to recalculate
this._lastSelection = null;
this._refreshLastCaretIfNeeded();
}
_onSelectionChange = () => {
this._refreshLastCaretIfNeeded();
const selection = document.getSelection();
if (this._hasTextSelected && selection.isCollapsed) {
this._hasTextSelected = false;
if (this._formatBarRef) {
this._formatBarRef.hide();
}
} else if (!selection.isCollapsed) {
this._hasTextSelected = true;
if (this._formatBarRef) {
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
this._formatBarRef.showAt(selectionRect);
}
}
}
_onKeyDown = (event) => {
const model = this.props.model;
const modKey = IS_MAC ? event.metaKey : event.ctrlKey;
let handled = false;
// format bold
if (modKey && event.key === "b") {
this._onFormatAction("bold");
handled = true;
// format italics
} else if (modKey && event.key === "i") {
this._onFormatAction("italics");
handled = true;
// format quote
} else if (modKey && event.key === ">") {
this._onFormatAction("quote");
handled = true;
// undo
if (modKey && event.key === "z") {
} else if (modKey && event.key === "z") {
if (this.historyManager.canUndo()) {
const {parts, caret} = this.historyManager.undo(this.props.model);
// pass matching inputType so historyManager doesn't push echo
@ -106,27 +318,35 @@ export default class BasicMessageEditor extends React.Component {
}
handled = true;
// insert newline on Shift+Enter
} else if (event.shiftKey && event.key === "Enter") {
} else if (event.key === "Enter" && (event.shiftKey || (IS_MAC && event.altKey))) {
this._insertText("\n");
handled = true;
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
} else if (!(event.metaKey || event.altKey || event.shiftKey)) {
if (model.autoComplete) {
if (model.autoComplete && model.autoComplete.hasCompletions()) {
const autoComplete = model.autoComplete;
switch (event.key) {
case "Enter":
autoComplete.onEnter(event); break;
case "ArrowUp":
autoComplete.onUpArrow(event); break;
autoComplete.onUpArrow(event);
handled = true;
break;
case "ArrowDown":
autoComplete.onDownArrow(event); break;
autoComplete.onDownArrow(event);
handled = true;
break;
case "Tab":
autoComplete.onTab(event); break;
autoComplete.onTab(event);
handled = true;
break;
case "Escape":
autoComplete.onEscape(event); break;
autoComplete.onEscape(event);
handled = true;
break;
default:
return; // don't preventDefault on anything else
}
} else if (event.key === "Tab") {
this._tabCompleteName();
handled = true;
}
}
@ -136,9 +356,38 @@ export default class BasicMessageEditor extends React.Component {
}
}
_cancelEdit = () => {
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
async _tabCompleteName() {
try {
await new Promise(resolve => this.setState({showVisualBell: false}, resolve));
const {model} = this.props;
const caret = this.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
const range = model.startRange(position);
range.expandBackwardsWhile((index, offset, part) => {
return part.text[offset] !== " " && (
part.type === "plain" ||
part.type === "pill-candidate" ||
part.type === "command"
);
});
const {partCreator} = model;
// await for auto-complete to be open
await model.transform(() => {
const addedLen = range.replace([partCreator.pillCandidate(range.text)]);
return model.positionForOffset(caret.offset + addedLen, true);
});
await model.autoComplete.onTab();
if (!model.autoComplete.hasSelection()) {
this.setState({showVisualBell: true});
model.autoComplete.close();
}
} catch (err) {
console.error(err);
}
}
getEditableRootNode() {
return this._editorRef;
}
isModified() {
@ -155,17 +404,22 @@ export default class BasicMessageEditor extends React.Component {
componentWillUnmount() {
this._editorRef.removeEventListener("input", this._onInput, true);
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
}
componentDidMount() {
const model = this.props.model;
model.setUpdateCallback(this._updateEditorState);
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
model.setTransformCallback(this._replaceEmoticon);
}
const partCreator = model.partCreator;
// TODO: does this allow us to get rid of EditorStateTransfer?
// not really, but we could not serialize the parts, and just change the autoCompleter
partCreator.setAutoCompleteCreator(autoCompleteCreator(
() => this._autocompleteRef,
query => this.setState({query}),
query => new Promise(resolve => this.setState({query}, resolve)),
));
this.historyManager = new HistoryManager(partCreator);
// initial render of model
@ -173,6 +427,8 @@ export default class BasicMessageEditor extends React.Component {
// attach input listener by hand so React doesn't proxy the events,
// as the proxied event doesn't support inputType, which we need.
this._editorRef.addEventListener("input", this._onInput, true);
this._editorRef.addEventListener("compositionstart", this._onCompositionStart, true);
this._editorRef.addEventListener("compositionend", this._onCompositionEnd, true);
this._editorRef.focus();
}
@ -190,15 +446,32 @@ export default class BasicMessageEditor extends React.Component {
return caretPosition;
}
isCaretAtStart() {
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === 0;
}
isCaretAtEnd() {
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === text.length;
_onFormatAction = (action) => {
const range = getRangeForSelection(
this._editorRef,
this.props.model,
document.getSelection());
if (range.length === 0) {
return;
}
this.historyManager.ensureLastChangesPushed(this.props.model);
switch (action) {
case "bold":
toggleInlineFormat(range, "**");
break;
case "italics":
toggleInlineFormat(range, "_");
break;
case "strikethrough":
toggleInlineFormat(range, "<del>", "</del>");
break;
case "code":
formatRangeAsCode(range);
break;
case "quote":
formatRangeAsQuote(range);
break;
}
}
render() {
@ -206,7 +479,7 @@ export default class BasicMessageEditor extends React.Component {
if (this.state.autoComplete) {
const query = this.state.query;
const queryLen = query.length;
autoComplete = <div className="mx_MessageEditor_AutoCompleteWrapper">
autoComplete = (<div className="mx_BasicMessageComposer_AutoCompleteWrapper">
<Autocomplete
ref={ref => this._autocompleteRef = ref}
query={query}
@ -215,18 +488,37 @@ export default class BasicMessageEditor extends React.Component {
selection={{beginning: true, end: queryLen, start: queryLen}}
room={this.props.room}
/>
</div>;
</div>);
}
return <div className={this.props.className}>
{ autoComplete }
<div
className="mx_MessageEditor_editor"
contentEditable="true"
tabIndex="1"
onKeyDown={this._onKeyDown}
ref={ref => this._editorRef = ref}
aria-label={_t("Edit message")}
></div>
</div>;
const classes = classNames("mx_BasicMessageComposer", {
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
});
const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar');
const shortcuts = {
bold: ctrlShortcutLabel("B"),
italics: ctrlShortcutLabel("I"),
quote: ctrlShortcutLabel(">"),
};
return (<div className={classes}>
{ autoComplete }
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
<div
className="mx_BasicMessageComposer_input"
contentEditable="true"
tabIndex="1"
onBlur={this._onBlur}
onFocus={this._onFocus}
onPaste={this._onPaste}
onKeyDown={this._onKeyDown}
ref={ref => this._editorRef = ref}
aria-label={this.props.label}
></div>
</div>);
}
focus() {
this._editorRef.focus();
}
}

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