Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering

This commit is contained in:
Tulir Asokan 2020-09-04 12:41:38 +03:00
commit 75a0178dad
400 changed files with 12445 additions and 9279 deletions

View file

@ -18,16 +18,13 @@ limitations under the License.
import { _t } from '../../../languageHandler';
import React from 'react';
import createReactClass from 'create-react-class';
export default createReactClass({
displayName: 'AuthFooter',
render: function() {
export default class AuthFooter extends React.Component {
render() {
return (
<div className="mx_AuthFooter">
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
</div>
);
},
});
}
}

View file

@ -17,17 +17,14 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'AuthHeader',
propTypes: {
export default class AuthHeader extends React.Component {
static propTypes = {
disableLanguageSelector: PropTypes.bool,
},
};
render: function() {
render() {
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
@ -37,5 +34,5 @@ export default createReactClass({
<LanguageSelector disabled={this.props.disableLanguageSelector} />
</div>
);
},
});
}
}

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
@ -24,36 +23,31 @@ const DIV_ID = 'mx_recaptcha';
/**
* A pure UI component which displays a captcha form.
*/
export default createReactClass({
displayName: 'CaptchaForm',
propTypes: {
export default class CaptchaForm extends React.Component {
static propTypes = {
sitePublicKey: PropTypes.string,
// called with the captcha response
onCaptchaResponse: PropTypes.func,
},
};
getDefaultProps: function() {
return {
onCaptchaResponse: () => {},
};
},
static defaultProps = {
onCaptchaResponse: () => {},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
errorText: null,
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._captchaWidgetId = null;
this._recaptchaContainer = createRef();
},
}
componentDidMount: function() {
componentDidMount() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead.
if (global.grecaptcha) {
@ -68,13 +62,13 @@ export default createReactClass({
);
this._recaptchaContainer.current.appendChild(scriptTag);
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this._resetRecaptcha();
},
}
_renderRecaptcha: function(divId) {
_renderRecaptcha(divId) {
if (!global.grecaptcha) {
console.error("grecaptcha not loaded!");
throw new Error("Recaptcha did not load successfully");
@ -93,15 +87,15 @@ export default createReactClass({
sitekey: publicKey,
callback: this.props.onCaptchaResponse,
});
},
}
_resetRecaptcha: function() {
_resetRecaptcha() {
if (this._captchaWidgetId !== null) {
global.grecaptcha.reset(this._captchaWidgetId);
}
},
}
_onCaptchaLoaded: function() {
_onCaptchaLoaded() {
console.log("Loaded recaptcha script.");
try {
this._renderRecaptcha(DIV_ID);
@ -110,9 +104,9 @@ export default createReactClass({
errorText: e.toString(),
});
}
},
}
render: function() {
render() {
let error = null;
if (this.state.errorText) {
error = (
@ -131,5 +125,5 @@ export default createReactClass({
{ error }
</div>
);
},
});
}
}

View file

@ -16,14 +16,11 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default createReactClass({
displayName: 'CustomServerDialog',
render: function() {
export default class CustomServerDialog extends React.Component {
render() {
const brand = SdkConfig.get().brand;
return (
<div className="mx_ErrorDialog">
@ -46,5 +43,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import url from 'url';
import classnames from 'classnames';
@ -75,14 +74,10 @@ import AccessibleButton from "../elements/AccessibleButton";
export const DEFAULT_PHASE = 0;
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
export class PasswordAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.password";
statics: {
LOGIN_TYPE: "m.login.password",
},
propTypes: {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
@ -90,26 +85,24 @@ export const PasswordAuthEntry = createReactClass({
// happen?
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
}
getInitialState: function() {
return {
password: "",
};
},
state = {
password: "",
};
_onSubmit: function(e) {
_onSubmit = e => {
e.preventDefault();
if (this.props.busy) return;
this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE,
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
// See https://github.com/vector-im/element-web/issues/10312
user: this.props.matrixClient.credentials.userId,
identifier: {
type: "m.id.user",
@ -117,16 +110,16 @@ export const PasswordAuthEntry = createReactClass({
},
password: this.state.password,
});
},
};
_onPasswordFieldChange: function(ev) {
_onPasswordFieldChange = ev => {
// enable the submit button iff the password is non-empty
this.setState({
password: ev.target.value,
});
},
};
render: function() {
render() {
const passwordBoxClass = classnames({
"error": this.props.errorText,
});
@ -176,36 +169,32 @@ export const PasswordAuthEntry = createReactClass({
{ errorSection }
</div>
);
},
});
}
}
export const RecaptchaAuthEntry = createReactClass({
displayName: 'RecaptchaAuthEntry',
export class RecaptchaAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.recaptcha";
statics: {
LOGIN_TYPE: "m.login.recaptcha",
},
propTypes: {
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
}
_onCaptchaResponse: function(response) {
_onCaptchaResponse = response => {
this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE,
response: response,
});
},
};
render: function() {
render() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
@ -241,31 +230,24 @@ export const RecaptchaAuthEntry = createReactClass({
{ errorSection }
</div>
);
},
});
}
}
export const TermsAuthEntry = createReactClass({
displayName: 'TermsAuthEntry',
export class TermsAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.terms";
statics: {
LOGIN_TYPE: "m.login.terms",
},
propTypes: {
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Move this to constructor
componentWillMount: function() {
// example stageParams:
//
// {
@ -310,17 +292,22 @@ export const TermsAuthEntry = createReactClass({
pickedPolicies.push(langPolicy);
}
this.setState({
"toggledPolicies": initToggles,
"policies": pickedPolicies,
});
},
this.state = {
toggledPolicies: initToggles,
policies: pickedPolicies,
};
}
tryContinue: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
tryContinue = () => {
this._trySubmit();
},
};
_togglePolicy: function(policyId) {
_togglePolicy(policyId) {
const newToggles = {};
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
@ -329,9 +316,9 @@ export const TermsAuthEntry = createReactClass({
newToggles[policy.id] = checked;
}
this.setState({"toggledPolicies": newToggles});
},
}
_trySubmit: function() {
_trySubmit = () => {
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
@ -340,9 +327,9 @@ export const TermsAuthEntry = createReactClass({
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
},
};
render: function() {
render() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
@ -387,17 +374,13 @@ export const TermsAuthEntry = createReactClass({
{ submitButton }
</div>
);
},
});
}
}
export const EmailIdentityAuthEntry = createReactClass({
displayName: 'EmailIdentityAuthEntry',
export class EmailIdentityAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.email.identity";
statics: {
LOGIN_TYPE: "m.login.email.identity",
},
propTypes: {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
authSessionId: PropTypes.string.isRequired,
@ -407,13 +390,13 @@ export const EmailIdentityAuthEntry = createReactClass({
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
}
render: function() {
render() {
// This component is now only displayed once the token has been requested,
// so we know the email has been sent. It can also get loaded after the user
// has clicked the validation link if the server takes a while to propagate
@ -434,17 +417,13 @@ export const EmailIdentityAuthEntry = createReactClass({
</div>
);
}
},
});
}
}
export const MsisdnAuthEntry = createReactClass({
displayName: 'MsisdnAuthEntry',
export class MsisdnAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.msisdn";
statics: {
LOGIN_TYPE: "m.login.msisdn",
},
propTypes: {
static propTypes = {
inputs: PropTypes.shape({
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
@ -454,16 +433,14 @@ export const MsisdnAuthEntry = createReactClass({
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
onPhaseChange: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
token: '',
requestingToken: false,
};
},
state = {
token: '',
requestingToken: false,
};
componentDidMount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
this._submitUrl = null;
@ -477,12 +454,12 @@ export const MsisdnAuthEntry = createReactClass({
}).finally(() => {
this.setState({requestingToken: false});
});
},
}
/*
* Requests a verification token by SMS.
*/
_requestMsisdnToken: function() {
_requestMsisdnToken() {
return this.props.matrixClient.requestRegisterMsisdnToken(
this.props.inputs.phoneCountry,
this.props.inputs.phoneNumber,
@ -493,15 +470,15 @@ export const MsisdnAuthEntry = createReactClass({
this._sid = result.sid;
this._msisdn = result.msisdn;
});
},
}
_onTokenChange: function(e) {
_onTokenChange = e => {
this.setState({
token: e.target.value,
});
},
};
_onFormSubmit: async function(e) {
_onFormSubmit = async e => {
e.preventDefault();
if (this.state.token == '') return;
@ -538,7 +515,7 @@ export const MsisdnAuthEntry = createReactClass({
this.props.submitAuthDict({
type: MsisdnAuthEntry.LOGIN_TYPE,
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
// See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
@ -552,9 +529,9 @@ export const MsisdnAuthEntry = createReactClass({
this.props.fail(e);
console.log("Failed to submit msisdn token");
}
},
};
render: function() {
render() {
if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
@ -598,8 +575,8 @@ export const MsisdnAuthEntry = createReactClass({
</div>
);
}
},
});
}
}
export class SSOAuthEntry extends React.Component {
static propTypes = {
@ -686,46 +663,46 @@ export class SSOAuthEntry extends React.Component {
}
}
export const FallbackAuthEntry = createReactClass({
displayName: 'FallbackAuthEntry',
propTypes: {
export class FallbackAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
},
};
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// we have to make the user click a button, as browsers will block
// the popup if we open it immediately.
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
this._fallbackButton = createRef();
},
}
componentWillUnmount: function() {
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage);
if (this._popupWindow) {
this._popupWindow.close();
}
},
}
focus: function() {
focus = () => {
if (this._fallbackButton.current) {
this._fallbackButton.current.focus();
}
},
};
_onShowFallbackClick: function(e) {
_onShowFallbackClick = e => {
e.preventDefault();
e.stopPropagation();
@ -735,18 +712,18 @@ export const FallbackAuthEntry = createReactClass({
);
this._popupWindow = window.open(url);
this._popupWindow.opener = null;
},
};
_onReceiveMessage: function(event) {
_onReceiveMessage = event => {
if (
event.data === "authDone" &&
event.origin === this.props.matrixClient.getHomeserverUrl()
) {
this.props.submitAuthDict({});
}
},
};
render: function() {
render() {
let errorSection;
if (this.props.errorText) {
errorSection = (
@ -761,8 +738,8 @@ export const FallbackAuthEntry = createReactClass({
{errorSection}
</div>
);
},
});
}
}
const AuthEntryComponents = [
PasswordAuthEntry,

View file

@ -18,7 +18,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as Email from '../../../email';
@ -39,13 +38,11 @@ const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
/**
/*
* A pure UI component which displays a registration form.
*/
export default createReactClass({
displayName: 'RegistrationForm',
propTypes: {
export default class RegistrationForm extends React.Component {
static propTypes = {
// Values pre-filled in the input boxes when the component loads
defaultEmail: PropTypes.string,
defaultPhoneCountry: PropTypes.string,
@ -58,17 +55,17 @@ export default createReactClass({
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
canSubmit: PropTypes.bool,
serverRequiresIdServer: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
onValidationChange: console.error,
canSubmit: true,
};
},
static defaultProps = {
onValidationChange: console.error,
canSubmit: true,
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
// Field error codes by field ID
fieldValid: {},
// The ISO2 country code selected in the phone number entry
@ -80,9 +77,9 @@ export default createReactClass({
passwordConfirm: this.props.defaultPassword || "",
passwordComplexity: null,
};
},
}
onSubmit: async function(ev) {
onSubmit = async ev => {
ev.preventDefault();
if (!this.props.canSubmit) return;
@ -118,7 +115,7 @@ export default createReactClass({
title: _t("Warning!"),
description: desc,
button: _t("Continue"),
onFinished: function(confirmed) {
onFinished(confirmed) {
if (confirmed) {
self._doSubmit(ev);
}
@ -127,9 +124,9 @@ export default createReactClass({
} else {
self._doSubmit(ev);
}
},
};
_doSubmit: function(ev) {
_doSubmit(ev) {
const email = this.state.email.trim();
const promise = this.props.onRegisterClick({
username: this.state.username.trim(),
@ -145,7 +142,7 @@ export default createReactClass({
ev.target.disabled = false;
});
}
},
}
async verifyFieldsBeforeSubmit() {
// Blur the active element if any, so we first run its blur validation,
@ -196,12 +193,12 @@ export default createReactClass({
invalidField.focus();
invalidField.validate({ allowEmpty: false, focused: true });
return false;
},
}
/**
* @returns {boolean} true if all fields were valid last time they were validated.
*/
allFieldsValid: function() {
allFieldsValid() {
const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) {
if (!this.state.fieldValid[keys[i]]) {
@ -209,7 +206,7 @@ export default createReactClass({
}
}
return true;
},
}
findFirstInvalidField(fieldIDs) {
for (const fieldID of fieldIDs) {
@ -218,34 +215,34 @@ export default createReactClass({
}
}
return null;
},
}
markFieldValid: function(fieldID, valid) {
markFieldValid(fieldID, valid) {
const { fieldValid } = this.state;
fieldValid[fieldID] = valid;
this.setState({
fieldValid,
});
},
}
onEmailChange(ev) {
onEmailChange = ev => {
this.setState({
email: ev.target.value,
});
},
};
async onEmailValidate(fieldState) {
onEmailValidate = async fieldState => {
const result = await this.validateEmailRules(fieldState);
this.markFieldValid(FIELD_EMAIL, result.valid);
return result;
},
};
validateEmailRules: withValidation({
validateEmailRules = withValidation({
description: () => _t("Use an email address to recover your account"),
rules: [
{
key: "required",
test: function({ value, allowEmpty }) {
test({ value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value;
},
invalid: () => _t("Enter email address (required on this homeserver)"),
@ -256,31 +253,31 @@ export default createReactClass({
invalid: () => _t("Doesn't look like a valid email address"),
},
],
}),
});
onPasswordChange(ev) {
onPasswordChange = ev => {
this.setState({
password: ev.target.value,
});
},
};
onPasswordValidate(result) {
onPasswordValidate = result => {
this.markFieldValid(FIELD_PASSWORD, result.valid);
},
};
onPasswordConfirmChange(ev) {
onPasswordConfirmChange = ev => {
this.setState({
passwordConfirm: ev.target.value,
});
},
};
async onPasswordConfirmValidate(fieldState) {
onPasswordConfirmValidate = async fieldState => {
const result = await this.validatePasswordConfirmRules(fieldState);
this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid);
return result;
},
};
validatePasswordConfirmRules: withValidation({
validatePasswordConfirmRules = withValidation({
rules: [
{
key: "required",
@ -289,39 +286,39 @@ export default createReactClass({
},
{
key: "match",
test: function({ value }) {
test({ value }) {
return !value || value === this.state.password;
},
invalid: () => _t("Passwords don't match"),
},
],
}),
});
onPhoneCountryChange(newVal) {
onPhoneCountryChange = newVal => {
this.setState({
phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
});
},
};
onPhoneNumberChange(ev) {
onPhoneNumberChange = ev => {
this.setState({
phoneNumber: ev.target.value,
});
},
};
async onPhoneNumberValidate(fieldState) {
onPhoneNumberValidate = async fieldState => {
const result = await this.validatePhoneNumberRules(fieldState);
this.markFieldValid(FIELD_PHONE_NUMBER, result.valid);
return result;
},
};
validatePhoneNumberRules: withValidation({
validatePhoneNumberRules = withValidation({
description: () => _t("Other users can invite you to rooms using your contact details"),
rules: [
{
key: "required",
test: function({ value, allowEmpty }) {
test({ value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value;
},
invalid: () => _t("Enter phone number (required on this homeserver)"),
@ -332,21 +329,21 @@ export default createReactClass({
invalid: () => _t("Doesn't look like a valid phone number"),
},
],
}),
});
onUsernameChange(ev) {
onUsernameChange = ev => {
this.setState({
username: ev.target.value,
});
},
};
async onUsernameValidate(fieldState) {
onUsernameValidate = async fieldState => {
const result = await this.validateUsernameRules(fieldState);
this.markFieldValid(FIELD_USERNAME, result.valid);
return result;
},
};
validateUsernameRules: withValidation({
validateUsernameRules = withValidation({
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
rules: [
{
@ -360,7 +357,7 @@ export default createReactClass({
invalid: () => _t("Some characters not allowed"),
},
],
}),
});
/**
* A step is required if all flows include that step.
@ -372,7 +369,7 @@ export default createReactClass({
return this.props.flows.every((flow) => {
return flow.stages.includes(step);
});
},
}
/**
* A step is used if any flows include that step.
@ -384,7 +381,7 @@ export default createReactClass({
return this.props.flows.some((flow) => {
return flow.stages.includes(step);
});
},
}
_showEmail() {
const haveIs = Boolean(this.props.serverConfig.isUrl);
@ -395,7 +392,7 @@ export default createReactClass({
return false;
}
return true;
},
}
_showPhoneNumber() {
const threePidLogin = !SdkConfig.get().disable_3pid_login;
@ -408,7 +405,7 @@ export default createReactClass({
return false;
}
return true;
},
}
renderEmail() {
if (!this._showEmail()) {
@ -426,7 +423,7 @@ export default createReactClass({
onChange={this.onEmailChange}
onValidate={this.onEmailValidate}
/>;
},
}
renderPassword() {
return <PassphraseField
@ -437,7 +434,7 @@ export default createReactClass({
onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate}
/>;
},
}
renderPasswordConfirm() {
const Field = sdk.getComponent('elements.Field');
@ -451,7 +448,7 @@ export default createReactClass({
onChange={this.onPasswordConfirmChange}
onValidate={this.onPasswordConfirmValidate}
/>;
},
}
renderPhoneNumber() {
if (!this._showPhoneNumber()) {
@ -477,7 +474,7 @@ export default createReactClass({
onChange={this.onPhoneNumberChange}
onValidate={this.onPhoneNumberValidate}
/>;
},
}
renderUsername() {
const Field = sdk.getComponent('elements.Field');
@ -491,9 +488,9 @@ export default createReactClass({
onChange={this.onUsernameChange}
onValidate={this.onUsernameValidate}
/>;
},
}
render: function() {
render() {
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
@ -578,5 +575,5 @@ export default createReactClass({
</form>
</div>
);
},
});
}
}

View file

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useContext, useEffect, useState} from 'react';
import classNames from 'classnames';
import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
@ -42,34 +42,35 @@ interface IProps {
className?: string;
}
const calculateUrls = (url, urls) => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = urls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
// deduplicate URLs
return Array.from(new Set(_urls));
};
const useImageUrl = ({url, urls}): [string, () => void] => {
const [imageUrls, setUrls] = useState<string[]>([]);
const [urlsIndex, setIndex] = useState<number>();
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
const [urlsIndex, setIndex] = useState<number>(0);
const onError = useCallback(() => {
setIndex(i => i + 1); // try the next one
}, []);
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = memoizedUrls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
// deduplicate URLs
_urls = Array.from(new Set(_urls));
setUrls(calculateUrls(url, urls));
setIndex(0);
setUrls(_urls);
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState, prevState) => {
@ -95,7 +96,7 @@ const BaseAvatar = (props: IProps) => {
urls,
width = 40,
height = 40,
resizeMethod = "crop", // eslint-disable-line no-unused-vars
resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
defaultToInitialLetter = true,
onClick,
inputRef,

View file

@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar";
import RoomTileIcon from "../rooms/RoomTileIcon";
import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import {isPresenceEnabled} from "../../../utils/presence";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";
interface IProps {
room: Room;
@ -36,18 +43,134 @@ interface IProps {
interface IState {
notificationState?: NotificationState;
icon: Icon;
}
enum Icon {
// Note: the names here are used in CSS class names
None = "NONE", // ... except this one
Globe = "GLOBE",
PresenceOnline = "ONLINE",
PresenceAway = "AWAY",
PresenceOffline = "OFFLINE",
}
function tooltipText(variant: Icon) {
switch (variant) {
case Icon.Globe:
return _t("This room is public");
case Icon.PresenceOnline:
return _t("Online");
case Icon.PresenceAway:
return _t("Away");
case Icon.PresenceOffline:
return _t("Offline");
}
}
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User;
private isUnmounted = false;
private isWatchingTimeline = false;
constructor(props: IProps) {
super(props);
this.state = {
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
icon: this.calculateIcon(),
};
}
public componentWillUnmount() {
this.isUnmounted = true;
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
this.dmUser = null; // clear listeners, if any
}
private get isPublicRoom(): boolean {
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
return joinRule === 'public';
}
private get dmUser(): User {
return this._dmUser;
}
private set dmUser(val: User) {
const oldUser = this._dmUser;
this._dmUser = val;
if (oldUser && oldUser !== this._dmUser) {
oldUser.off('User.currentlyActive', this.onPresenceUpdate);
oldUser.off('User.presence', this.onPresenceUpdate);
}
if (this._dmUser && oldUser !== this._dmUser) {
this._dmUser.on('User.currentlyActive', this.onPresenceUpdate);
this._dmUser.on('User.presence', this.onPresenceUpdate);
}
}
private onRoomTimeline = (ev: MatrixEvent, room: Room) => {
if (this.isUnmounted) return;
// apparently these can happen?
if (!room) return;
if (this.props.room.roomId !== room.roomId) return;
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
this.setState({icon: this.calculateIcon()});
}
};
private onPresenceUpdate = () => {
if (this.isUnmounted) return;
const newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
};
private getPresenceIcon(): Icon {
if (!this.dmUser) return Icon.None;
let icon = Icon.None;
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
if (isOnline) {
icon = Icon.PresenceOnline;
} else if (this.dmUser.presence === 'offline') {
icon = Icon.PresenceOffline;
} else if (this.dmUser.presence === 'unavailable') {
icon = Icon.PresenceAway;
}
return icon;
}
private calculateIcon(): Icon {
let icon = Icon.None;
// We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId && this.props.room.getJoinedMemberCount() === 2) {
// Track presence, if available
if (isPresenceEnabled()) {
if (otherUserId) {
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
icon = this.getPresenceIcon();
}
}
} else {
// Track publicity
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
if (!this.isWatchingTimeline) {
this.props.room.on('Room.timeline', this.onRoomTimeline);
this.isWatchingTimeline = true;
}
}
return icon;
}
public render(): React.ReactNode {
let badge: React.ReactNode;
if (this.props.displayBadge) {
@ -58,7 +181,19 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
/>;
}
return <div className="mx_DecoratedRoomAvatar">
let icon;
if (this.state.icon !== Icon.None) {
icon = <TextWithTooltip
tooltip={tooltipText(this.state.icon)}
class={`mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_${this.state.icon.toLowerCase()}`}
/>;
}
const classes = classNames("mx_DecoratedRoomAvatar", {
mx_DecoratedRoomAvatar_cutout: icon,
});
return <div className={classes}>
<RoomAvatar
room={this.props.room}
width={this.props.avatarSize}
@ -66,7 +201,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
oobData={this.props.oobData}
viewAvatarOnClick={this.props.viewAvatarOnClick}
/>
<RoomTileIcon room={this.props.room} />
{icon}
{badge}
</div>;
}

View file

@ -47,7 +47,7 @@ export default class GroupAvatar extends React.Component<IProps> {
render() {
// extract the props we use from props so we can pass any others through
// should consider adding this as a global rule in js-sdk?
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
return (

View file

@ -53,7 +53,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
if (!SettingsStore.getValue("feature_custom_status")) {
return;
}
const { user } = this.props.member;
@ -105,7 +105,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
resizeMethod={this.props.resizeMethod}
/>;
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
if (!SettingsStore.getValue("feature_custom_status")) {
return avatar;
}

View file

@ -25,4 +25,4 @@ const PulsedAvatar: React.FC<IProps> = (props) => {
</div>;
};
export default PulsedAvatar;
export default PulsedAvatar;

View file

@ -114,9 +114,12 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
}
private onRoomAvatarClick = () => {
const avatarUrl = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
null, null, null, false);
const avatarUrl = Avatar.avatarUrlForRoom(
this.props.room,
null,
null,
null,
);
const params = {
src: avatarUrl,
name: this.props.room.name,

View file

@ -0,0 +1,124 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import classNames from "classnames";
import {
ChevronFace,
ContextMenu,
IProps as IContextMenuProps,
MenuItem,
MenuItemCheckbox, MenuItemRadio,
} from "../../structures/ContextMenu";
interface IProps extends IContextMenuProps {
className?: string;
compact?: boolean;
}
interface IOptionListProps {
first?: boolean;
red?: boolean;
className?: string;
}
interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
iconClassName: string;
}
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
iconClassName: string;
}
interface IRadioProps extends React.ComponentProps<typeof MenuItemRadio> {
iconClassName: string;
}
export const IconizedContextMenuRadio: React.FC<IRadioProps> = ({
label,
iconClassName,
active,
className,
...props
}) => {
return <MenuItemRadio
{...props}
className={classNames(className, {
mx_IconizedContextMenu_active: active,
})}
active={active}
label={label}
>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{label}</span>
{active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
</MenuItemRadio>;
};
export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
label,
iconClassName,
active,
className,
...props
}) => {
return <MenuItemCheckbox
{...props}
className={classNames(className, {
mx_IconizedContextMenu_active: active,
})}
active={active}
label={label}
>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{label}</span>
{active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
</MenuItemCheckbox>;
};
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({label, iconClassName, ...props}) => {
return <MenuItem {...props} label={label}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{label}</span>
</MenuItem>;
};
export const IconizedContextMenuOptionList: React.FC<IOptionListProps> = ({first, red, className, children}) => {
const classes = classNames("mx_IconizedContextMenu_optionList", className, {
mx_IconizedContextMenu_optionList_notFirst: !first,
mx_IconizedContextMenu_optionList_red: red,
});
return <div className={classes}>
{children}
</div>;
};
const IconizedContextMenu: React.FC<IProps> = ({className, children, compact, ...props}) => {
const classes = classNames("mx_IconizedContextMenu", className, {
mx_IconizedContextMenu_compact: compact,
});
return <ContextMenu chevronFace={ChevronFace.None} {...props}>
<div className={classes}>
{ children }
</div>
</ContextMenu>;
};
export default IconizedContextMenu;

View file

@ -19,7 +19,6 @@ 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';
@ -37,10 +36,8 @@ function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
}
export default createReactClass({
displayName: 'MessageContextMenu',
propTypes: {
export default class MessageContextMenu extends React.Component {
static propTypes = {
/* the MatrixEvent associated with the context menu */
mxEvent: PropTypes.object.isRequired,
@ -52,28 +49,26 @@ export default createReactClass({
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
},
};
getInitialState: function() {
return {
canRedact: false,
canPin: false,
};
},
state = {
canRedact: false,
canPin: false,
};
componentDidMount: function() {
componentDidMount() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
this._checkPermissions();
},
}
componentWillUnmount: function() {
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
}
},
}
_checkPermissions: function() {
_checkPermissions = () => {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
@ -81,50 +76,50 @@ export default createReactClass({
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
if (!SettingsStore.isFeatureEnabled("feature_pinning")) canPin = false;
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
this.setState({canRedact, canPin});
},
};
_isPinned: function() {
_isPinned() {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
if (!pinnedEvent) return false;
const content = pinnedEvent.getContent();
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
},
}
onResendClick: function() {
onResendClick = () => {
Resend.resend(this.props.mxEvent);
this.closeMenu();
},
};
onResendEditClick: function() {
onResendEditClick = () => {
Resend.resend(this.props.mxEvent.replacingEvent());
this.closeMenu();
},
};
onResendRedactionClick: function() {
onResendRedactionClick = () => {
Resend.resend(this.props.mxEvent.localRedactionEvent());
this.closeMenu();
},
};
onResendReactionsClick: function() {
onResendReactionsClick = () => {
for (const reaction of this._getUnsentReactions()) {
Resend.resend(reaction);
}
this.closeMenu();
},
};
onReportEventClick: function() {
onReportEventClick = () => {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent');
this.closeMenu();
},
};
onViewSourceClick: function() {
onViewSourceClick = () => {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
@ -133,9 +128,9 @@ export default createReactClass({
content: ev.event,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
};
onViewClearSourceClick: function() {
onViewClearSourceClick = () => {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
@ -145,9 +140,9 @@ export default createReactClass({
content: ev._clearEvent,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
};
onRedactClick: function() {
onRedactClick = () => {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
onFinished: async (proceed) => {
@ -176,9 +171,9 @@ export default createReactClass({
},
}, 'mx_Dialog_confirmredact');
this.closeMenu();
},
};
onCancelSendClick: function() {
onCancelSendClick = () => {
const mxEvent = this.props.mxEvent;
const editEvent = mxEvent.replacingEvent();
const redactEvent = mxEvent.localRedactionEvent();
@ -199,17 +194,17 @@ export default createReactClass({
Resend.removeFromQueue(this.props.mxEvent);
}
this.closeMenu();
},
};
onForwardClick: function() {
onForwardClick = () => {
dis.dispatch({
action: 'forward_event',
event: this.props.mxEvent,
});
this.closeMenu();
},
};
onPinClick: function() {
onPinClick = () => {
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
.catch((e) => {
// Intercept the Event Not Found error and fall through the promise chain with no event.
@ -230,28 +225,28 @@ export default createReactClass({
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
});
this.closeMenu();
},
};
closeMenu: function() {
closeMenu = () => {
if (this.props.onFinished) this.props.onFinished();
},
};
onUnhidePreviewClick: function() {
onUnhidePreviewClick = () => {
if (this.props.eventTileOps) {
this.props.eventTileOps.unhideWidget();
}
this.closeMenu();
},
};
onQuoteClick: function() {
onQuoteClick = () => {
dis.dispatch({
action: 'quote',
event: this.props.mxEvent,
});
this.closeMenu();
},
};
onPermalinkClick: function(e: Event) {
onPermalinkClick = (e: Event) => {
e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
@ -259,12 +254,12 @@ export default createReactClass({
permalinkCreator: this.props.permalinkCreator,
});
this.closeMenu();
},
};
onCollapseReplyThreadClick: function() {
onCollapseReplyThreadClick = () => {
this.props.collapseReplyThread();
this.closeMenu();
},
};
_getReactions(filter) {
const cli = MatrixClientPeg.get();
@ -277,17 +272,17 @@ export default createReactClass({
relation.event_id === eventId &&
filter(e);
});
},
}
_getPendingReactions() {
return this._getReactions(e => canCancel(e.status));
},
}
_getUnsentReactions() {
return this._getReactions(e => e.status === EventStatus.NOT_SENT);
},
}
render: function() {
render() {
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
const mxEvent = this.props.mxEvent;
@ -489,5 +484,5 @@ export default createReactClass({
{ reportEventButton }
</div>
);
},
});
}
}

View file

@ -1,404 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 createReactClass from 'create-react-class';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import DMRoomMap from '../../../utils/DMRoomMap';
import * as Rooms from '../../../Rooms';
import * as RoomNotifs from '../../../RoomNotifs';
import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
import {sleep} from "../../../utils/promise";
import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextMenu";
const RoomTagOption = ({active, onClick, src, srcSet, label}) => {
const classes = classNames('mx_RoomTileContextMenu_tag_field', {
'mx_RoomTileContextMenu_tag_fieldSet': active,
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
});
return (
<MenuItemCheckbox className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_tag_icon" src={src} width="15" height="15" alt="" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={srcSet} width="15" height="15" alt="" />
{ label }
</MenuItemCheckbox>
);
};
const NotifOption = ({active, onClick, src, label}) => {
const classes = classNames('mx_RoomTileContextMenu_notif_field', {
'mx_RoomTileContextMenu_notif_fieldSet': active,
});
return (
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" alt="" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={src} width="16" height="12" alt="" />
{ label }
</MenuItemRadio>
);
};
export default createReactClass({
displayName: 'RoomTileContextMenu',
propTypes: {
room: PropTypes.object.isRequired,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
},
getInitialState() {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
return {
roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
};
},
componentDidMount: function() {
this._unmounted = false;
},
componentWillUnmount: function() {
this._unmounted = true;
},
_toggleTag: function(tagNameOn, tagNameOff) {
if (!MatrixClientPeg.get().isGuest()) {
sleep(500).then(() => {
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
tagNameOff, tagNameOn,
undefined, 0,
), true);
this.props.onFinished();
});
}
},
_onClickFavourite: function() {
// Tag room as 'Favourite'
if (!this.state.isFavourite && this.state.isLowPriority) {
this.setState({
isFavourite: true,
isLowPriority: false,
});
this._toggleTag("m.favourite", "m.lowpriority");
} else if (this.state.isFavourite) {
this.setState({isFavourite: false});
this._toggleTag(null, "m.favourite");
} else if (!this.state.isFavourite) {
this.setState({isFavourite: true});
this._toggleTag("m.favourite");
}
},
_onClickLowPriority: function() {
// Tag room as 'Low Priority'
if (!this.state.isLowPriority && this.state.isFavourite) {
this.setState({
isFavourite: false,
isLowPriority: true,
});
this._toggleTag("m.lowpriority", "m.favourite");
} else if (this.state.isLowPriority) {
this.setState({isLowPriority: false});
this._toggleTag(null, "m.lowpriority");
} else if (!this.state.isLowPriority) {
this.setState({isLowPriority: true});
this._toggleTag("m.lowpriority");
}
},
_onClickDM: function() {
if (MatrixClientPeg.get().isGuest()) return;
const newIsDirectMessage = !this.state.isDirectMessage;
this.setState({
isDirectMessage: newIsDirectMessage,
});
Rooms.guessAndSetDMRoom(
this.props.room, newIsDirectMessage,
).then(sleep(500)).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
}, (err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to set Direct Message status of room', '', ErrorDialog, {
title: _t('Failed to set Direct Message status of room'),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
},
_onClickLeave: function() {
// Leave room
dis.dispatch({
action: 'leave_room',
room_id: this.props.room.roomId,
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_onClickReject: function() {
dis.dispatch({
action: 'reject_invite',
room_id: this.props.room.roomId,
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_onClickForget: function() {
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
MatrixClientPeg.get().forget(this.props.room.roomId).then(() => {
// Switch to another room view if we're currently viewing the
// historical room
if (RoomViewStore.getRoomId() === this.props.room.roomId) {
dis.dispatch({ action: 'view_next_room' });
}
}, function(err) {
const errCode = err.errcode || _td("unknown error code");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t('Failed to forget room %(errCode)s', {errCode: errCode}),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_saveNotifState: function(newState) {
if (MatrixClientPeg.get().isGuest()) return;
const oldState = this.state.roomNotifState;
const roomId = this.props.room.roomId;
this.setState({
roomNotifState: newState,
});
RoomNotifs.setRoomNotifsState(roomId, newState).then(() => {
// delay slightly so that the user can see their state change
// before closing the menu
return sleep(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
});
}, (error) => {
// TODO: some form of error notification to the user
// to inform them that their state change failed.
// For now we at least set the state back
if (this._unmounted) return;
this.setState({
roomNotifState: oldState,
});
});
},
_onClickAlertMe: function() {
this._saveNotifState(RoomNotifs.ALL_MESSAGES_LOUD);
},
_onClickAllNotifs: function() {
this._saveNotifState(RoomNotifs.ALL_MESSAGES);
},
_onClickMentions: function() {
this._saveNotifState(RoomNotifs.MENTIONS_ONLY);
},
_onClickMute: function() {
this._saveNotifState(RoomNotifs.MUTE);
},
_renderNotifMenu: function() {
return (
<div className="mx_RoomTileContextMenu" role="group" aria-label={_t("Notification settings")}>
<div className="mx_RoomTileContextMenu_notif_picker" role="presentation">
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" alt="" />
</div>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES_LOUD}
label={_t('All messages (noisy)')}
onClick={this._onClickAlertMe}
src={require("../../../../res/img/icon-context-mute-off-copy.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES}
label={_t('All messages')}
onClick={this._onClickAllNotifs}
src={require("../../../../res/img/icon-context-mute-off.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MENTIONS_ONLY}
label={_t('Mentions only')}
onClick={this._onClickMentions}
src={require("../../../../res/img/icon-context-mute-mentions.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MUTE}
label={_t('Mute')}
onClick={this._onClickMute}
src={require("../../../../res/img/icon-context-mute.svg")}
/>
</div>
);
},
_onClickSettings: function() {
dis.dispatch({
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
if (this.props.onFinished) {
this.props.onFinished();
}
},
_renderSettingsMenu: function() {
return (
<div>
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/feather-customised/settings.svg")} width="15" height="15" alt="" />
{ _t('Settings') }
</MenuItem>
</div>
);
},
_renderLeaveMenu: function(membership) {
if (!membership) {
return null;
}
let leaveClickHandler = null;
let leaveText = null;
switch (membership) {
case "join":
leaveClickHandler = this._onClickLeave;
leaveText = _t('Leave');
break;
case "leave":
case "ban":
leaveClickHandler = this._onClickForget;
leaveText = _t('Forget');
break;
case "invite":
leaveClickHandler = this._onClickReject;
leaveText = _t('Reject');
break;
}
return (
<div>
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
{ leaveText }
</MenuItem>
</div>
);
},
_renderRoomTagMenu: function() {
return (
<div>
<RoomTagOption
active={this.state.isFavourite}
label={_t('Favourite')}
onClick={this._onClickFavourite}
src={require("../../../../res/img/icon_context_fave.svg")}
srcSet={require("../../../../res/img/icon_context_fave_on.svg")}
/>
<RoomTagOption
active={this.state.isLowPriority}
label={_t('Low Priority')}
onClick={this._onClickLowPriority}
src={require("../../../../res/img/icon_context_low.svg")}
srcSet={require("../../../../res/img/icon_context_low_on.svg")}
/>
<RoomTagOption
active={this.state.isDirectMessage}
label={_t('Direct Chat')}
onClick={this._onClickDM}
src={require("../../../../res/img/icon_context_person.svg")}
srcSet={require("../../../../res/img/icon_context_person_on.svg")}
/>
</div>
);
},
render: function() {
const myMembership = this.props.room.getMyMembership();
switch (myMembership) {
case 'join':
return <div>
{ this._renderNotifMenu() }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderRoomTagMenu() }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
case 'invite':
return <div>
{ this._renderLeaveMenu(myMembership) }
</div>;
default:
return <div>
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
}
},
});

View file

@ -1,155 +0,0 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
import {Action} from "../../../dispatcher/actions";
export default class TopLeftMenu extends React.Component {
static propTypes = {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
onFinished: PropTypes.func,
// Optional function to collect a reference to the container
// of this component directly.
containerRef: PropTypes.func,
};
constructor() {
super();
this.viewHomePage = this.viewHomePage.bind(this);
this.openSettings = this.openSettings.bind(this);
this.signIn = this.signIn.bind(this);
this.signOut = this.signOut.bind(this);
}
hasHomePage() {
return !!getHomePageUrl(SdkConfig.get());
}
render() {
const isGuest = MatrixClientPeg.get().isGuest();
const hostingSignupLink = getHostingLink('user-context-menu');
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <div className="mx_TopLeftMenu_upgradeLink">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub =>
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
let homePageItem = null;
if (this.hasHomePage()) {
homePageItem = (
<MenuItem className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
{_t("Home")}
</MenuItem>
);
}
let signInOutItem;
if (isGuest) {
signInOutItem = (
<MenuItem className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
{_t("Sign in")}
</MenuItem>
);
} else {
signInOutItem = (
<MenuItem className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
{_t("Sign out")}
</MenuItem>
);
}
const helpItem = (
<MenuItem className="mx_TopLeftMenu_icon_help" onClick={this.openHelp}>
{_t("Help")}
</MenuItem>
);
const settingsItem = (
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
{_t("Settings")}
</MenuItem>
);
return <div className="mx_TopLeftMenu" ref={this.props.containerRef} role="menu">
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true} tabIndex={-1}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
{homePageItem}
{settingsItem}
{helpItem}
{signInOutItem}
</ul>
</div>;
}
openHelp = () => {
this.closeMenu();
const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
};
viewHomePage() {
dis.dispatch({action: 'view_home_page'});
this.closeMenu();
}
openSettings() {
dis.fire(Action.ViewUserSettings);
this.closeMenu();
}
signIn() {
dis.dispatch({action: 'start_login'});
this.closeMenu();
}
signOut() {
Modal.createTrackedDialog('Logout E2E Export', '', LogoutDialog);
this.closeMenu();
}
closeMenu() {
if (this.props.onFinished) this.props.onFinished();
}
}

View file

@ -1,57 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
const Presets = {
PrivateChat: "private_chat",
PublicChat: "public_chat",
Custom: "custom",
};
export default createReactClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: PropTypes.func,
preset: PropTypes.string,
},
Presets: Presets,
getDefaultProps: function() {
return {
onChange: function() {},
};
},
onValueChanged: function(ev) {
this.props.onChange(ev.target.value);
},
render: function() {
return (
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
<option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
<option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
<option value={this.Presets.Custom}>{ _t("Custom") }</option>
</select>
);
},
});

View file

@ -1,106 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'RoomAlias',
propTypes: {
// Specifying a homeserver will make magical things happen when you,
// e.g. start typing in the room alias box.
homeserver: PropTypes.string,
alias: PropTypes.string,
onChange: PropTypes.func,
},
getDefaultProps: function() {
return {
onChange: function() {},
alias: '',
};
},
getAliasLocalpart: function() {
let room_alias = this.props.alias;
if (room_alias && this.props.homeserver) {
const suffix = ":" + this.props.homeserver;
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
room_alias = room_alias.slice(1, -suffix.length);
}
}
return room_alias;
},
onValueChanged: function(ev) {
this.props.onChange(ev.target.value);
},
onFocus: function(ev) {
const target = ev.target;
const curr_val = ev.target.value;
if (this.props.homeserver) {
if (curr_val == "") {
const self = this;
setTimeout(function() {
target.value = "#:" + self.props.homeserver;
target.setSelectionRange(1, 1);
}, 0);
} else {
const suffix = ":" + this.props.homeserver;
setTimeout(function() {
target.setSelectionRange(
curr_val.startsWith("#") ? 1 : 0,
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length,
);
}, 0);
}
}
},
onBlur: function(ev) {
const curr_val = ev.target.value;
if (this.props.homeserver) {
if (curr_val == "#:" + this.props.homeserver) {
ev.target.value = "";
return;
}
if (curr_val != "") {
let new_val = ev.target.value;
const suffix = ":" + this.props.homeserver;
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
ev.target.value = new_val;
}
}
},
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias} />
);
},
});

View file

@ -19,7 +19,6 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
@ -45,10 +44,8 @@ const addressTypeName = {
};
export default createReactClass({
displayName: "AddressPickerDialog",
propTypes: {
export default class AddressPickerDialog extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.node,
// Extra node inserted after picker input, dropdown and errors
@ -66,26 +63,28 @@ export default createReactClass({
// Whether the current user should be included in the addresses returned. Only
// applicable when pickerType is `user`. Default: false.
includeSelf: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
value: "",
focus: true,
validAddressTypes: addressTypes,
pickerType: 'user',
includeSelf: false,
};
},
static defaultProps = {
value: "",
focus: true,
validAddressTypes: addressTypes,
pickerType: 'user',
includeSelf: false,
};
constructor(props) {
super(props);
this._textinput = createRef();
getInitialState: function() {
let validAddressTypes = this.props.validAddressTypes;
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) {
validAddressTypes = validAddressTypes.filter(type => type !== "email");
}
return {
this.state = {
// Whether to show an error message because of an invalid address
invalidAddressError: false,
// List of UserAddressType objects representing
@ -106,19 +105,14 @@ export default createReactClass({
// dialog is open and represents the supported list of address types at this time.
validAddressTypes,
};
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
componentDidMount: function() {
componentDidMount() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this._textinput.current.value = this.props.value;
}
},
}
getPlaceholder() {
const { placeholder } = this.props;
@ -127,9 +121,9 @@ export default createReactClass({
}
// Otherwise it's a function, as checked by prop types.
return placeholder(this.state.validAddressTypes);
},
}
onButtonClick: function() {
onButtonClick = () => {
let selectedList = this.state.selectedList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local selectedList
@ -138,13 +132,13 @@ export default createReactClass({
if (selectedList === null) return;
}
this.props.onFinished(true, selectedList);
},
};
onCancel: function() {
onCancel = () => {
this.props.onFinished(false);
},
};
onKeyDown: function(e) {
onKeyDown = e => {
const textInput = this._textinput.current ? this._textinput.current.value : undefined;
if (e.key === Key.ESCAPE) {
@ -181,9 +175,9 @@ export default createReactClass({
e.preventDefault();
this._addAddressesToList([textInput]);
}
},
};
onQueryChanged: function(ev) {
onQueryChanged = ev => {
const query = ev.target.value;
if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
@ -216,28 +210,24 @@ export default createReactClass({
searchError: null,
});
}
},
};
onDismissed: function(index) {
return () => {
const selectedList = this.state.selectedList.slice();
selectedList.splice(index, 1);
this.setState({
selectedList,
suggestedList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
},
onDismissed = index => () => {
const selectedList = this.state.selectedList.slice();
selectedList.splice(index, 1);
this.setState({
selectedList,
suggestedList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
onClick: function(index) {
return () => {
this.onSelected(index);
};
},
onClick = index => () => {
this.onSelected(index);
};
onSelected: function(index) {
onSelected = index => {
const selectedList = this.state.selectedList.slice();
selectedList.push(this._getFilteredSuggestions()[index]);
this.setState({
@ -246,9 +236,9 @@ export default createReactClass({
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
},
};
_doNaiveGroupSearch: function(query) {
_doNaiveGroupSearch(query) {
const lowerCaseQuery = query.toLowerCase();
this.setState({
busy: true,
@ -280,9 +270,9 @@ export default createReactClass({
busy: false,
});
});
},
}
_doNaiveGroupRoomSearch: function(query) {
_doNaiveGroupRoomSearch(query) {
const lowerCaseQuery = query.toLowerCase();
const results = [];
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
@ -302,9 +292,9 @@ export default createReactClass({
this.setState({
busy: false,
});
},
}
_doRoomSearch: function(query) {
_doRoomSearch(query) {
const lowerCaseQuery = query.toLowerCase();
const rooms = MatrixClientPeg.get().getRooms();
const results = [];
@ -359,9 +349,9 @@ export default createReactClass({
this.setState({
busy: false,
});
},
}
_doUserDirectorySearch: function(query) {
_doUserDirectorySearch(query) {
this.setState({
busy: true,
query,
@ -393,9 +383,9 @@ export default createReactClass({
busy: false,
});
});
},
}
_doLocalSearch: function(query) {
_doLocalSearch(query) {
this.setState({
query,
searchError: null,
@ -417,9 +407,9 @@ export default createReactClass({
});
});
this._processResults(results, query);
},
}
_processResults: function(results, query) {
_processResults(results, query) {
const suggestedList = [];
results.forEach((result) => {
if (result.room_id) {
@ -485,9 +475,9 @@ export default createReactClass({
}, () => {
if (this.addressSelector) this.addressSelector.moveSelectionTop();
});
},
}
_addAddressesToList: function(addressTexts) {
_addAddressesToList(addressTexts) {
const selectedList = this.state.selectedList.slice();
let hasError = false;
@ -529,9 +519,9 @@ export default createReactClass({
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return hasError ? null : selectedList;
},
}
_lookupThreepid: async function(medium, address) {
async _lookupThreepid(medium, address) {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
@ -577,9 +567,9 @@ export default createReactClass({
searchError: _t('Something went wrong!'),
});
}
},
}
_getFilteredSuggestions: function() {
_getFilteredSuggestions() {
// map addressType => set of addresses to avoid O(n*m) operation
const selectedAddresses = {};
this.state.selectedList.forEach(({address, addressType}) => {
@ -591,17 +581,17 @@ export default createReactClass({
return this.state.suggestedList.filter(({address, addressType}) => {
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
});
},
}
_onPaste: function(e) {
_onPaste = e => {
// Prevent the text being pasted into the textarea
e.preventDefault();
const text = e.clipboardData.getData("text");
// Process it as a list of addresses to add instead
this._addAddressesToList(text.split(/[\s,]+/));
},
};
onUseDefaultIdentityServerClick(e) {
onUseDefaultIdentityServerClick = e => {
e.preventDefault();
// Update the IS in account data. Actually using it may trigger terms.
@ -612,15 +602,15 @@ export default createReactClass({
const { validAddressTypes } = this.state;
validAddressTypes.push('email');
this.setState({ validAddressTypes });
},
};
onManageSettingsClick(e) {
onManageSettingsClick = e => {
e.preventDefault();
dis.fire(Action.ViewUserSettings);
this.onCancel();
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AddressSelector = sdk.getComponent("elements.AddressSelector");
@ -738,5 +728,5 @@ export default createReactClass({
onCancel={this.onCancel} />
</BaseDialog>
);
},
});
}
}

View file

@ -16,37 +16,36 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default createReactClass({
propTypes: {
export default class AskInviteAnywayDialog extends React.Component {
static propTypes = {
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
onInviteAnyways: PropTypes.func.isRequired,
onGiveUp: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},
};
_onInviteClicked: function() {
_onInviteClicked = () => {
this.props.onInviteAnyways();
this.props.onFinished(true);
},
};
_onInviteNeverWarnClicked: function() {
_onInviteNeverWarnClicked = () => {
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
this.props.onInviteAnyways();
this.props.onFinished(true);
},
};
_onGiveUpClicked: function() {
_onGiveUpClicked = () => {
this.props.onGiveUp();
this.props.onFinished(false);
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const errorList = this.props.unknownProfileUsers
@ -78,5 +77,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import FocusLock from 'react-focus-lock';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@ -28,16 +27,14 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
/**
/*
* Basic container for modal dialogs.
*
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
export default createReactClass({
displayName: 'BaseDialog',
propTypes: {
export default class BaseDialog extends React.Component {
static propTypes = {
// onFinished callback to call when Escape is pressed
// Take a boolean which is true if the dialog was dismissed
// with a positive / confirm action or false if it was
@ -81,21 +78,20 @@ export default createReactClass({
PropTypes.object,
PropTypes.arrayOf(PropTypes.string),
]),
},
};
getDefaultProps: function() {
return {
hasCancel: true,
fixedWidth: true,
};
},
static defaultProps = {
hasCancel: true,
fixedWidth: true,
};
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
}
_onKeyDown: function(e) {
_onKeyDown = (e) => {
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
@ -104,13 +100,13 @@ export default createReactClass({
e.preventDefault();
this.props.onFinished(false);
}
},
};
_onCancelClick: function(e) {
_onCancelClick = (e) => {
this.props.onFinished(false);
},
};
render: function() {
render() {
let cancelButton;
if (this.props.hasCancel) {
cancelButton = (
@ -161,5 +157,5 @@ export default createReactClass({
</FocusLock>
</MatrixClientContext.Provider>
);
},
});
}
}

View file

@ -23,7 +23,8 @@ import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sendBugReport from '../../../rageshake/submit-rageshake';
import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-rageshake';
import AccessibleButton from "../elements/AccessibleButton";
export default class BugReportDialog extends React.Component {
constructor(props) {
@ -35,6 +36,8 @@ export default class BugReportDialog extends React.Component {
issueUrl: "",
text: "",
progress: null,
downloadBusy: false,
downloadProgress: null,
};
this._unmounted = false;
this._onSubmit = this._onSubmit.bind(this);
@ -43,6 +46,7 @@ export default class BugReportDialog extends React.Component {
this._onIssueUrlChange = this._onIssueUrlChange.bind(this);
this._onSendLogsChange = this._onSendLogsChange.bind(this);
this._sendProgressCallback = this._sendProgressCallback.bind(this);
this._downloadProgressCallback = this._downloadProgressCallback.bind(this);
}
componentWillUnmount() {
@ -95,6 +99,31 @@ export default class BugReportDialog extends React.Component {
});
}
_onDownload = async (ev) => {
this.setState({ downloadBusy: true });
this._downloadProgressCallback(_t("Preparing to download logs"));
try {
await downloadBugReport({
sendLogs: true,
progressCallback: this._downloadProgressCallback,
label: this.props.label,
});
this.setState({
downloadBusy: false,
downloadProgress: null,
});
} catch (err) {
if (!this._unmounted) {
this.setState({
downloadBusy: false,
downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
});
}
}
};
_onTextChange(ev) {
this.setState({ text: ev.target.value });
}
@ -114,6 +143,13 @@ export default class BugReportDialog extends React.Component {
this.setState({progress: progress});
}
_downloadProgressCallback(downloadProgress) {
if (this._unmounted) {
return;
}
this.setState({ downloadProgress });
}
render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -166,20 +202,28 @@ export default class BugReportDialog extends React.Component {
{
a: (sub) => <a
target="_blank"
href="https://github.com/vector-im/riot-web/issues/new"
href="https://github.com/vector-im/element-web/issues/new"
>
{ sub }
</a>,
},
) }
</b></p>
<div className="mx_BugReportDialog_download">
<AccessibleButton onClick={this._onDownload} kind="link" disabled={this.state.downloadBusy}>
{ _t("Download logs") }
</AccessibleButton>
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
</div>
<Field
type="text"
className="mx_BugReportDialog_field_input"
label={_t("GitHub issue")}
onChange={this._onIssueUrlChange}
value={this.state.issueUrl}
placeholder="https://github.com/vector-im/riot-web/issues/..."
placeholder="https://github.com/vector-im/element-web/issues/..."
/>
<Field
className="mx_BugReportDialog_field_input"

View file

@ -21,7 +21,7 @@ import * as sdk from '../../../index';
import request from 'browser-request';
import { _t } from '../../../languageHandler';
const REPOS = ['vector-im/riot-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
export default class ChangelogDialog extends React.Component {
constructor(props) {

View file

@ -0,0 +1,248 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent, FormEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { arrayFastClone } from "../../../utils/arrays";
import SdkConfig from "../../../SdkConfig";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import InviteDialog from "./InviteDialog";
import BaseAvatar from "../avatars/BaseAvatar";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
interface IProps extends IDialogProps {
roomId: string;
communityName: string;
}
interface IPerson {
userId: string;
user: RoomMember;
lastActive: number;
}
interface IState {
emailTargets: string[];
userTargets: string[];
showPeople: boolean;
people: IPerson[];
numPeople: number;
busy: boolean;
}
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
emailTargets: [],
userTargets: [],
showPeople: false,
people: this.buildSuggestions(),
numPeople: 5, // arbitrary default
busy: false,
};
}
private buildSuggestions(): IPerson[] {
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
if (this.props.roomId) {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
// add banned users, so we don't try to invite them
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
}
return InviteDialog.buildRecents(alreadyInvited);
}
private onSubmit = async (ev: FormEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true});
try {
const targets = [...this.state.emailTargets, ...this.state.userTargets];
const result = await inviteMultipleToRoom(this.props.roomId, targets);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const success = showAnyInviteErrors(result.states, room, result.inviter);
if (success) {
this.props.onFinished(true);
} else {
this.setState({busy: false});
}
} catch (e) {
this.setState({busy: false});
console.error(e);
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((e && e.message) ? e.message : _t("Operation failed")),
});
}
};
private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) {
targets.push(ev.target.value);
} else {
targets[index] = ev.target.value;
}
this.setState({emailTargets: targets});
};
private onAddressBlur = (index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) return; // not important
if (targets[index].trim() === "") {
targets.splice(index, 1);
this.setState({emailTargets: targets});
}
};
private onShowPeopleClick = () => {
this.setState({showPeople: !this.state.showPeople});
};
private setPersonToggle = (person: IPerson, selected: boolean) => {
const targets = arrayFastClone(this.state.userTargets);
if (selected && !targets.includes(person.userId)) {
targets.push(person.userId);
} else if (!selected && targets.includes(person.userId)) {
targets.splice(targets.indexOf(person.userId), 1);
}
this.setState({userTargets: targets});
};
private renderPerson(person: IPerson, key: any) {
const avatarSize = 36;
return (
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
<BaseAvatar
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
name={person.user.name}
idName={person.user.userId}
width={avatarSize}
height={avatarSize}
/>
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
<span className="mx_CommunityPrototypeInviteDialog_personName">{person.user.name}</span>
<span className="mx_CommunityPrototypeInviteDialog_personId">{person.userId}</span>
</div>
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
</div>
);
}
private onShowMorePeople = () => {
this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase
};
public render() {
const emailAddresses = [];
this.state.emailTargets.forEach((address, i) => {
emailAddresses.push((
<Field
key={i}
value={address}
onChange={(e) => this.onAddressChange(e, i)}
label={_t("Email address")}
placeholder={_t("Email address")}
onBlur={() => this.onAddressBlur(i)}
/>
));
});
// Push a clean input
emailAddresses.push((
<Field
key={emailAddresses.length}
value={""}
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
/>
));
let peopleIntro = null;
const people = [];
if (this.state.showPeople) {
const humansToPresent = this.state.people.slice(0, this.state.numPeople);
humansToPresent.forEach((person, i) => {
people.push(this.renderPerson(person, i));
});
if (humansToPresent.length < this.state.people.length) {
people.push((
<AccessibleButton
onClick={this.onShowMorePeople}
kind="link" key="more"
className="mx_CommunityPrototypeInviteDialog_morePeople"
>{_t("Show more")}</AccessibleButton>
));
}
}
if (this.state.people.length > 0) {
peopleIntro = (
<div className="mx_CommunityPrototypeInviteDialog_people">
<span>{_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})}</span>
<AccessibleButton onClick={this.onShowPeopleClick}>
{this.state.showPeople ? _t("Hide") : _t("Show")}
</AccessibleButton>
</div>
);
}
let buttonText = _t("Skip");
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
if (targetCount > 0) {
buttonText = _t("Send %(count)s invites", {count: targetCount});
}
return (
<BaseDialog
className="mx_CommunityPrototypeInviteDialog"
onFinished={this.props.onFinished}
title={_t("Invite people to join %(communityName)s", {communityName: this.props.communityName})}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
{emailAddresses}
{peopleIntro}
{people}
<AccessibleButton
kind="primary" onClick={this.onSubmit}
disabled={this.state.busy}
className="mx_CommunityPrototypeInviteDialog_primaryButton"
>{buttonText}</AccessibleButton>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -15,17 +15,14 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
/*
* A dialog for confirming a redaction.
*/
export default createReactClass({
displayName: 'ConfirmRedactDialog',
render: function() {
export default class ConfirmRedactDialog extends React.Component {
render() {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (
<QuestionDialog onFinished={this.props.onFinished}
@ -36,5 +33,5 @@ export default createReactClass({
button={_t("Remove")}>
</QuestionDialog>
);
},
});
}
}

View file

@ -15,7 +15,6 @@ 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 * as sdk from '../../../index';
@ -30,9 +29,8 @@ 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 createReactClass({
displayName: 'ConfirmUserActionDialog',
propTypes: {
export default class ConfirmUserActionDialog extends React.Component {
static propTypes = {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: PropTypes.object,
// group member object. Supply either this or 'member'
@ -48,35 +46,36 @@ export default createReactClass({
askReason: PropTypes.bool,
danger: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
};
getDefaultProps: () => ({
static defaultProps = {
danger: false,
askReason: false,
}),
};
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._reasonField = null;
},
}
onOk: function() {
onOk = () => {
let reason;
if (this._reasonField) {
reason = this._reasonField.value;
}
this.props.onFinished(true, reason);
},
};
onCancel: function() {
onCancel = () => {
this.props.onFinished(false);
},
};
_collectReasonField: function(e) {
_collectReasonField = e => {
this._reasonField = e;
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
@ -134,5 +133,5 @@ export default createReactClass({
onCancel={this.onCancel} />
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,227 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
import GroupStore from "../../../stores/GroupStore";
interface IProps extends IDialogProps {
}
interface IState {
name: string;
localpart: string;
error: string;
busy: boolean;
avatarFile: File;
avatarPreview: string;
}
export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: IProps) {
super(props);
this.state = {
name: "",
localpart: "",
error: null,
busy: false,
avatarFile: null,
avatarPreview: null,
};
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
this.setState({name: ev.target.value, localpart});
};
private onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (this.state.busy) return;
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({busy: true});
try {
let avatarUrl = ''; // must be a string for synapse to accept it
if (this.state.avatarFile) {
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
}
const result = await MatrixClientPeg.get().createGroup({
localpart: this.state.localpart,
profile: {
name: this.state.name,
avatar_url: avatarUrl,
},
});
// Ensure the tag gets selected now that we've created it
dis.dispatch({action: 'deselect_tags'}, true);
dis.dispatch({
action: 'select_tag',
tag: result.group_id,
});
// Close our own dialog before moving much further
this.props.onFinished(true);
if (result.room_id) {
// Force the group store to update as it might have missed the general chat
await GroupStore.refreshGroupRooms(result.group_id);
dis.dispatch({
action: 'view_room',
room_id: result.room_id,
});
showCommunityRoomInviteDialog(result.room_id, this.state.name);
} else {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
}
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t(
"There was an error creating your community. The name may be taken or the " +
"server is unable to process your request.",
),
});
}
};
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({avatarFile: null});
} else {
this.setState({busy: true});
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
};
reader.readAsDataURL(file);
}
};
private onChangeAvatar = () => {
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
};
public render() {
let communityId = null;
if (this.state.localpart) {
communityId = (
<span className="mx_CreateCommunityPrototypeDialog_communityId">
{_t("Community ID: +<localpart />:%(domain)s", {
domain: MatrixClientPeg.getHomeserverName(),
}, {
localpart: () => <u>{this.state.localpart}</u>,
})}
<InfoTooltip
tooltip={_t(
"Use this when referencing your community to others. The community ID " +
"cannot be changed.",
)}
/>
</span>
);
}
let helpText = (
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{_t("You can change this later if needed.")}
</span>
);
if (this.state.error) {
const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
helpText = (
<span className={classes}>
{this.state.error}
</span>
);
}
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />
}
return (
<BaseDialog
className="mx_CreateCommunityPrototypeDialog"
onFinished={this.props.onFinished}
title={_t("What's the name of your community or team?")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateCommunityPrototypeDialog_colName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
{helpText}
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{/*nbsp is to reserve the height of this element when there's nothing*/}
&nbsp;{communityId}
</span>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{_t("Create")}
</AccessibleButton>
</div>
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
<input
type="file" style={{display: "none"}}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton
onClick={this.onChangeAvatar}
className="mx_CreateCommunityPrototypeDialog_avatarContainer"
>
{preview}
</AccessibleButton>
<div className="mx_CreateCommunityPrototypeDialog_tip">
<b>{_t("Add image (optional)")}</b>
<span>
{_t("An image will help people identify your community.")}
</span>
</div>
</div>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -15,46 +15,42 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
export default createReactClass({
displayName: 'CreateGroupDialog',
propTypes: {
export default class CreateGroupDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
groupName: '',
groupId: '',
groupError: null,
creating: false,
createError: null,
};
},
state = {
groupName: '',
groupId: '',
groupError: null,
creating: false,
createError: null,
};
_onGroupNameChange: function(e) {
_onGroupNameChange = e => {
this.setState({
groupName: e.target.value,
});
},
};
_onGroupIdChange: function(e) {
_onGroupIdChange = e => {
this.setState({
groupId: e.target.value,
});
},
};
_onGroupIdBlur: function(e) {
_onGroupIdBlur = e => {
this._checkGroupId();
},
};
_checkGroupId: function(e) {
_checkGroupId(e) {
let error = null;
if (!this.state.groupId) {
error = _t("Community IDs cannot be empty.");
@ -67,9 +63,9 @@ export default createReactClass({
createError: null,
});
return error;
},
}
_onFormSubmit: function(e) {
_onFormSubmit = e => {
e.preventDefault();
if (this._checkGroupId()) return;
@ -94,13 +90,13 @@ export default createReactClass({
}).finally(() => {
this.setState({creating: false});
});
},
};
_onCancel: function() {
_onCancel = () => {
this.props.onFinished(false);
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
@ -171,5 +167,5 @@ export default createReactClass({
</form>
</BaseDialog>
);
},
});
}
}

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
@ -25,17 +24,19 @@ import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import {privateShouldBeEncrypted} from "../../../createRoom";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
export default createReactClass({
displayName: 'CreateRoomDialog',
propTypes: {
export default class CreateRoomDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
defaultPublic: PropTypes.bool,
},
};
constructor(props) {
super(props);
getInitialState() {
const config = SdkConfig.get();
return {
this.state = {
isPublic: this.props.defaultPublic || false,
isEncrypted: privateShouldBeEncrypted(),
name: "",
@ -45,7 +46,7 @@ export default createReactClass({
noFederate: config.default_federate === false,
nameIsValid: false,
};
},
}
_roomCreateOptions() {
const opts = {};
@ -70,28 +71,32 @@ export default createReactClass({
opts.encryption = this.state.isEncrypted;
}
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
}
return opts;
},
}
componentDidMount() {
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
// move focus to first field when showing dialog
this._nameFieldRef.focus();
},
}
componentWillUnmount() {
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
},
}
_onKeyDown: function(event) {
_onKeyDown = event => {
if (event.key === Key.ENTER) {
this.onOk();
event.preventDefault();
event.stopPropagation();
}
},
};
onOk: async function() {
onOk = async () => {
const activeElement = document.activeElement;
if (activeElement) {
activeElement.blur();
@ -117,51 +122,51 @@ export default createReactClass({
field.validate({ allowEmpty: false, focused: true });
}
}
},
};
onCancel: function() {
onCancel = () => {
this.props.onFinished(false);
},
};
onNameChange(ev) {
onNameChange = ev => {
this.setState({name: ev.target.value});
},
};
onTopicChange(ev) {
onTopicChange = ev => {
this.setState({topic: ev.target.value});
},
};
onPublicChange(isPublic) {
onPublicChange = isPublic => {
this.setState({isPublic});
},
};
onEncryptedChange(isEncrypted) {
onEncryptedChange = isEncrypted => {
this.setState({isEncrypted});
},
};
onAliasChange(alias) {
onAliasChange = alias => {
this.setState({alias});
},
};
onDetailsToggled(ev) {
onDetailsToggled = ev => {
this.setState({detailsOpen: ev.target.open});
},
};
onNoFederateChange(noFederate) {
onNoFederateChange = noFederate => {
this.setState({noFederate});
},
};
collectDetailsRef(ref) {
collectDetailsRef = ref => {
this._detailsRef = ref;
},
};
async onNameValidate(fieldState) {
const result = await this._validateRoomName(fieldState);
onNameValidate = async fieldState => {
const result = await CreateRoomDialog._validateRoomName(fieldState);
this.setState({nameIsValid: result.valid});
return result;
},
};
_validateRoomName: withValidation({
static _validateRoomName = withValidation({
rules: [
{
key: "required",
@ -169,27 +174,34 @@ export default createReactClass({
invalid: () => _t("Please enter a name for the room"),
},
],
}),
});
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field');
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
</div>
);
} else {
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
let publicPrivateLabel = <p>{_t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone.",
)}</p>;
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
publicPrivateLabel = <p>{_t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone in this community.",
)}</p>;
}
let e2eeSection;
@ -212,7 +224,24 @@ export default createReactClass({
</React.Fragment>;
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
let federateLabel = _t(
"You might enable this if the room will only be used for collaborating with internal " +
"teams on your homeserver. This cannot be changed later.",
);
if (SdkConfig.get().default_federate === false) {
// We only change the label if the default setting is different to avoid jarring text changes to the
// user. They will have read the implications of turning this off/on, so no need to rephrase for them.
federateLabel = _t(
"You might disable this if the room will be used for collaborating with external " +
"teams who have their own homeserver. This cannot be changed later.",
);
}
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
title = _t("Create a room in %(communityName)s", {communityName: name});
}
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
title={title}
@ -227,7 +256,15 @@ export default createReactClass({
{ aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
<LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
<LabelledToggleSwitch
label={_t(
"Block anyone not part of %(serverName)s from ever joining this room.",
{serverName: MatrixClientPeg.getHomeserverName()},
)}
onChange={this.onNoFederateChange}
value={this.state.noFederate}
/>
<p>{federateLabel}</p>
</details>
</div>
</form>
@ -236,5 +273,5 @@ export default createReactClass({
onCancel={this.onCancel} />
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,167 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import FlairStore from "../../../stores/FlairStore";
interface IProps extends IDialogProps {
communityId: string;
}
interface IState {
name: string;
error: string;
busy: boolean;
currentAvatarUrl: string;
avatarFile: File;
avatarPreview: string;
}
// XXX: This is a lot of duplication from the create dialog, just in a different shape
export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: IProps) {
super(props);
const profile = CommunityPrototypeStore.instance.getCommunityProfile(props.communityId);
this.state = {
name: profile?.name || "",
error: null,
busy: false,
avatarFile: null,
avatarPreview: null,
currentAvatarUrl: profile?.avatarUrl,
};
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({name: ev.target.value});
};
private onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (this.state.busy) return;
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({busy: true});
try {
let avatarUrl = this.state.currentAvatarUrl || ""; // must be a string for synapse to accept it
if (this.state.avatarFile) {
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
}
await MatrixClientPeg.get().setGroupProfile(this.props.communityId, {
name: this.state.name,
avatar_url: avatarUrl,
});
// ask the flair store to update the profile too
await FlairStore.refreshGroupProfile(MatrixClientPeg.get(), this.props.communityId);
// we did it, so close the dialog
this.props.onFinished(true);
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t("There was an error updating your community. The server is unable to process your request."),
});
}
};
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({avatarFile: null});
} else {
this.setState({busy: true});
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
};
reader.readAsDataURL(file);
}
};
private onChangeAvatar = () => {
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
};
public render() {
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
if (this.state.currentAvatarUrl) {
const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl);
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
} else {
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />
}
}
return (
<BaseDialog
className="mx_EditCommunityPrototypeDialog"
onFinished={this.props.onFinished}
title={_t("Update community")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="mx_EditCommunityPrototypeDialog_rowName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
</div>
<div className="mx_EditCommunityPrototypeDialog_rowAvatar">
<input
type="file" style={{display: "none"}}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton
onClick={this.onChangeAvatar}
className="mx_EditCommunityPrototypeDialog_avatarContainer"
>{preview}</AccessibleButton>
<div className="mx_EditCommunityPrototypeDialog_tip">
<b>{_t("Add image (optional)")}</b>
<span>
{_t("An image will help people identify your community.")}
</span>
</div>
</div>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{_t("Save")}
</AccessibleButton>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -26,14 +26,12 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'ErrorDialog',
propTypes: {
export default class ErrorDialog extends React.Component {
static propTypes = {
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
@ -43,18 +41,16 @@ export default createReactClass({
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
},
};
getDefaultProps: function() {
return {
focus: true,
title: null,
description: null,
button: null,
};
},
static defaultProps = {
focus: true,
title: null,
description: null,
button: null,
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog
@ -74,5 +70,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -0,0 +1,19 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export interface IDialogProps {
onFinished: (bool) => void;
}

View file

@ -17,15 +17,13 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classNames from "classnames";
export default createReactClass({
displayName: 'InfoDialog',
propTypes: {
export default class InfoDialog extends React.Component {
static propTypes = {
className: PropTypes.string,
title: PropTypes.string,
description: PropTypes.node,
@ -33,21 +31,19 @@ export default createReactClass({
onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func,
},
};
getDefaultProps: function() {
return {
title: '',
description: '',
hasCloseButton: false,
};
},
static defaultProps = {
title: '',
description: '',
hasCloseButton: false,
};
onFinished: function() {
onFinished = () => {
this.props.onFinished();
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
@ -69,5 +65,5 @@ export default createReactClass({
</DialogButtons>
</BaseDialog>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
@ -27,10 +26,8 @@ import AccessibleButton from '../elements/AccessibleButton';
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
export default createReactClass({
displayName: 'InteractiveAuthDialog',
propTypes: {
export default class InteractiveAuthDialog extends React.Component {
static propTypes = {
// matrix client to use for UI auth requests
matrixClient: PropTypes.object.isRequired,
@ -70,19 +67,17 @@ export default createReactClass({
//
// Default is defined in _getDefaultDialogAesthetics()
aestheticsForStagePhases: PropTypes.object,
},
};
getInitialState: function() {
return {
authError: null,
state = {
authError: null,
// See _onUpdateStagePhase()
uiaStage: null,
uiaStagePhase: null,
};
},
// See _onUpdateStagePhase()
uiaStage: null,
uiaStagePhase: null,
};
_getDefaultDialogAesthetics: function() {
_getDefaultDialogAesthetics() {
const ssoAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
@ -102,9 +97,9 @@ export default createReactClass({
[SSOAuthEntry.LOGIN_TYPE]: ssoAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: ssoAesthetics,
};
},
}
_onAuthFinished: function(success, result) {
_onAuthFinished = (success, result) => {
if (success) {
this.props.onFinished(true, result);
} else {
@ -116,18 +111,18 @@ export default createReactClass({
});
}
}
},
};
_onUpdateStagePhase: function(newStage, newPhase) {
_onUpdateStagePhase = (newStage, newPhase) => {
// We copy the stage and stage phase params into state for title selection in render()
this.setState({uiaStage: newStage, uiaStagePhase: newPhase});
},
};
_onDismissClick: function() {
_onDismissClick = () => {
this.props.onFinished(false);
},
};
render: function() {
render() {
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -190,5 +185,5 @@ export default createReactClass({
{ content }
</BaseDialog>
);
},
});
}
}

View file

@ -32,11 +32,12 @@ import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {DefaultTagID} from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@ -327,7 +328,7 @@ export default class InviteDialog extends React.PureComponent {
this.state = {
targets: [], // array of Member objects (see interface above)
filterText: "",
recents: this._buildRecents(alreadyInvited),
recents: InviteDialog.buildRecents(alreadyInvited),
numRecentsShown: INITIAL_ROOMS_SHOWN,
suggestions: this._buildSuggestions(alreadyInvited),
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
@ -344,12 +345,12 @@ export default class InviteDialog extends React.PureComponent {
this._editorRef = createRef();
}
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM];
const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM] || [];
const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
@ -909,12 +910,23 @@ export default class InviteDialog extends React.PureComponent {
this.props.onFinished();
};
_onCommunityInviteClick = (e) => {
this.props.onFinished();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
};
_renderSection(kind: "recents"|"suggestions") {
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
let sectionSubname = null;
if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
}
if (this.props.kind === KIND_INVITE) {
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
@ -993,6 +1005,7 @@ export default class InviteDialog extends React.PureComponent {
return (
<div className='mx_InviteDialog_section'>
<h3>{sectionName}</h3>
{sectionSubname ? <p className="mx_InviteDialog_subname">{sectionSubname}</p> : null}
{tiles}
{showMore}
</div>
@ -1083,6 +1096,33 @@ export default class InviteDialog extends React.PureComponent {
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
}},
);
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address. " +
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click " +
"<a>here</a>.",
{communityName}, {
userId: () => {
return (
<a
href={makeUserPermalink(userId)}
rel="noreferrer noopener"
target="_blank"
>{userId}</a>
);
},
a: (sub) => {
return (
<AccessibleButton
kind="link"
onClick={this._onCommunityInviteClick}
>{sub}</AccessibleButton>
);
},
},
);
}
buttonText = _t("Go");
goButtonFn = this._startDm;
} else { // KIND_INVITE

View file

@ -16,14 +16,12 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'QuestionDialog',
propTypes: {
export default class QuestionDialog extends React.Component {
static propTypes = {
title: PropTypes.string,
description: PropTypes.node,
extraButtons: PropTypes.node,
@ -34,29 +32,27 @@ export default createReactClass({
headerImage: PropTypes.string,
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
fixedWidth: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
title: "",
description: "",
extraButtons: null,
focus: true,
hasCancelButton: true,
danger: false,
quitOnly: false,
};
},
static defaultProps = {
title: "",
description: "",
extraButtons: null,
focus: true,
hasCancelButton: true,
danger: false,
quitOnly: false,
};
onOk: function() {
onOk = () => {
this.props.onFinished(true);
},
};
onCancel: function() {
onCancel = () => {
this.props.onFinished(false);
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let primaryButtonClass = "";
@ -88,5 +84,5 @@ export default createReactClass({
</DialogButtons>
</BaseDialog>
);
},
});
}
}

View file

@ -1,116 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import DialogButtons from '../elements/DialogButtons';
export enum RebrandDialogKind {
NAG,
ONE_TIME,
}
interface IProps {
onFinished: (bool) => void;
kind: RebrandDialogKind;
targetUrl?: string;
}
export default class RebrandDialog extends React.PureComponent<IProps> {
private onDoneClick = () => {
this.props.onFinished(true);
};
private onGoToElementClick = () => {
this.props.onFinished(true);
};
private onRemindMeLaterClick = () => {
this.props.onFinished(false);
};
private getPrettyTargetUrl() {
const u = new URL(this.props.targetUrl);
let ret = u.host;
if (u.pathname !== '/') ret += u.pathname;
return ret;
}
getBodyText() {
if (this.props.kind === RebrandDialogKind.NAG) {
return _t(
"Use your account to sign in to the latest version of the app at <a />", {},
{
a: sub => <a href={this.props.targetUrl} rel="noopener noreferrer" target="_blank">{this.getPrettyTargetUrl()}</a>,
},
);
} else {
return _t(
"Youre already signed in and good to go here, but you can also grab the latest " +
"versions of the app on all platforms at <a>element.io/get-started</a>.", {},
{
a: sub => <a href="https://element.io/get-started" rel="noopener noreferrer" target="_blank">{sub}</a>,
},
);
}
}
getDialogButtons() {
if (this.props.kind === RebrandDialogKind.NAG) {
return <DialogButtons primaryButton={_t("Go to Element")}
primaryButtonClass='primary'
onPrimaryButtonClick={this.onGoToElementClick}
hasCancel={true}
cancelButton={"Remind me later"}
onCancel={this.onRemindMeLaterClick}
focus={true}
/>;
} else {
return <DialogButtons primaryButton={_t("Done")}
primaryButtonClass='primary'
hasCancel={false}
onPrimaryButtonClick={this.onDoneClick}
focus={true}
/>;
}
}
render() {
return <BaseDialog title={_t("Were excited to announce Riot is now Element!")}
className='mx_RebrandDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
hasCancel={false}
>
<div className="mx_RebrandDialog_body">{this.getBodyText()}</div>
<div className="mx_RebrandDialog_logoContainer">
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/riot-logo.svg")} alt="Riot Logo" />
<span className="mx_RebrandDialog_chevron" />
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/element-logo.svg")} alt="Element Logo" />
</div>
<div>
{_t(
"Learn more at <a>element.io/previously-riot</a>", {}, {
a: sub => <a href="https://element.io/previously-riot" rel="noopener noreferrer" target="_blank">{sub}</a>,
}
)}
</div>
{this.getDialogButtons()}
</BaseDialog>;
}
}

View file

@ -19,9 +19,9 @@ import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
export default (props) => {
const existingIssuesUrl = "https://github.com/vector-im/riot-web/issues" +
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new";
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
const description1 =
_t("If you run into any bugs or have feedback you'd like to share, " +

View file

@ -87,7 +87,7 @@ export default class RoomSettingsDialog extends React.Component {
<NotificationSettingsTab roomId={this.props.roomId} />,
));
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
if (SettingsStore.getValue("feature_bridge_state")) {
tabs.push(new Tab(
ROOM_BRIDGES_TAB,
_td("Bridges"),

View file

@ -15,38 +15,33 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'RoomUpgradeDialog',
propTypes: {
export default class RoomUpgradeDialog extends React.Component {
static propTypes = {
room: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
},
};
componentDidMount: async function() {
state = {
busy: true,
};
async componentDidMount() {
const recommended = await this.props.room.getRecommendedVersion();
this._targetVersion = recommended.version;
this.setState({busy: false});
},
}
getInitialState: function() {
return {
busy: true,
};
},
_onCancelClick: function() {
_onCancelClick = () => {
this.props.onFinished(false);
},
};
_onUpgradeClick: function() {
_onUpgradeClick = () => {
this.setState({busy: true});
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => {
this.props.onFinished(true);
@ -59,9 +54,9 @@ export default createReactClass({
}).finally(() => {
this.setState({busy: false});
});
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('views.elements.Spinner');
@ -106,5 +101,5 @@ export default createReactClass({
{buttons}
</BaseDialog>
);
},
});
}
}

View file

@ -27,9 +27,9 @@ import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IDialogProps } from "./IDialogProps";
interface IProps {
onFinished: (bool) => void;
interface IProps extends IDialogProps {
}
export default class ServerOfflineDialog extends React.PureComponent<IProps> {

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
@ -25,20 +24,18 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'SessionRestoreErrorDialog',
propTypes: {
export default class SessionRestoreErrorDialog extends React.Component {
static propTypes = {
error: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
};
_sendBugReport: function() {
_sendBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
},
};
_onClearStorageClick: function() {
_onClearStorageClick = () => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
title: _t("Sign out"),
@ -48,15 +45,15 @@ export default createReactClass({
danger: true,
onFinished: this.props.onFinished,
});
},
};
_onRefreshClick: function() {
_onRefreshClick = () => {
// Is this likely to help? Probably not, but giving only one button
// that clears your storage seems awful.
window.location.reload(true);
},
};
render: function() {
render() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -110,5 +107,5 @@ export default createReactClass({
{ dialogButtons }
</BaseDialog>
);
},
});
}
}

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as Email from '../../../email';
@ -25,31 +24,28 @@ import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
/**
/*
* Prompt the user to set an email address.
*
* On success, `onFinished(true)` is called.
*/
export default createReactClass({
displayName: 'SetEmailDialog',
propTypes: {
export default class SetEmailDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
emailAddress: '',
emailBusy: false,
};
},
state = {
emailAddress: '',
emailBusy: false,
};
onEmailAddressChanged: function(value) {
onEmailAddressChanged = value => {
this.setState({
emailAddress: value,
});
},
};
onSubmit: function() {
onSubmit = () => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -81,21 +77,21 @@ export default createReactClass({
});
});
this.setState({emailBusy: true});
},
};
onCancelled: function() {
onCancelled = () => {
this.props.onFinished(false);
},
};
onEmailDialogFinished: function(ok) {
onEmailDialogFinished = ok => {
if (ok) {
this.verifyEmailAddress();
} else {
this.setState({emailBusy: false});
}
},
};
verifyEmailAddress: function() {
verifyEmailAddress() {
this._addThreepid.checkEmailLinkClicked().then(() => {
this.props.onFinished(true);
}, (err) => {
@ -119,9 +115,9 @@ export default createReactClass({
});
}
});
},
}
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const EditableText = sdk.getComponent('elements.EditableText');
@ -161,5 +157,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -29,23 +28,27 @@ import { SAFE_LOCALPART_REGEX } from '../../../Registration';
// sending a request to the server
const USERNAME_CHECK_DEBOUNCE_MS = 250;
/**
/*
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default createReactClass({
displayName: 'SetMxIdDialog',
propTypes: {
export default class SetMxIdDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver
onDifferentServerClicked: PropTypes.func.isRequired,
// Called if the user wants to switch to login instead
onLoginClick: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this._input_value = createRef();
this._uiAuth = createRef();
this.state = {
// The entered username
username: '',
// Indicate ongoing work on the username
@ -60,21 +63,15 @@ export default createReactClass({
// Indicate error with auth
authError: '',
};
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._input_value = createRef();
this._uiAuth = createRef();
},
componentDidMount: function() {
componentDidMount() {
this._input_value.current.select();
this._matrixClient = MatrixClientPeg.get();
},
}
onValueChange: function(ev) {
onValueChange = ev => {
this.setState({
username: ev.target.value,
usernameBusy: true,
@ -99,24 +96,24 @@ export default createReactClass({
});
}, USERNAME_CHECK_DEBOUNCE_MS);
});
},
};
onKeyUp: function(ev) {
onKeyUp = ev => {
if (ev.key === Key.ENTER) {
this.onSubmit();
}
},
};
onSubmit: function(ev) {
onSubmit = ev => {
if (this._uiAuth.current) {
this._uiAuth.current.tryContinue();
}
this.setState({
doingUIAuth: true,
});
},
};
_doUsernameCheck: function() {
_doUsernameCheck() {
// We do a quick check ahead of the username availability API to ensure the
// user ID roughly looks okay from a Matrix perspective.
if (!SAFE_LOCALPART_REGEX.test(this.state.username)) {
@ -167,13 +164,13 @@ export default createReactClass({
this.setState(newState);
},
);
},
}
_generatePassword: function() {
_generatePassword() {
return Math.random().toString(36).slice(2);
},
}
_makeRegisterRequest: function(auth) {
_makeRegisterRequest = auth => {
// Not upgrading - changing mxids
const guestAccessToken = null;
if (!this._generatedPassword) {
@ -187,9 +184,9 @@ export default createReactClass({
{},
guestAccessToken,
);
},
};
_onUIAuthFinished: function(success, response) {
_onUIAuthFinished = (success, response) => {
this.setState({
doingUIAuth: false,
});
@ -207,9 +204,9 @@ export default createReactClass({
accessToken: response.access_token,
password: this._generatedPassword,
});
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
@ -303,5 +300,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -63,32 +62,25 @@ const WarmFuzzy = function(props) {
*
* On success, `onFinished()` when finished
*/
export default createReactClass({
displayName: 'SetPasswordDialog',
propTypes: {
export default class SetPasswordDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
error: null,
};
},
state = {
error: null,
};
componentDidMount: function() {
console.info('SetPasswordDialog component did mount');
},
_onPasswordChanged: function(res) {
_onPasswordChanged = res => {
Modal.createDialog(WarmFuzzy, {
didSetEmail: res.didSetEmail,
onFinished: () => {
this.props.onFinished();
},
});
},
};
_onPasswordChangeError: function(err) {
_onPasswordChangeError = err => {
let errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = _t('Failed to change password. Is your password correct?');
@ -101,9 +93,9 @@ export default createReactClass({
this.setState({
error: errMsg,
});
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const ChangePassword = sdk.getComponent('views.settings.ChangePassword');
@ -132,5 +124,5 @@ export default createReactClass({
</div>
</BaseDialog>
);
},
});
}
}

View file

@ -30,6 +30,8 @@ import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { IDialogProps } from "./IDialogProps";
const socials = [
{
@ -59,8 +61,7 @@ const socials = [
},
];
interface IProps {
onFinished: () => void;
interface IProps extends IDialogProps {
target: Room | User | Group | RoomMember | MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
}
@ -185,8 +186,8 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
title = _t('Share Room Message');
checkbox = <div>
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onClick={this.onLinkSpecificEventCheckboxClick}
checked={this.state.linkSpecificEvent}
onClick={this.onLinkSpecificEventCheckboxClick}
>
{ _t('Link to selected message') }
</StyledCheckbox>
@ -197,23 +198,26 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const encodedUrl = encodeURIComponent(matrixToUrl);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return <BaseDialog title={title}
className='mx_ShareDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
return <BaseDialog
title={title}
className='mx_ShareDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
>
<div className="mx_ShareDialog_content">
<div className="mx_ShareDialog_matrixto">
<a href={matrixToUrl}
onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link"
<a
href={matrixToUrl}
onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link"
>
{ matrixToUrl }
</a>
<a href={matrixToUrl} className="mx_ShareDialog_matrixto_copy" onClick={this.onCopyClick}>
{ _t('COPY') }
<div>&nbsp;</div>
</a>
<AccessibleTooltipButton
title={_t("Copy")}
onClick={this.onCopyClick}
className="mx_ShareDialog_matrixto_copy"
/>
</div>
{ checkbox }
<hr />

View file

@ -15,14 +15,12 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import Field from "../elements/Field";
export default createReactClass({
displayName: 'TextInputDialog',
propTypes: {
export default class TextInputDialog extends React.Component {
static propTypes = {
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
@ -36,39 +34,36 @@ export default createReactClass({
hasCancel: PropTypes.bool,
validator: PropTypes.func, // result of withValidation
fixedWidth: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
title: "",
value: "",
description: "",
focus: true,
hasCancel: true,
};
},
static defaultProps = {
title: "",
value: "",
description: "",
focus: true,
hasCancel: true,
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this._field = createRef();
this.state = {
value: this.props.value,
valid: false,
};
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._field = createRef();
},
componentDidMount: function() {
componentDidMount() {
if (this.props.focus) {
// Set the cursor at the end of the text input
// this._field.current.value = this.props.value;
this._field.current.focus();
}
},
}
onOk: async function(ev) {
onOk = async ev => {
ev.preventDefault();
if (this.props.validator) {
await this._field.current.validate({ allowEmpty: false });
@ -80,27 +75,27 @@ export default createReactClass({
}
}
this.props.onFinished(true, this.state.value);
},
};
onCancel: function() {
onCancel = () => {
this.props.onFinished(false);
},
};
onChange: function(ev) {
onChange = ev => {
this.setState({
value: ev.target.value,
});
},
};
onValidate: async function(fieldState) {
onValidate = async fieldState => {
const result = await this.props.validator(fieldState);
this.setState({
valid: result.valid,
});
return result;
},
};
render: function() {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
@ -137,5 +132,5 @@ export default createReactClass({
/>
</BaseDialog>
);
},
});
}
}

View file

@ -54,7 +54,7 @@ export default class UserSettingsDialog extends React.Component {
super();
this.state = {
mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"),
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
};
}
@ -116,7 +116,7 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
if (SdkConfig.get()['showLabsSettings']) {
tabs.push(new Tab(
USER_LABS_TAB,
_td("Labs"),

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { debounce } from 'lodash';
import {debounce} from "lodash";
import classNames from 'classnames';
import React from 'react';
import PropTypes from "prop-types";

View file

@ -25,6 +25,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
title: string;
tooltip?: React.ReactNode;
tooltipClassName?: string;
forceHide?: boolean;
}
interface IState {
@ -39,7 +40,16 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
};
}
componentDidUpdate(prevProps: Readonly<ITooltipProps>) {
if (!prevProps.forceHide && this.props.forceHide && this.state.hover) {
this.setState({
hover: false,
});
}
}
onMouseOver = () => {
if (this.props.forceHide) return;
this.setState({
hover: true,
});

View file

@ -16,16 +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/dispatcher';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
export default createReactClass({
displayName: 'RoleButton',
propTypes: {
export default class ActionButton extends React.Component {
static propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
action: PropTypes.string.isRequired,
@ -33,39 +30,35 @@ export default createReactClass({
label: PropTypes.string.isRequired,
iconPath: PropTypes.string,
className: PropTypes.string,
},
};
getDefaultProps: function() {
return {
size: "25",
tooltip: false,
};
},
static defaultProps = {
size: "25",
tooltip: false,
};
getInitialState: function() {
return {
showTooltip: false,
};
},
state = {
showTooltip: false,
};
_onClick: function(ev) {
_onClick = (ev) => {
ev.stopPropagation();
Analytics.trackEvent('Action Button', 'click', this.props.action);
dis.dispatch({action: this.props.action});
},
};
_onMouseEnter: function() {
_onMouseEnter = () => {
if (this.props.tooltip) this.setState({showTooltip: true});
if (this.props.mouseOverAction) {
dis.dispatch({action: this.props.mouseOverAction});
}
},
};
_onMouseLeave: function() {
_onMouseLeave = () => {
this.setState({showTooltip: false});
},
};
render: function() {
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let tooltip;
@ -94,5 +87,5 @@ export default createReactClass({
{ tooltip }
</AccessibleButton>
);
},
});
}
}

View file

@ -17,15 +17,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress';
export default createReactClass({
displayName: 'AddressSelector',
propTypes: {
export default class AddressSelector extends React.Component {
static propTypes = {
onSelected: PropTypes.func.isRequired,
// List of the addresses to display
@ -37,90 +34,91 @@ export default createReactClass({
// Element to put as a header on top of the list
header: PropTypes.node,
},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
selected: this.props.selected === undefined ? 0 : this.props.selected,
hover: false,
};
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(props) {
UNSAFE_componentWillReceiveProps(props) {
// Make sure the selected item isn't outside the list bounds
const selected = this.state.selected;
const maxSelected = this._maxSelected(props.addressList);
if (selected > maxSelected) {
this.setState({ selected: maxSelected });
}
},
}
componentDidUpdate: function() {
componentDidUpdate() {
// As the user scrolls with the arrow keys keep the selected item
// at the top of the window.
if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
const elementHeight = this.addressListElement.getBoundingClientRect().height;
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
}
},
}
moveSelectionTop: function() {
moveSelectionTop = () => {
if (this.state.selected > 0) {
this.setState({
selected: 0,
hover: false,
});
}
},
};
moveSelectionUp: function() {
moveSelectionUp = () => {
if (this.state.selected > 0) {
this.setState({
selected: this.state.selected - 1,
hover: false,
});
}
},
};
moveSelectionDown: function() {
moveSelectionDown = () => {
if (this.state.selected < this._maxSelected(this.props.addressList)) {
this.setState({
selected: this.state.selected + 1,
hover: false,
});
}
},
};
chooseSelection: function() {
chooseSelection = () => {
this.selectAddress(this.state.selected);
},
};
onClick: function(index) {
onClick = index => {
this.selectAddress(index);
},
};
onMouseEnter: function(index) {
onMouseEnter = index => {
this.setState({
selected: index,
hover: true,
});
},
};
onMouseLeave: function() {
onMouseLeave = () => {
this.setState({ hover: false });
},
};
selectAddress: function(index) {
selectAddress = index => {
// Only try to select an address if one exists
if (this.props.addressList.length !== 0) {
this.props.onSelected(index);
this.setState({ hover: false });
}
},
};
createAddressListTiles: function() {
const self = this;
createAddressListTiles() {
const AddressTile = sdk.getComponent("elements.AddressTile");
const maxSelected = this._maxSelected(this.props.addressList);
const addressList = [];
@ -157,15 +155,15 @@ export default createReactClass({
}
}
return addressList;
},
}
_maxSelected: function(list) {
_maxSelected(list) {
const listSize = list.length === 0 ? 0 : list.length - 1;
const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
return maxSelected;
},
}
render: function() {
render() {
const classes = classNames({
"mx_AddressSelector": true,
"mx_AddressSelector_empty": this.props.addressList.length === 0,
@ -177,5 +175,5 @@ export default createReactClass({
{ this.createAddressListTiles() }
</div>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@ -25,25 +24,21 @@ import { _t } from '../../../languageHandler';
import { UserAddressType } from '../../../UserAddress.js';
export default createReactClass({
displayName: 'AddressTile',
propTypes: {
export default class AddressTile extends React.Component {
static propTypes = {
address: UserAddressType.isRequired,
canDismiss: PropTypes.bool,
onDismissed: PropTypes.func,
justified: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
canDismiss: false,
onDismissed: function() {}, // NOP
justified: false,
};
},
static defaultProps = {
canDismiss: false,
onDismissed: function() {}, // NOP
justified: false,
};
render: function() {
render() {
const address = this.props.address;
const name = address.displayName || address.address;
@ -144,5 +139,5 @@ export default createReactClass({
{ dismiss }
</div>
);
},
});
}
}

View file

@ -311,16 +311,16 @@ export default class AppTile extends React.Component {
this.props.onEditClick();
} else {
// TODO: Open the right manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
'type_' + this.props.type,
'type_' + this.props.app.type,
this.props.app.id,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
'type_' + this.props.type,
'type_' + this.props.app.type,
this.props.app.id,
);
}
@ -361,14 +361,14 @@ export default class AppTile extends React.Component {
return terminationPromise.finally(() => {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
// This is relative to where the Element instance is located.
this._appFrame.current.src = 'about:blank';
}
@ -727,7 +727,7 @@ export default class AppTile extends React.Component {
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to programmatically remove the sandbox attribute, but
// this would only be for content hosted on the same origin as the riot client: anything
// this would only be for content hosted on the same origin as the element client: anything
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
@ -735,7 +735,7 @@ export default class AppTile extends React.Component {
// Additional iframe feature pemissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media; autoplay;";
const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;";
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
@ -924,7 +924,7 @@ AppTile.propTypes = {
// Optionally show the reload widget icon
// This is not currently intended for use with production widgets. However
// it can be useful when developing persistent widgets in order to avoid
// having to reload all of riot to get new widget content.
// having to reload all of Element to get new widget content.
showReload: PropTypes.bool,
// Widget capabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX

View file

@ -18,16 +18,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.
*/
export default createReactClass({
displayName: "DialogButtons",
propTypes: {
export default class DialogButtons extends React.Component {
static propTypes = {
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
@ -57,20 +54,18 @@ export default createReactClass({
// disables only the primary button
primaryDisabled: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
hasCancel: true,
disabled: false,
};
},
static defaultProps = {
hasCancel: true,
disabled: false,
};
_onCancelClick: function() {
_onCancelClick = () => {
this.props.onCancel();
},
};
render: function() {
render() {
let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass;
@ -104,5 +99,5 @@ export default createReactClass({
</button>
</div>
);
},
});
}
}

View file

@ -34,7 +34,6 @@ export interface ILocationState {
}
export default class Draggable extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
@ -77,5 +76,4 @@ export default class Draggable extends React.Component<IProps, IState> {
render() {
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />;
}
}
}

View file

@ -17,13 +17,10 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {Key} from "../../../Keyboard";
export default createReactClass({
displayName: 'EditableText',
propTypes: {
export default class EditableText extends React.Component {
static propTypes = {
onValueChanged: PropTypes.func,
initialValue: PropTypes.string,
label: PropTypes.string,
@ -36,60 +33,58 @@ export default createReactClass({
// Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: PropTypes.bool,
editable: PropTypes.bool,
},
};
Phases: {
static Phases = {
Display: "display",
Edit: "edit",
},
};
getDefaultProps: function() {
return {
onValueChanged: function() {},
initialValue: '',
label: '',
placeholder: '',
editable: true,
className: "mx_EditableText",
placeholderClassName: "mx_EditableText_placeholder",
blurToSubmit: false,
};
},
static defaultProps = {
onValueChanged() {},
initialValue: '',
label: '',
placeholder: '',
editable: true,
className: "mx_EditableText",
placeholderClassName: "mx_EditableText_placeholder",
blurToSubmit: false,
};
getInitialState: function() {
return {
phase: this.Phases.Display,
};
},
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(nextProps) {
if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue;
if (this._editable_div.current) {
this.showPlaceholder(!this.value);
}
}
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// we track value as an JS object field rather than in React state
// as React doesn't play nice with contentEditable.
this.value = '';
this.placeholder = false;
this._editable_div = createRef();
},
}
componentDidMount: function() {
state = {
phase: EditableText.Phases.Display,
};
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue;
if (this._editable_div.current) {
this.showPlaceholder(!this.value);
}
}
}
componentDidMount() {
this.value = this.props.initialValue;
if (this._editable_div.current) {
this.showPlaceholder(!this.value);
}
},
}
showPlaceholder: function(show) {
showPlaceholder = show => {
if (show) {
this._editable_div.current.textContent = this.props.placeholder;
this._editable_div.current.setAttribute("class", this.props.className
@ -101,38 +96,36 @@ export default createReactClass({
this._editable_div.current.setAttribute("class", this.props.className);
this.placeholder = false;
}
},
};
getValue: function() {
return this.value;
},
getValue = () => this.value;
setValue: function(value) {
setValue = value => {
this.value = value;
this.showPlaceholder(!this.value);
},
};
edit: function() {
edit = () => {
this.setState({
phase: this.Phases.Edit,
phase: EditableText.Phases.Edit,
});
},
};
cancelEdit: function() {
cancelEdit = () => {
this.setState({
phase: this.Phases.Display,
phase: EditableText.Phases.Display,
});
this.value = this.props.initialValue;
this.showPlaceholder(!this.value);
this.onValueChanged(false);
this._editable_div.current.blur();
},
};
onValueChanged: function(shouldSubmit) {
onValueChanged = shouldSubmit => {
this.props.onValueChanged(this.value, shouldSubmit);
},
};
onKeyDown: function(ev) {
onKeyDown = ev => {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (this.placeholder) {
@ -145,9 +138,9 @@ export default createReactClass({
}
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
},
};
onKeyUp: function(ev) {
onKeyUp = ev => {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (!ev.target.textContent) {
@ -163,17 +156,17 @@ export default createReactClass({
}
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
},
};
onClickDiv: function(ev) {
onClickDiv = ev => {
if (!this.props.editable) return;
this.setState({
phase: this.Phases.Edit,
phase: EditableText.Phases.Edit,
});
},
};
onFocus: function(ev) {
onFocus = ev => {
//ev.target.setSelectionRange(0, ev.target.textContent.length);
const node = ev.target.childNodes[0];
@ -186,21 +179,21 @@ export default createReactClass({
sel.removeAllRanges();
sel.addRange(range);
}
},
};
onFinish: function(ev, shouldSubmit) {
onFinish = (ev, shouldSubmit) => {
const self = this;
const submit = (ev.key === Key.ENTER) || shouldSubmit;
this.setState({
phase: this.Phases.Display,
phase: EditableText.Phases.Display,
}, () => {
if (this.value !== this.props.initialValue) {
self.onValueChanged(submit);
}
});
},
};
onBlur: function(ev) {
onBlur = ev => {
const sel = window.getSelection();
sel.removeAllRanges();
@ -211,13 +204,15 @@ export default createReactClass({
}
this.showPlaceholder(!this.value);
},
};
render: function() {
render() {
const {className, editable, initialValue, label, labelClassName} = this.props;
let editableEl;
if (!editable || (this.state.phase === this.Phases.Display && (label || labelClassName) && !this.value)) {
if (!editable || (this.state.phase === EditableText.Phases.Display &&
(label || labelClassName) && !this.value)
) {
// show the label
editableEl = <div className={className + " " + labelClassName} onClick={this.onClickDiv}>
{ label || initialValue }
@ -234,5 +229,5 @@ export default createReactClass({
}
return editableEl;
},
});
}
}

View file

@ -72,7 +72,7 @@ export default class ErrorBoundary extends React.PureComponent {
render() {
if (this.state.error) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new";
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>

View file

@ -39,11 +39,13 @@ interface IProps {
className: string;
}
/* eslint-disable camelcase */
interface IState {
userId: string;
displayname: string;
avatar_url: string;
}
/* eslint-enable camelcase */
const AVATAR_SIZE = 32;
@ -63,19 +65,18 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
const client = MatrixClientPeg.get();
const userId = client.getUserId();
const profileInfo = await client.getProfileInfo(userId);
const avatar_url = Avatar.avatarUrlForUser(
const avatarUrl = Avatar.avatarUrlForUser(
{avatarUrl: profileInfo.avatar_url},
AVATAR_SIZE, AVATAR_SIZE, "crop");
this.setState({
userId,
displayname: profileInfo.displayname,
avatar_url,
avatar_url: avatarUrl,
});
}
private fakeEvent({userId, displayname, avatar_url}: IState) {
private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
// Fake it till we make it
const event = new MatrixEvent(JSON.parse(`{
"type": "m.room.message",
@ -85,12 +86,12 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
"msgtype": "m.text",
"body": "${this.props.message}",
"displayname": "${displayname}",
"avatar_url": "${avatar_url}"
"avatar_url": "${avatarUrl}"
},
"msgtype": "m.text",
"body": "${this.props.message}",
"displayname": "${displayname}",
"avatar_url": "${avatar_url}"
"avatar_url": "${avatarUrl}"
},
"unsigned": {
"age": 97
@ -104,7 +105,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
name: displayname,
userId: userId,
getAvatarUrl: (..._) => {
return avatar_url;
return avatarUrl;
},
};
@ -114,13 +115,10 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
public render() {
const event = this.fakeEvent(this.state);
let className = classnames(
this.props.className,
{
"mx_IRCLayout": this.props.useIRCLayout,
"mx_GroupLayout": !this.props.useIRCLayout,
}
);
const className = classnames(this.props.className, {
"mx_IRCLayout": this.props.useIRCLayout,
"mx_GroupLayout": !this.props.useIRCLayout,
});
return <div className={className}>
<EventTile mxEvent={event} useIRCLayout={this.props.useIRCLayout} />

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { debounce } from 'lodash';
import {debounce} from "lodash";
import {IFieldState, IValidationResult} from "./Validation";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
@ -198,11 +198,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
}
}
public render() {
const {
element, prefixComponent, postfixComponent, className, onValidate, children,
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { element, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props;
// Set some defaults for the <input> element

View file

@ -78,7 +78,12 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
private onMoueUp(event: MouseEvent) {
if (this.props.roomId) {
SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width);
SettingsStore.setValue(
"ircDisplayNameWidth",
this.props.roomId,
SettingLevel.ROOM_DEVICE,
this.state.width,
);
}
}

View file

@ -0,0 +1,72 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import Tooltip from './Tooltip';
import { _t } from "../../../languageHandler";
interface ITooltipProps {
tooltip?: React.ReactNode;
tooltipClassName?: string;
}
interface IState {
hover: boolean;
}
export default class InfoTooltip extends React.PureComponent<ITooltipProps, IState> {
constructor(props: ITooltipProps) {
super(props);
this.state = {
hover: false,
};
}
onMouseOver = () => {
this.setState({
hover: true,
});
};
onMouseLeave = () => {
this.setState({
hover: false,
});
};
render() {
const {tooltip, children, tooltipClassName} = this.props;
const title = _t("Information");
// Tooltip are forced on the right for a more natural feel to them on info icons
const tip = this.state.hover ? <Tooltip
className="mx_InfoTooltip_container"
tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
label={tooltip || title}
forceOnRight={true}
/> : <div />;
return (
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
<span className="mx_InfoTooltip_icon" aria-label={title} />
{children}
{tip}
</div>
);
}
}

View file

@ -15,20 +15,17 @@ limitations under the License.
*/
import React from "react";
import createReactClass from 'create-react-class';
import {_t} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({
displayName: 'InlineSpinner',
render: function() {
export default class InlineSpinner extends React.Component {
render() {
const w = this.props.w || 16;
const h = this.props.h || 16;
const imgClass = this.props.imgClassName || "";
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
if (SettingsStore.getValue('feature_new_spinner')) {
imageSource = require("../../../../res/img/spinner.svg");
} else {
imageSource = require("../../../../res/img/spinner.gif");
@ -45,5 +42,5 @@ export default createReactClass({
/>
</div>
);
},
});
}
}

View file

@ -34,7 +34,7 @@ export default class ManageIntegsButton extends React.Component {
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
if (SettingsStore.getValue("feature_many_integration_managers")) {
managers.openAll(this.props.room);
} else {
managers.getPrimaryManager().open(this.props.room);

View file

@ -18,17 +18,14 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import * as sdk from "../../../index";
import {MatrixEvent} from "matrix-js-sdk";
import {isValid3pidInvite} from "../../../RoomInvite";
export default createReactClass({
displayName: 'MemberEventListSummary',
propTypes: {
export default class MemberEventListSummary extends React.Component {
static propTypes = {
// An array of member events to summarise
events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
// An array of EventTiles to render when expanded
@ -43,17 +40,15 @@ export default createReactClass({
onToggle: PropTypes.func,
// Whether or not to begin with state.expanded=true
startExpanded: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
};
},
static defaultProps = {
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
};
shouldComponentUpdate: function(nextProps) {
shouldComponentUpdate(nextProps) {
// Update if
// - The number of summarised events has changed
// - or if the summary is about to toggle to become collapsed
@ -62,7 +57,7 @@ export default createReactClass({
nextProps.events.length !== this.props.events.length ||
nextProps.events.length < this.props.threshold
);
},
}
/**
* Generate the text for users aggregated by their transition sequences (`eventAggregates`) where
@ -73,7 +68,7 @@ export default createReactClass({
* `Object.keys(eventAggregates)`.
* @returns {string} the textual summary of the aggregated events that occurred.
*/
_generateSummary: function(eventAggregates, orderedTransitionSequences) {
_generateSummary(eventAggregates, orderedTransitionSequences) {
const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions];
const nameList = this._renderNameList(userNames);
@ -105,7 +100,7 @@ export default createReactClass({
}
return summaries.join(", ");
},
}
/**
* @param {string[]} users an array of user display names or user IDs.
@ -113,9 +108,9 @@ export default createReactClass({
* more items in `users` than `this.props.summaryLength`, which is the number of names
* included before "and [n] others".
*/
_renderNameList: function(users) {
_renderNameList(users) {
return formatCommaSeparatedList(users, this.props.summaryLength);
},
}
/**
* Canonicalise an array of transitions such that some pairs of transitions become
@ -124,7 +119,7 @@ export default createReactClass({
* @param {string[]} transitions an array of transitions.
* @returns {string[]} an array of transitions.
*/
_getCanonicalTransitions: function(transitions) {
_getCanonicalTransitions(transitions) {
const modMap = {
'joined': {
'after': 'left',
@ -155,7 +150,7 @@ export default createReactClass({
res.push(transition);
}
return res;
},
}
/**
* Transform an array of transitions into an array of transitions and how many times
@ -171,7 +166,7 @@ export default createReactClass({
* @param {string[]} transitions the array of transitions to transform.
* @returns {object[]} an array of coalesced transitions.
*/
_coalesceRepeatedTransitions: function(transitions) {
_coalesceRepeatedTransitions(transitions) {
const res = [];
for (let i = 0; i < transitions.length; i++) {
if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
@ -184,7 +179,7 @@ export default createReactClass({
}
}
return res;
},
}
/**
* For a certain transition, t, describe what happened to the users that
@ -268,11 +263,11 @@ export default createReactClass({
}
return res;
},
}
_getTransitionSequence: function(events) {
_getTransitionSequence(events) {
return events.map(this._getTransition);
},
}
/**
* Label a given membership event, `e`, where `getContent().membership` has
@ -282,7 +277,7 @@ export default createReactClass({
* @returns {string?} the transition type given to this event. This defaults to `null`
* if a transition is not recognised.
*/
_getTransition: function(e) {
_getTransition(e) {
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
// Handle 3pid invites the same as invites so they get bundled together
if (!isValid3pidInvite(e.mxEvent)) {
@ -323,9 +318,9 @@ export default createReactClass({
}
default: return null;
}
},
}
_getAggregate: function(userEvents) {
_getAggregate(userEvents) {
// A map of aggregate type to arrays of display names. Each aggregate type
// is a comma-delimited string of transitions, e.g. "joined,left,kicked".
// The array of display names is the array of users who went through that
@ -364,9 +359,9 @@ export default createReactClass({
names: aggregate,
indices: aggregateIndices,
};
},
}
render: function() {
render() {
const eventsToRender = this.props.events;
// Map user IDs to an array of objects:
@ -420,5 +415,5 @@ export default createReactClass({
children={this.props.children}
summaryMembers={avatarMembers}
summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />;
},
});
}
}

View file

@ -16,49 +16,44 @@ 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 * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
export default createReactClass({
displayName: 'PersistentApp',
export default class PersistentApp extends React.Component {
state = {
roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
};
getInitialState: function() {
return {
roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
};
},
componentDidMount: function() {
componentDidMount() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
},
}
_onRoomViewStoreUpdate: function(payload) {
_onRoomViewStoreUpdate = payload => {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({
roomId: RoomViewStore.getRoomId(),
});
},
};
_onActiveWidgetStoreUpdate: function() {
_onActiveWidgetStoreUpdate = () => {
this.setState({
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
});
},
};
render: function() {
render() {
if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (this.state.roomId !== persistentWidgetInRoomId) {
@ -91,6 +86,6 @@ export default createReactClass({
}
}
return null;
},
});
}
}

View file

@ -16,7 +16,6 @@ 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 * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
@ -32,27 +31,29 @@ import {Action} from "../../../dispatcher/actions";
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
const Pill = createReactClass({
statics: {
isPillUrl: (url) => {
return !!getPrimaryPermalinkEntity(url);
},
isMessagePillUrl: (url) => {
return !!REGEX_LOCAL_PERMALINK.exec(url);
},
roomNotifPos: (text) => {
return text.indexOf("@room");
},
roomNotifLen: () => {
return "@room".length;
},
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
TYPE_GROUP_MENTION: 'TYPE_GROUP_MENTION',
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
},
class Pill extends React.Component {
static isPillUrl(url) {
return !!getPrimaryPermalinkEntity(url);
}
props: {
static isMessagePillUrl(url) {
return !!REGEX_LOCAL_PERMALINK.exec(url);
}
static roomNotifPos(text) {
return text.indexOf("@room");
}
static roomNotifLen() {
return "@room".length;
}
static TYPE_USER_MENTION = 'TYPE_USER_MENTION';
static TYPE_ROOM_MENTION = 'TYPE_ROOM_MENTION';
static TYPE_GROUP_MENTION = 'TYPE_GROUP_MENTION';
static TYPE_AT_ROOM_MENTION = 'TYPE_AT_ROOM_MENTION'; // '@room' mention
static propTypes = {
// The Type of this Pill. If url is given, this is auto-detected.
type: PropTypes.string,
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
@ -65,25 +66,24 @@ const Pill = createReactClass({
shouldShowPillAvatar: PropTypes.bool,
// Whether to render this pill as if it were highlit by a selection
isSelected: PropTypes.bool,
},
};
getInitialState() {
return {
// ID/alias of the room/user
resourceId: null,
// Type of pill
pillType: null,
state = {
// ID/alias of the room/user
resourceId: null,
// Type of pill
pillType: null,
// The member related to the user pill
member: null,
// The group related to the group pill
group: null,
// The room related to the room pill
room: null,
};
},
// The member related to the user pill
member: null,
// The group related to the group pill
group: null,
// The room related to the room pill
room: null,
};
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
async UNSAFE_componentWillReceiveProps(nextProps) {
let resourceId;
let prefix;
@ -155,7 +155,7 @@ const Pill = createReactClass({
}
}
this.setState({resourceId, pillType, member, group, room});
},
}
componentDidMount() {
this._unmounted = false;
@ -163,13 +163,13 @@ const Pill = createReactClass({
// eslint-disable-next-line new-cap
this.UNSAFE_componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves.
},
}
componentWillUnmount() {
this._unmounted = true;
},
}
doProfileLookup: function(userId, member) {
doProfileLookup(userId, member) {
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
if (this._unmounted) {
return;
@ -188,15 +188,16 @@ const Pill = createReactClass({
}).catch((err) => {
console.error('Could not retrieve profile data for ' + userId + ':', err);
});
},
}
onUserPillClicked: function() {
onUserPillClicked = () => {
dis.dispatch({
action: Action.ViewUser,
member: this.state.member,
});
},
render: function() {
};
render() {
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
@ -285,7 +286,7 @@ const Pill = createReactClass({
// Deliberately render nothing if the URL isn't recognised
return null;
}
},
});
}
}
export default Pill;

View file

@ -16,16 +16,13 @@ limitations under the License.
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";
import {Key} from "../../../Keyboard";
export default createReactClass({
displayName: 'PowerSelector',
propTypes: {
export default class PowerSelector extends React.Component {
static propTypes = {
value: PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: PropTypes.number.isRequired,
@ -42,10 +39,17 @@ export default createReactClass({
// The name to annotate the selector with
label: PropTypes.string,
},
}
getInitialState: function() {
return {
static defaultProps = {
maxValue: Infinity,
usersDefault: 0,
};
constructor(props) {
super(props);
this.state = {
levelRoleMap: {},
// List of power levels to show in the drop-down
options: [],
@ -53,26 +57,17 @@ export default createReactClass({
customValue: this.props.value,
selectValue: 0,
};
},
getDefaultProps: function() {
return {
maxValue: Infinity,
usersDefault: 0,
};
},
componentDidMount: function() {
// TODO: [REACT-WARNING] Move this to class constructor
this._initStateFromProps(this.props);
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
this._initStateFromProps(newProps);
},
}
_initStateFromProps: function(newProps) {
_initStateFromProps(newProps) {
// This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter(level => {
@ -92,9 +87,9 @@ export default createReactClass({
customLevel: newProps.value,
selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value,
});
},
}
onSelectChange: function(event) {
onSelectChange = event => {
const isCustom = event.target.value === "SELECT_VALUE_CUSTOM";
if (isCustom) {
this.setState({custom: true});
@ -102,20 +97,20 @@ export default createReactClass({
this.props.onChange(event.target.value, this.props.powerLevelKey);
this.setState({selectValue: event.target.value});
}
},
};
onCustomChange: function(event) {
onCustomChange = event => {
this.setState({customValue: event.target.value});
},
};
onCustomBlur: function(event) {
onCustomBlur = event => {
event.preventDefault();
event.stopPropagation();
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
},
};
onCustomKeyDown: function(event) {
onCustomKeyDown = event => {
if (event.key === Key.ENTER) {
event.preventDefault();
event.stopPropagation();
@ -127,9 +122,9 @@ export default createReactClass({
// handle the onBlur safely.
event.target.blur();
}
},
};
render: function() {
render() {
let picker;
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) {
@ -166,5 +161,5 @@ export default createReactClass({
{ picker }
</div>
);
},
});
}
}

View file

@ -41,7 +41,7 @@ const QRCode: React.FC<IProps> = ({data, className, ...options}) => {
return () => {
cancelled = true;
};
}, [JSON.stringify(data), options]);
}, [JSON.stringify(data), options]); // eslint-disable-line react-hooks/exhaustive-deps
return <div className={classNames("mx_QRCode", className)}>
{ dataUri ? <img src={dataUri} className="mx_VerificationQRCode" alt={_t("QR Code")} /> : <Spinner /> }

View file

@ -45,8 +45,8 @@ export default class ReplyThread extends React.Component {
static contextType = MatrixClientContext;
constructor(props) {
super(props);
constructor(props, context) {
super(props, context);
this.state = {
// The loaded events to be rendered as linear-replies
@ -335,8 +335,14 @@ export default class ReplyThread extends React.Component {
{
_t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
url={makeUserPermalink(ev.getSender())} shouldShowPillAvatar={true} />,
'pill': (
<Pill
type={Pill.TYPE_USER_MENTION}
room={room}
url={makeUserPermalink(ev.getSender())}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>
),
})
}
</blockquote>;

View file

@ -45,7 +45,7 @@ export default class Slider extends React.Component<IProps> {
// non linear slider.
private offset(values: number[], value: number): number {
// the index of the first number greater than value.
let closest = values.reduce((prev, curr) => {
const closest = values.reduce((prev, curr) => {
return (value > curr ? prev + 1 : prev);
}, 0);
@ -68,17 +68,16 @@ export default class Slider extends React.Component<IProps> {
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue);
return 100 * (closest - 1 + linearInterpolation) * intervalWidth;
}
render(): React.ReactNode {
const dots = this.props.values.map(v =>
<Dot active={v <= this.props.value}
label={this.props.displayFunc(v)}
onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
key={v}
disabled={this.props.disabled}
/>);
const dots = this.props.values.map(v => <Dot
active={v <= this.props.value}
label={this.props.displayFunc(v)}
onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
key={v}
disabled={this.props.disabled}
/>);
let selection = null;
@ -93,7 +92,7 @@ export default class Slider extends React.Component<IProps> {
return <div className="mx_Slider">
<div>
<div className="mx_Slider_bar">
<hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)}/>
<hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)} />
{ selection }
</div>
<div className="mx_Slider_dotContainer">

View file

@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
if (SettingsStore.getValue('feature_new_spinner')) {
imageSource = require("../../../../res/img/spinner.svg");
} else {
imageSource = require("../../../../res/img/spinner.gif");

View file

@ -17,8 +17,6 @@ limitations under the License.
import React from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
const CHECK_BOX_SVG = require("../../../../res/img/feather-customised/check.svg");
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
}
@ -39,13 +37,14 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
}
public render() {
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { children, className, ...otherProps } = this.props;
return <span className={"mx_Checkbox " + className}>
<input id={this.id} {...otherProps} type="checkbox" />
<label htmlFor={this.id}>
{/* Using the div to center the image */}
<div className="mx_Checkbox_background">
<img src={CHECK_BOX_SVG}/>
<img src={require("../../../../res/img/feather-customised/check.svg")} />
</div>
<div>
{ this.props.children }
@ -53,4 +52,4 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
</label>
</span>;
}
}
}

View file

@ -25,6 +25,7 @@ interface IDefinition<T extends string> {
disabled?: boolean;
label: React.ReactChild;
description?: React.ReactChild;
checked?: boolean; // If provided it will override the value comparison done in the group
}
interface IProps<T extends string> {
@ -33,7 +34,7 @@ interface IProps<T extends string> {
definitions: IDefinition<T>[];
value?: T; // if not provided no options will be selected
outlined?: boolean;
onChange(newValue: T);
onChange(newValue: T): void;
}
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
@ -46,7 +47,7 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
<StyledRadioButton
className={classNames(className, d.className)}
onChange={_onChange}
checked={d.value === value}
checked={d.checked !== undefined ? d.checked : d.value === value}
name={name}
value={d.value}
disabled={d.disabled}

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
@ -30,36 +29,32 @@ import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - 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 createReactClass({
displayName: 'TagTile',
propTypes: {
export default class TagTile extends React.Component {
static propTypes = {
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
contextMenuButtonRef: PropTypes.object,
openMenu: PropTypes.func,
menuDisplayed: PropTypes.bool,
},
selected: PropTypes.bool,
};
statics: {
contextType: MatrixClientContext,
},
static contextType = MatrixClientContext;
getInitialState() {
return {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
},
state = {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
componentDidMount() {
this.unmounted = false;
@ -69,16 +64,16 @@ export default createReactClass({
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
},
}
componentWillUnmount() {
this.unmounted = true;
if (this.props.tag[0] === '+') {
FlairStore.removeListener('updateGroupProfile', this._onFlairStoreUpdated);
}
},
}
_onFlairStoreUpdated() {
_onFlairStoreUpdated = () => {
if (this.unmounted) return;
FlairStore.getGroupProfileCached(
this.context,
@ -89,14 +84,14 @@ export default createReactClass({
}).catch((err) => {
console.warn('Could not fetch group profile for ' + this.props.tag, err);
});
},
};
_refreshGroup(groupId) {
GroupStore.refreshGroupRooms(groupId);
GroupStore.refreshGroupMembers(groupId);
},
}
onClick: function(e) {
onClick = e => {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
@ -109,25 +104,27 @@ export default createReactClass({
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
},
};
onMouseOver: function() {
onMouseOver = () => {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
this.setState({ hover: true });
},
};
onMouseLeave: function() {
onMouseLeave = () => {
this.setState({ hover: false });
},
};
openMenu: function(e) {
openMenu = e => {
// Prevent the TagTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
this.setState({ hover: false });
this.props.openMenu();
},
};
render: function() {
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
@ -137,9 +134,12 @@ export default createReactClass({
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
const className = classNames({
mx_TagTile: true,
mx_TagTile_selected: this.props.selected,
mx_TagTile_prototype: isPrototype,
mx_TagTile_selected: this.props.selected && !isPrototype,
mx_TagTile_selected_prototype: this.props.selected && isPrototype,
});
const badge = TagOrderStore.getGroupBadge(this.props.tag);
@ -185,5 +185,5 @@ export default createReactClass({
{badgeElement}
</div>
</AccessibleTooltipButton>;
},
});
}
}

View file

@ -17,49 +17,44 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import Tinter from "../../../Tinter";
const TintableSvg = createReactClass({
displayName: 'TintableSvg',
propTypes: {
class TintableSvg extends React.Component {
static propTypes = {
src: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
},
};
statics: {
// list of currently mounted TintableSvgs
mounts: {},
idSequence: 0,
},
// list of currently mounted TintableSvgs
static mounts = {};
static idSequence = 0;
componentDidMount: function() {
componentDidMount() {
this.fixups = [];
this.id = TintableSvg.idSequence++;
TintableSvg.mounts[this.id] = this;
},
}
componentWillUnmount: function() {
componentWillUnmount() {
delete TintableSvg.mounts[this.id];
},
}
tint: function() {
tint = () => {
// TODO: only bother running this if the global tint settings have changed
// since we loaded!
Tinter.applySvgFixups(this.fixups);
},
};
onLoad: function(event) {
onLoad = event => {
// console.log("TintableSvg.onLoad for " + this.props.src);
this.fixups = Tinter.calcSvgFixups([event.target]);
Tinter.applySvgFixups(this.fixups);
},
};
render: function() {
render() {
return (
<object className={"mx_TintableSvg " + (this.props.className ? this.props.className : "")}
type="image/svg+xml"
@ -70,8 +65,8 @@ const TintableSvg = createReactClass({
tabIndex="-1"
/>
);
},
});
}
}
// Register with the Tinter so that we will be told if the tint changes
Tinter.registerTintable(function() {

View file

@ -16,31 +16,26 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'TooltipButton',
export default class TooltipButton extends React.Component {
state = {
hover: false,
};
getInitialState: function() {
return {
hover: false,
};
},
onMouseOver: function() {
onMouseOver = () => {
this.setState({
hover: true,
});
},
};
onMouseLeave: function() {
onMouseLeave = () => {
this.setState({
hover: false,
});
},
};
render: function() {
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const tip = this.state.hover ? <Tooltip
className="mx_TooltipButton_container"
@ -53,5 +48,5 @@ export default createReactClass({
{ tip }
</div>
);
},
});
}
}

View file

@ -17,13 +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';
export default createReactClass({
displayName: 'TruncatedList',
propTypes: {
export default class TruncatedList extends React.Component {
static propTypes = {
// The number of elements to show before truncating. If negative, no truncation is done.
truncateAt: PropTypes.number,
// The className to apply to the wrapping div
@ -40,20 +37,18 @@ export default createReactClass({
// A function which will be invoked when an overflow element is required.
// This will be inserted after the children.
createOverflowElement: PropTypes.func,
},
};
getDefaultProps: function() {
return {
truncateAt: 2,
createOverflowElement: function(overflowCount, totalCount) {
return (
<div>{ _t("And %(count)s more...", {count: overflowCount}) }</div>
);
},
};
},
static defaultProps ={
truncateAt: 2,
createOverflowElement(overflowCount, totalCount) {
return (
<div>{ _t("And %(count)s more...", {count: overflowCount}) }</div>
);
},
};
_getChildren: function(start, end) {
_getChildren(start, end) {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildren(start, end);
} else {
@ -64,9 +59,9 @@ export default createReactClass({
return c != null;
}).slice(start, end);
}
},
}
_getChildCount: function() {
_getChildCount() {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildCount();
} else {
@ -74,9 +69,9 @@ export default createReactClass({
return c != null;
}).length;
}
},
}
render: function() {
render() {
let overflowNode = null;
const totalChildren = this._getChildCount();
@ -98,5 +93,5 @@ export default createReactClass({
{ overflowNode }
</div>
);
},
});
}
}

View file

@ -1,76 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'UserSelector',
propTypes: {
onChange: PropTypes.func,
selected_users: PropTypes.arrayOf(PropTypes.string),
},
getDefaultProps: function() {
return {
onChange: function() {},
selected: [],
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._user_id_input = createRef();
},
addUser: function(user_id) {
if (this.props.selected_users.indexOf(user_id == -1)) {
this.props.onChange(this.props.selected_users.concat([user_id]));
}
},
removeUser: function(user_id) {
this.props.onChange(this.props.selected_users.filter(function(e) {
return e != user_id;
}));
},
onAddUserId: function() {
this.addUser(this._user_id_input.current.value);
this._user_id_input.current.value = "";
},
render: function() {
const self = this;
return (
<div>
<ul className="mx_UserSelector_UserIdList">
{ this.props.selected_users.map(function(user_id, i) {
return <li key={user_id}>{ user_id } - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
}) }
</ul>
<input type="text" ref={this._user_id_input} defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
{ _t("Add User") }
</button>
</div>
);
},
});

View file

@ -0,0 +1,85 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import * as fbEmitter from "fbemitter";
import TagOrderStore from "../../../stores/TagOrderStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
interface IProps {
}
interface IState {
selected: boolean;
}
export default class UserTagTile extends React.PureComponent<IProps, IState> {
private tagStoreRef: fbEmitter.EventSubscription;
constructor(props: IProps) {
super(props);
this.state = {
selected: TagOrderStore.getSelectedTags().length === 0,
};
}
public componentDidMount() {
this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
}
public componentWillUnmount() {
this.tagStoreRef.remove();
}
private onTagStoreUpdate = () => {
const selected = TagOrderStore.getSelectedTags().length === 0;
this.setState({selected});
};
private onTileClick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
// Deselect all tags
defaultDispatcher.dispatch({action: "deselect_tags"});
};
public render() {
// XXX: We reuse TagTile classes for ease of demonstration - we should probably generify
// TagTile instead if we continue to use this component.
const className = classNames({
mx_TagTile: true,
mx_TagTile_prototype: true,
mx_TagTile_selected_prototype: this.state.selected,
mx_TagTile_home: true,
});
return (
<AccessibleTooltipButton
className={className}
onClick={this.onTileClick}
title={_t("Home")}
>
<div className="mx_TagTile_avatar">
<div className="mx_TagTile_homeIcon" />
</div>
</AccessibleTooltipButton>
);
}
}

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import {_t} from '../../../languageHandler';
@ -29,50 +28,48 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
// XXX this class copies a lot from RoomTile.js
export default createReactClass({
displayName: 'GroupInviteTile',
propTypes: {
export default class GroupInviteTile extends React.Component {
static propTypes: {
group: PropTypes.object.isRequired,
},
};
statics: {
contextType: MatrixClientContext,
},
static contextType = MatrixClientContext;
getInitialState: function() {
return ({
constructor(props, context) {
super(props, context);
this.state = {
hover: false,
badgeHover: false,
menuDisplayed: false,
selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state
});
},
};
}
onClick: function(e) {
onClick = e => {
dis.dispatch({
action: 'view_group',
group_id: this.props.group.groupId,
});
},
};
onMouseEnter: function() {
onMouseEnter = () => {
const state = {hover: true};
// Only allow non-guests to access the context menu
if (!this.context.isGuest()) {
state.badgeHover = true;
}
this.setState(state);
},
};
onMouseLeave: function() {
onMouseLeave = () => {
this.setState({
badgeHover: false,
hover: false,
});
},
};
_showContextMenu: function(boundingClientRect) {
_showContextMenu(boundingClientRect) {
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
@ -86,17 +83,17 @@ export default createReactClass({
}
this.setState(state);
},
}
onContextMenuButtonClick: function(e) {
onContextMenuButtonClick = e => {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this._showContextMenu(e.target.getBoundingClientRect());
},
};
onContextMenu: function(e) {
onContextMenu = e => {
// Prevent the native context menu
e.preventDefault();
@ -105,15 +102,15 @@ export default createReactClass({
top: e.clientY,
height: 0,
});
},
};
closeMenu: function() {
closeMenu = () => {
this.setState({
contextMenuPosition: null,
});
},
};
render: function() {
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
@ -197,5 +194,5 @@ export default createReactClass({
{ contextMenu }
</React.Fragment>;
},
});
}
}

View file

@ -16,7 +16,6 @@ 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 * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
@ -30,33 +29,29 @@ import {Action} from "../../../dispatcher/actions";
const INITIAL_LOAD_NUM_MEMBERS = 30;
export default createReactClass({
displayName: 'GroupMemberList',
propTypes: {
export default class GroupMemberList extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
},
};
getInitialState: function() {
return {
members: null,
membersError: null,
invitedMembers: null,
invitedMembersError: null,
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
};
},
state = {
members: null,
membersError: null,
invitedMembers: null,
invitedMembersError: null,
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
};
componentDidMount: function() {
componentDidMount() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this._unmounted = true;
},
}
_initGroupStore: function(groupId) {
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, () => {
this._fetchMembers();
});
@ -73,17 +68,17 @@ export default createReactClass({
});
}
});
},
}
_fetchMembers: function() {
_fetchMembers() {
if (this._unmounted) return;
this.setState({
members: GroupStore.getGroupMembers(this.props.groupId),
invitedMembers: GroupStore.getGroupInvitedMembers(this.props.groupId),
});
},
}
_createOverflowTile: function(overflowCount, totalCount) {
_createOverflowTile = (overflowCount, totalCount) => {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
@ -94,19 +89,19 @@ export default createReactClass({
} name={text} presenceState="online" suppressOnHover={true}
onClick={this._showFullMemberList} />
);
},
};
_showFullMemberList: function() {
_showFullMemberList = () => {
this.setState({
truncateAt: -1,
});
},
};
onSearchQueryChanged: function(ev) {
onSearchQueryChanged = ev => {
this.setState({ searchQuery: ev.target.value });
},
};
makeGroupMemberTiles: function(query, memberList, memberListError) {
makeGroupMemberTiles(query, memberList, memberListError) {
if (memberListError) {
return <div className="warning">{ _t("Failed to load group members") }</div>;
}
@ -160,9 +155,9 @@ export default createReactClass({
>
{ memberTiles }
</TruncatedList>;
},
}
onInviteToGroupButtonClick() {
onInviteToGroupButtonClick = () => {
showGroupInviteDialog(this.props.groupId).then(() => {
dis.dispatch({
action: Action.SetRightPanelPhase,
@ -170,9 +165,9 @@ export default createReactClass({
refireParams: { groupId: this.props.groupId },
});
});
},
};
render: function() {
render() {
if (this.state.fetching || this.state.fetchingInvitedMembers) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_MemberList">
@ -230,5 +225,5 @@ export default createReactClass({
{ inputBox }
</div>
);
},
});
}
}

View file

@ -18,37 +18,28 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { GroupMemberType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({
displayName: 'GroupMemberTile',
propTypes: {
export default class GroupMemberTile extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
member: GroupMemberType.isRequired,
},
};
getInitialState: function() {
return {};
},
static contextType = MatrixClientContext;
statics: {
contextType: MatrixClientContext,
},
onClick: function(e) {
onClick = e => {
dis.dispatch({
action: 'view_group_user',
member: this.props.member,
groupId: this.props.groupId,
});
},
};
render: function() {
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile');
@ -74,5 +65,5 @@ export default createReactClass({
powerStatus={this.props.member.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null}
/>
);
},
});
}
}

View file

@ -16,44 +16,39 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import GroupStore from '../../../stores/GroupStore';
import ToggleSwitch from "../elements/ToggleSwitch";
export default createReactClass({
displayName: 'GroupPublicityToggle',
propTypes: {
export default class GroupPublicityToggle extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
},
};
getInitialState() {
return {
busy: false,
ready: false,
isGroupPublicised: false, // assume false as <ToggleSwitch /> expects a boolean
};
},
state = {
busy: false,
ready: false,
isGroupPublicised: false, // assume false as <ToggleSwitch /> expects a boolean
};
componentDidMount: function() {
componentDidMount() {
this._initGroupStore(this.props.groupId);
},
}
_initGroupStore: function(groupId) {
_initGroupStore(groupId) {
this._groupStoreToken = GroupStore.registerListener(groupId, () => {
this.setState({
isGroupPublicised: Boolean(GroupStore.getGroupPublicity(groupId)),
ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
});
});
},
}
componentWillUnmount() {
if (this._groupStoreToken) this._groupStoreToken.unregister();
},
}
_onPublicityToggle: function() {
_onPublicityToggle = () => {
this.setState({
busy: true,
// Optimistic early update
@ -64,7 +59,7 @@ export default createReactClass({
busy: false,
});
});
},
};
render() {
const GroupTile = sdk.getComponent('groups.GroupTile');
@ -76,5 +71,5 @@ export default createReactClass({
disabled={!this.state.ready || this.state.busy}
onChange={this._onPublicityToggle} />
</div>;
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
@ -26,50 +25,45 @@ import GroupStore from '../../../stores/GroupStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
export default createReactClass({
displayName: 'GroupRoomInfo',
export default class GroupRoomInfo extends React.Component {
static contextType = MatrixClientContext;
statics: {
contextType: MatrixClientContext,
},
propTypes: {
static propTypes = {
groupId: PropTypes.string,
groupRoomId: PropTypes.string,
},
};
getInitialState: function() {
return {
isUserPrivilegedInGroup: null,
groupRoom: null,
groupRoomPublicityLoading: false,
groupRoomRemoveLoading: false,
};
},
state = {
isUserPrivilegedInGroup: null,
groupRoom: null,
groupRoomPublicityLoading: false,
groupRoomRemoveLoading: false,
};
componentDidMount: function() {
componentDidMount() {
this._initGroupStore(this.props.groupId);
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
}
componentWillUnmount() {
this._unregisterGroupStore(this.props.groupId);
},
}
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
}
_unregisterGroupStore(groupId) {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
}
_updateGroupRoom() {
this.setState({
@ -77,16 +71,16 @@ export default createReactClass({
(r) => r.roomId === this.props.groupRoomId,
),
});
},
}
onGroupStoreUpdated: function() {
onGroupStoreUpdated = () => {
this.setState({
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
this._updateGroupRoom();
},
};
_onRemove: function(e) {
_onRemove = e => {
const groupId = this.props.groupId;
const roomName = this.state.groupRoom.displayname;
e.preventDefault();
@ -119,15 +113,15 @@ export default createReactClass({
});
},
});
},
};
_onCancel: function(e) {
_onCancel = e => {
dis.dispatch({
action: "view_group_room_list",
});
},
};
_changeGroupRoomPublicity(e) {
_changeGroupRoomPublicity = e => {
const isPublic = e.target.value === "public";
this.setState({
groupRoomPublicityLoading: true,
@ -150,9 +144,9 @@ export default createReactClass({
groupRoomPublicityLoading: false,
});
});
},
};
render: function() {
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
@ -235,5 +229,5 @@ export default createReactClass({
</AutoHideScrollbar>
</div>
);
},
});
}
}

View file

@ -14,7 +14,6 @@ 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 * as sdk from '../../../index';
import GroupStore from '../../../stores/GroupStore';
@ -25,34 +24,32 @@ import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
const INITIAL_LOAD_NUM_ROOMS = 30;
export default createReactClass({
propTypes: {
export default class GroupRoomList extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
},
};
getInitialState: function() {
return {
rooms: null,
truncateAt: INITIAL_LOAD_NUM_ROOMS,
searchQuery: "",
};
},
state = {
rooms: null,
truncateAt: INITIAL_LOAD_NUM_ROOMS,
searchQuery: "",
};
componentDidMount: function() {
componentDidMount() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},
}
componentWillUnmount() {
this._unmounted = true;
this._unregisterGroupStore();
},
}
_unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
}
_initGroupStore: function(groupId) {
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
// XXX: This is also leaked - we should remove it when unmounting
@ -62,16 +59,16 @@ export default createReactClass({
rooms: null,
});
});
},
}
onGroupStoreUpdated: function() {
onGroupStoreUpdated = () => {
if (this._unmounted) return;
this.setState({
rooms: GroupStore.getGroupRooms(this.props.groupId),
});
},
};
_createOverflowTile: function(overflowCount, totalCount) {
_createOverflowTile = (overflowCount, totalCount) => {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
@ -82,25 +79,25 @@ export default createReactClass({
} name={text} presenceState="online" suppressOnHover={true}
onClick={this._showFullRoomList} />
);
},
};
_showFullRoomList: function() {
_showFullRoomList = () => {
this.setState({
truncateAt: -1,
});
},
};
onSearchQueryChanged: function(ev) {
onSearchQueryChanged = ev => {
this.setState({ searchQuery: ev.target.value });
},
};
onAddRoomToGroupButtonClick() {
onAddRoomToGroupButtonClick = () => {
showGroupAddRoomDialog(this.props.groupId).then(() => {
this.forceUpdate();
});
},
};
makeGroupRoomTiles: function(query) {
makeGroupRoomTiles(query) {
const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile");
query = (query || "").toLowerCase();
@ -123,9 +120,9 @@ export default createReactClass({
});
return roomList;
},
}
render: function() {
render() {
if (this.state.rooms === null) {
return null;
}
@ -160,5 +157,5 @@ export default createReactClass({
{ inputBox }
</div>
);
},
});
}
}

View file

@ -16,29 +16,28 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { GroupRoomType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const GroupRoomTile = createReactClass({
displayName: 'GroupRoomTile',
propTypes: {
class GroupRoomTile extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
groupRoom: GroupRoomType.isRequired,
},
};
onClick: function(e) {
static contextType = MatrixClientContext
onClick = e => {
dis.dispatch({
action: 'view_group_room',
groupId: this.props.groupId,
groupRoomId: this.props.groupRoom.roomId,
});
},
};
render: function() {
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const avatarUrl = this.context.mxcUrlToHttp(
@ -63,10 +62,7 @@ const GroupRoomTile = createReactClass({
</div>
</AccessibleButton>
);
},
});
GroupRoomTile.contextType = MatrixClientContext;
}
}
export default GroupRoomTile;

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
@ -25,53 +24,45 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
function nop() {}
const GroupTile = createReactClass({
displayName: 'GroupTile',
propTypes: {
class GroupTile extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
// Whether to show the short description of the group on the tile
showDescription: PropTypes.bool,
// Height of the group avatar in pixels
avatarHeight: PropTypes.number,
draggable: PropTypes.bool,
},
};
statics: {
contextType: MatrixClientContext,
},
static contextType = MatrixClientContext;
getInitialState() {
return {
profile: null,
};
},
static defaultProps = {
showDescription: true,
avatarHeight: 50,
draggable: true,
};
getDefaultProps() {
return {
showDescription: true,
avatarHeight: 50,
draggable: true,
};
},
state = {
profile: null,
};
componentDidMount: function() {
componentDidMount() {
FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
this.setState({profile});
}).catch((err) => {
console.error('Error whilst getting cached profile for GroupTile', err);
});
},
}
onMouseDown: function(e) {
onMouseDown = e => {
e.preventDefault();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupId,
});
},
};
render: function() {
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const profile = this.state.profile || {};
@ -126,7 +117,7 @@ const GroupTile = createReactClass({
}
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
{ avatarElement }
<div className="mx_GroupTile_profile">
@ -135,7 +126,7 @@ const GroupTile = createReactClass({
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
</div>
</AccessibleButton>;
},
});
}
}
export default GroupTile;

View file

@ -15,33 +15,26 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({
displayName: 'GroupUserSettings',
export default class GroupUserSettings extends React.Component {
static contextType = MatrixClientContext;
statics: {
contextType: MatrixClientContext,
},
state = {
error: null,
groups: null,
};
getInitialState() {
return {
error: null,
groups: null,
};
},
componentDidMount: function() {
componentDidMount() {
this.context.getJoinedGroups().then((result) => {
this.setState({groups: result.groups || [], error: null});
}, (err) => {
console.error(err);
this.setState({groups: null, error: err});
});
},
}
render() {
let text = "";
@ -70,5 +63,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import filesize from 'filesize';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
@ -117,16 +116,8 @@ function computedStyle(element) {
return cssText;
}
export default createReactClass({
displayName: 'MFileBody',
getInitialState: function() {
return {
decryptedBlob: (this.props.decryptedBlob ? this.props.decryptedBlob : null),
};
},
propTypes: {
export default class MFileBody extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
/* already decrypted blob */
@ -135,7 +126,19 @@ export default createReactClass({
onHeightChanged: PropTypes.func,
/* the shape of the tile, used */
tileShape: PropTypes.string,
},
};
constructor(props) {
super(props);
this.state = {
decryptedBlob: (this.props.decryptedBlob ? this.props.decryptedBlob : null),
};
this._iframe = createRef();
this._dummyLink = createRef();
this._downloadImage = createRef();
}
/**
* Extracts a human readable label for the file attachment to use as
@ -144,7 +147,7 @@ export default createReactClass({
* @params {Object} content The "content" key of the matrix event.
* @return {string} the human readable link text for the attachment.
*/
presentableTextForFile: function(content) {
presentableTextForFile(content) {
let linkText = _t("Attachment");
if (content.body && content.body.length > 0) {
// The content body should be the name of the file including a
@ -163,40 +166,33 @@ export default createReactClass({
linkText += ' (' + filesize(content.info.size) + ')';
}
return linkText;
},
}
_getContentUrl: function() {
_getContentUrl() {
const content = this.props.mxEvent.getContent();
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._iframe = createRef();
this._dummyLink = createRef();
this._downloadImage = createRef();
},
componentDidMount: function() {
componentDidMount() {
// Add this to the list of mounted components to receive notifications
// when the tint changes.
this.id = nextMountId++;
mounts[this.id] = this;
this.tint();
},
}
componentDidUpdate: function(prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
this.props.onHeightChanged();
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
// Remove this from the list of mounted components
delete mounts[this.id];
},
}
tint: function() {
tint = () => {
// Update our tinted copy of require("../../../../res/img/download.svg")
if (this._downloadImage.current) {
this._downloadImage.current.src = tintedDownloadImageURL;
@ -210,9 +206,9 @@ export default createReactClass({
style: computedStyle(this._dummyLink.current),
}, "*");
}
},
};
render: function() {
render() {
const content = this.props.mxEvent.getContent();
const text = this.presentableTextForFile(content);
const isEncrypted = content.file !== undefined;
@ -378,5 +374,5 @@ export default createReactClass({
{ _t("Invalid file%(extra)s", { extra: extra }) }
</span>;
}
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
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,27 +24,23 @@ import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import InlineSpinner from '../elements/InlineSpinner';
export default createReactClass({
displayName: 'MVideoBody',
propTypes: {
export default class MVideoBody extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
/* called when the video has loaded */
onHeightChanged: PropTypes.func.isRequired,
},
};
getInitialState: function() {
return {
decryptedUrl: null,
decryptedThumbnailUrl: null,
decryptedBlob: null,
error: null,
};
},
state = {
decryptedUrl: null,
decryptedThumbnailUrl: null,
decryptedBlob: null,
error: null,
};
thumbScale: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
thumbScale(fullWidth, fullHeight, thumbWidth, thumbHeight) {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy
@ -64,18 +59,18 @@ export default createReactClass({
// height is the dominant dimension so scaling will be fixed on that
return heightMulti;
}
},
}
_getContentUrl: function() {
_getContentUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
}
},
}
_getThumbUrl: function() {
_getThumbUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedThumbnailUrl;
@ -84,9 +79,9 @@ export default createReactClass({
} else {
return null;
}
},
}
componentDidMount: function() {
componentDidMount() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);
@ -118,18 +113,18 @@ export default createReactClass({
});
});
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
if (this.state.decryptedThumbnailUrl) {
URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
}
},
}
render: function() {
render() {
const content = this.props.mxEvent.getContent();
if (this.state.error !== null) {
@ -182,5 +177,5 @@ export default createReactClass({
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
</span>
);
},
});
}
}

View file

@ -18,6 +18,7 @@ limitations under the License.
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import { EventStatus } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
@ -114,13 +115,19 @@ export default class MessageActionBar extends React.PureComponent {
static contextType = RoomContext;
componentDidMount() {
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
this.props.mxEvent.on("Event.status", this.onSent);
}
if (this.props.mxEvent.isBeingDecrypted()) {
this.props.mxEvent.once("Event.decrypted", this.onDecrypted);
}
this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
}
componentWillUnmount() {
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
this.props.mxEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
this.props.mxEvent.off("Event.status", this.onSent);
this.props.mxEvent.off("Event.decrypted", this.onDecrypted);
this.props.mxEvent.off("Event.beforeRedaction", this.onBeforeRedaction);
}
onDecrypted = () => {
@ -134,6 +141,11 @@ export default class MessageActionBar extends React.PureComponent {
this.forceUpdate();
};
onSent = () => {
// When an event is sent and echoed the possible actions change.
this.forceUpdate();
};
onFocusChange = (focused) => {
if (!this.props.onFocusChange) {
return;

View file

@ -16,17 +16,14 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import {Mjolnir} from "../../../mjolnir/Mjolnir";
import RedactedBody from "./RedactedBody";
import UnknownBody from "./UnknownBody";
export default createReactClass({
displayName: 'MessageEvent',
propTypes: {
export default class MessageEvent extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
@ -50,22 +47,23 @@ export default createReactClass({
overrideBodyTypes: PropTypes.object,
overrideEventTypes: PropTypes.object,
},
};
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._body = createRef();
},
}
getEventTileOps: function() {
getEventTileOps = () => {
return this._body.current && this._body.current.getEventTileOps ? this._body.current.getEventTileOps() : null;
},
};
onTileUpdate: function() {
onTileUpdate = () => {
this.forceUpdate();
},
};
render: function() {
render() {
const bodyTypes = {
'm.text': sdk.getComponent('messages.TextualBody'),
'm.notice': sdk.getComponent('messages.TextualBody'),
@ -100,7 +98,7 @@ export default createReactClass({
}
}
if (SettingsStore.isFeatureEnabled("feature_mjolnir")) {
if (SettingsStore.getValue("feature_mjolnir")) {
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
const allowRender = localStorage.getItem(key) === "true";
@ -128,5 +126,5 @@ export default createReactClass({
onHeightChanged={this.props.onHeightChanged}
onMessageAllowed={this.onTileUpdate}
/> : null;
},
});
}
}

View file

@ -18,22 +18,19 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
export default createReactClass({
displayName: 'RoomAvatarEvent',
propTypes: {
export default class RoomAvatarEvent extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
},
};
onAvatarClick: function() {
onAvatarClick = () => {
const cli = MatrixClientPeg.get();
const ev = this.props.mxEvent;
const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
@ -50,9 +47,9 @@ export default createReactClass({
name: text,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
};
render: function() {
render() {
const ev = this.props.mxEvent;
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
@ -86,5 +83,5 @@ export default createReactClass({
}
</div>
);
},
});
}
}

View file

@ -17,22 +17,19 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher/dispatcher';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
export default createReactClass({
displayName: 'RoomCreate',
propTypes: {
export default class RoomCreate extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
},
};
_onLinkClicked: function(e) {
_onLinkClicked = e => {
e.preventDefault();
const predecessor = this.props.mxEvent.getContent()['predecessor'];
@ -43,9 +40,9 @@ export default createReactClass({
highlighted: true,
room_id: predecessor['room_id'],
});
},
};
render: function() {
render() {
const predecessor = this.props.mxEvent.getContent()['predecessor'];
if (predecessor === undefined) {
return <div />; // We should never have been instaniated in this case
@ -66,5 +63,5 @@ export default createReactClass({
{_t("Click here to see older messages.")}
</a>
</div>;
},
});
}
}

View file

@ -16,31 +16,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({
displayName: 'SenderProfile',
propTypes: {
export default class SenderProfile extends React.Component {
static propTypes = {
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
text: PropTypes.string, // Text to show. Defaults to sender name
onClick: PropTypes.func,
},
};
statics: {
contextType: MatrixClientContext,
},
static contextType = MatrixClientContext;
getInitialState() {
return {
userGroups: null,
relatedGroups: [],
};
},
state = {
userGroups: null,
relatedGroups: [],
};
componentDidMount() {
this.unmounted = false;
@ -54,20 +48,20 @@ export default createReactClass({
});
this.context.on('RoomState.events', this.onRoomStateEvents);
},
}
componentWillUnmount() {
this.unmounted = true;
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
},
}
onRoomStateEvents(event) {
onRoomStateEvents = event => {
if (event.getType() === 'm.room.related_groups' &&
event.getRoomId() === this.props.mxEvent.getRoomId()
) {
this._updateRelatedGroups();
}
},
};
_updateRelatedGroups() {
if (this.unmounted) return;
@ -78,7 +72,7 @@ export default createReactClass({
this.setState({
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
});
},
}
_getDisplayedGroups(userGroups, relatedGroups) {
let displayedGroups = userGroups || [];
@ -90,7 +84,7 @@ export default createReactClass({
displayedGroups = [];
}
return displayedGroups;
},
}
render() {
const {mxEvent} = this.props;
@ -138,5 +132,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -19,7 +19,6 @@ limitations under the License.
import React, {createRef} 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';
@ -37,10 +36,8 @@ import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext} from "../../../utils/strings";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export default createReactClass({
displayName: 'TextualBody',
propTypes: {
export default class TextualBody extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
@ -58,10 +55,14 @@ export default createReactClass({
/* the shape of the tile, used */
tileShape: PropTypes.string,
},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this._content = createRef();
this.state = {
// the URLs (if any) to be previewed with a LinkPreviewWidget
// inside this TextualBody.
links: [],
@ -69,20 +70,15 @@ export default createReactClass({
// track whether the preview widget is hidden
widgetHidden: false,
};
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._content = createRef();
},
componentDidMount: function() {
componentDidMount() {
this._unmounted = false;
this._pills = [];
if (!this.props.editState) {
this._applyFormatting();
}
},
}
_applyFormatting() {
this.activateSpoilers([this._content.current]);
@ -119,9 +115,9 @@ export default createReactClass({
}
this._addCodeCopyButton();
}
},
}
componentDidUpdate: function(prevProps) {
componentDidUpdate(prevProps) {
if (!this.props.editState) {
const stoppedEditing = prevProps.editState && !this.props.editState;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
@ -129,14 +125,14 @@ export default createReactClass({
this._applyFormatting();
}
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this._unmounted = true;
unmountPills(this._pills);
},
}
shouldComponentUpdate: function(nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState) {
//console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
// exploit that events are immutable :)
@ -148,9 +144,9 @@ export default createReactClass({
nextProps.editState !== this.props.editState ||
nextState.links !== this.state.links ||
nextState.widgetHidden !== this.state.widgetHidden);
},
}
calculateUrlPreview: function() {
calculateUrlPreview() {
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview) {
@ -172,11 +168,13 @@ export default createReactClass({
const hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
this.setState({ widgetHidden: hidden });
}
} else if (this.state.links.length) {
this.setState({ links: [] });
}
}
},
}
activateSpoilers: function(nodes) {
activateSpoilers(nodes) {
let node = nodes[0];
while (node) {
if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
@ -202,9 +200,9 @@ export default createReactClass({
node = node.nextSibling;
}
},
}
findLinks: function(nodes) {
findLinks(nodes) {
let links = [];
for (let i = 0; i < nodes.length; i++) {
@ -221,9 +219,9 @@ export default createReactClass({
}
}
return links;
},
}
isLinkPreviewable: function(node) {
isLinkPreviewable(node) {
// don't try to preview relative links
if (!node.getAttribute("href").startsWith("http://") &&
!node.getAttribute("href").startsWith("https://")) {
@ -254,7 +252,7 @@ export default createReactClass({
return true;
}
}
},
}
_addCodeCopyButton() {
// Add 'copy' buttons to pre blocks
@ -286,41 +284,39 @@ export default createReactClass({
div.appendChild(p);
div.appendChild(button);
});
},
}
onCancelClick: function(event) {
onCancelClick = event => {
this.setState({ widgetHidden: true });
// FIXME: persist this somewhere smarter than local storage
if (global.localStorage) {
global.localStorage.setItem("hide_preview_" + this.props.mxEvent.getId(), "1");
}
this.forceUpdate();
},
};
onEmoteSenderClick: function(event) {
onEmoteSenderClick = event => {
const mxEvent = this.props.mxEvent;
dis.dispatch({
action: 'insert_mention',
user_id: mxEvent.getSender(),
});
},
};
getEventTileOps: function() {
return {
isWidgetHidden: () => {
return this.state.widgetHidden;
},
getEventTileOps = () => ({
isWidgetHidden: () => {
return this.state.widgetHidden;
},
unhideWidget: () => {
this.setState({ widgetHidden: false });
if (global.localStorage) {
global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId());
}
},
};
},
unhideWidget: () => {
this.setState({widgetHidden: false});
if (global.localStorage) {
global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId());
}
},
});
onStarterLinkClick: function(starterLink, ev) {
onStarterLinkClick = (starterLink, ev) => {
ev.preventDefault();
// We need to add on our scalar token to the starter link, but we may not have one!
// In addition, we can't fetch one on click and then go to it immediately as that
@ -351,7 +347,7 @@ export default createReactClass({
"Do you wish to continue?", { integrationsUrl: integrationsUrl }) }
</div>,
button: _t("Continue"),
onFinished: function(confirmed) {
onFinished(confirmed) {
if (!confirmed) {
return;
}
@ -365,14 +361,14 @@ export default createReactClass({
},
});
});
},
};
_openHistoryDialog: async function() {
_openHistoryDialog = async () => {
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
},
};
_renderEditedMarker: function() {
_renderEditedMarker() {
const date = this.props.mxEvent.replacingEventDate();
const dateString = date && formatDate(date);
@ -395,9 +391,9 @@ export default createReactClass({
<span>{`(${_t("edited")})`}</span>
</AccessibleTooltipButton>
);
},
}
render: function() {
render() {
if (this.props.editState) {
const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
@ -466,5 +462,5 @@ export default createReactClass({
</span>
);
}
},
});
}
}

View file

@ -17,22 +17,19 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as TextForEvent from "../../../TextForEvent";
export default createReactClass({
displayName: 'TextualEvent',
propTypes: {
export default class TextualEvent extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
},
};
render: function() {
render() {
const text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length === 0) return null;
return (
<div className="mx_TextualEvent">{ text }</div>
);
},
});
}
}

View file

@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, {forwardRef} from "react";
export default ({mxEvent}) => {
export default forwardRef(({mxEvent}, ref) => {
const text = mxEvent.getContent().body;
return (
<span className="mx_UnknownBody">
<span className="mx_UnknownBody" ref={ref}>
{ text }
</span>
);
};
});

View file

@ -76,14 +76,16 @@ const EncryptionInfo: React.FC<IProps> = ({
description = (
<div>
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
<p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
<p>{_t("Your messages are secured and only you and the recipient have " +
"the unique keys to unlock them.")}</p>
</div>
);
} else {
description = (
<div>
<p>{_t("Messages in this room are not end-to-end encrypted.")}</p>
<p>{_t("In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
<p>{_t("In encrypted rooms, your messages are secured and only you and the recipient have " +
"the unique keys to unlock them.")}</p>
</div>
);
}

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