Merge remote-tracking branch 'upstream/develop' into feature_confetti#14676
This commit is contained in:
commit
c86964cd5e
478 changed files with 21997 additions and 13673 deletions
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -26,6 +25,7 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -75,14 +75,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,19 +86,17 @@ 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;
|
||||
|
||||
|
@ -117,16 +111,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 +170,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 +231,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 +293,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 +317,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 +328,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 +375,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 +391,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
|
||||
|
@ -421,8 +405,12 @@ export const EmailIdentityAuthEntry = createReactClass({
|
|||
// the validation link, we won't know the email address, so if we don't have it,
|
||||
// assume that the link has been clicked and the server will realise when we poll.
|
||||
if (this.props.inputs.emailAddress === undefined) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
return <Spinner />;
|
||||
} else if (this.props.stageState?.emailSid) {
|
||||
// we only have a session ID if the user has clicked the link in their email,
|
||||
// so show a loading state instead of "an email has been sent to..." because
|
||||
// that's confusing when you've already read that email.
|
||||
return <Spinner />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
|
@ -434,17 +422,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 +438,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 +459,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 +475,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;
|
||||
|
||||
|
@ -552,9 +534,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 +580,8 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SSOAuthEntry extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -686,46 +668,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 +717,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 +743,8 @@ export const FallbackAuthEntry = createReactClass({
|
|||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const AuthEntryComponents = [
|
||||
PasswordAuthEntry,
|
||||
|
|
|
@ -40,11 +40,7 @@ interface IProps {
|
|||
onValidate(result: IValidationResult);
|
||||
}
|
||||
|
||||
interface IState {
|
||||
complexity: zxcvbn.ZXCVBNResult;
|
||||
}
|
||||
|
||||
class PassphraseField extends PureComponent<IProps, IState> {
|
||||
class PassphraseField extends PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
label: _td("Password"),
|
||||
labelEnterPassword: _td("Enter password"),
|
||||
|
@ -52,14 +48,16 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
|||
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
||||
};
|
||||
|
||||
state = { complexity: null };
|
||||
|
||||
public readonly validate = withValidation<this>({
|
||||
description: function() {
|
||||
const complexity = this.state.complexity;
|
||||
public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult>({
|
||||
description: function(complexity) {
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
||||
},
|
||||
deriveData: async ({ value }) => {
|
||||
if (!value) return null;
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
return scorePassword(value);
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -68,28 +66,24 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
|||
},
|
||||
{
|
||||
key: "complexity",
|
||||
test: async function({ value }) {
|
||||
test: async function({ value }, complexity) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
this.setState({ complexity });
|
||||
const safe = complexity.score >= this.props.minScore;
|
||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
valid: function() {
|
||||
valid: function(complexity) {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (this.state.complexity.score >= this.props.minScore) {
|
||||
if (complexity.score >= this.props.minScore) {
|
||||
return _t(this.props.labelStrongPassword);
|
||||
}
|
||||
return _t(this.props.labelAllowedButUnsafe);
|
||||
},
|
||||
invalid: function() {
|
||||
const complexity = this.state.complexity;
|
||||
invalid: function(complexity) {
|
||||
if (!complexity) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from "classnames";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import AuthPage from "./AuthPage";
|
||||
import {_td} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
@ -39,7 +43,9 @@ export default class Welcome extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<AuthPage>
|
||||
<div className="mx_Welcome">
|
||||
<div className={classNames("mx_Welcome", {
|
||||
mx_WelcomePage_registrationDisabled: !SettingsStore.getValue(UIFeature.Registration),
|
||||
})}>
|
||||
<EmbeddedPage
|
||||
className="mx_WelcomePage"
|
||||
url={pageUrl}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -126,7 +126,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
private onPresenceUpdate = () => {
|
||||
if (this.isUnmounted) return;
|
||||
|
||||
let newIcon = this.getPresenceIcon();
|
||||
const newIcon = this.getPresenceIcon();
|
||||
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -16,23 +16,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
|
||||
interface IProps {
|
||||
// TODO: replace with correct type
|
||||
member: any;
|
||||
fallbackUserId: string;
|
||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||
member: RoomMember;
|
||||
fallbackUserId?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
resizeMethod: string;
|
||||
resizeMethod?: string;
|
||||
// The onClick to give the avatar
|
||||
onClick: React.MouseEventHandler;
|
||||
onClick?: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick: boolean;
|
||||
title: string;
|
||||
viewUserOnClick?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -25,4 +25,4 @@ const PulsedAvatar: React.FC<IProps> = (props) => {
|
|||
</div>;
|
||||
};
|
||||
|
||||
export default PulsedAvatar;
|
||||
export default PulsedAvatar;
|
||||
|
|
|
@ -22,6 +22,7 @@ import ImageView from '../elements/ImageView';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
|
||||
interface IProps {
|
||||
// Room may be left unset here, but if it is,
|
||||
|
@ -32,7 +33,7 @@ interface IProps {
|
|||
oobData?: any;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: string;
|
||||
resizeMethod?: ResizeMethod;
|
||||
viewAvatarOnClick?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ interface IOptionListProps {
|
|||
}
|
||||
|
||||
interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
|
||||
iconClassName: string;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
|
||||
|
@ -92,7 +92,7 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
|
|||
|
||||
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({label, iconClassName, ...props}) => {
|
||||
return <MenuItem {...props} label={label}>
|
||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
||||
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
|
||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
||||
</MenuItem>;
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
@ -84,47 +79,47 @@ export default createReactClass({
|
|||
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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ export default class WidgetContextMenu extends React.Component {
|
|||
// Callback for when the revoke button is clicked. Required.
|
||||
onRevokeClicked: PropTypes.func.isRequired,
|
||||
|
||||
// Callback for when the unpin button is clicked. If absent, unpin will be hidden.
|
||||
onUnpinClicked: PropTypes.func,
|
||||
|
||||
// Callback for when the snapshot button is clicked. Button not shown
|
||||
// without a callback.
|
||||
onSnapshotClicked: PropTypes.func,
|
||||
|
@ -70,6 +73,8 @@ export default class WidgetContextMenu extends React.Component {
|
|||
this.proxyClick(this.props.onRevokeClicked);
|
||||
};
|
||||
|
||||
onUnpinClicked = () => this.proxyClick(this.props.onUnpinClicked);
|
||||
|
||||
render() {
|
||||
const options = [];
|
||||
|
||||
|
@ -81,6 +86,14 @@ export default class WidgetContextMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.onUnpinClicked) {
|
||||
options.push(
|
||||
<MenuItem className="mx_WidgetContextMenu_option" onClick={this.onUnpinClicked} key="unpin">
|
||||
{_t("Unpin")}
|
||||
</MenuItem>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onReloadClicked) {
|
||||
options.push(
|
||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked} key='reload'>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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} />
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class BugReportDialog extends React.Component {
|
|||
busy: false,
|
||||
err: null,
|
||||
issueUrl: "",
|
||||
text: "",
|
||||
text: props.initialText || "",
|
||||
progress: null,
|
||||
downloadBusy: false,
|
||||
downloadProgress: null,
|
||||
|
@ -255,4 +255,5 @@ export default class BugReportDialog extends React.Component {
|
|||
|
||||
BugReportDialog.propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
initialText: PropTypes.string,
|
||||
};
|
||||
|
|
248
src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
Normal file
248
src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
227
src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
Normal file
227
src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
Normal 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*/}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: "",
|
||||
|
@ -44,8 +45,12 @@ export default createReactClass({
|
|||
detailsOpen: false,
|
||||
noFederate: config.default_federate === false,
|
||||
nameIsValid: false,
|
||||
canChangeEncryption: true,
|
||||
};
|
||||
},
|
||||
|
||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
|
||||
.then(isForced => this.setState({canChangeEncryption: !isForced}));
|
||||
}
|
||||
|
||||
_roomCreateOptions() {
|
||||
const opts = {};
|
||||
|
@ -67,31 +72,41 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
if (!this.state.isPublic) {
|
||||
opts.encryption = this.state.isEncrypted;
|
||||
if (this.state.canChangeEncryption) {
|
||||
opts.encryption = this.state.isEncrypted;
|
||||
} else {
|
||||
// the server should automatically do this for us, but for safety
|
||||
// we'll demand it too.
|
||||
opts.encryption = true;
|
||||
}
|
||||
}
|
||||
|
||||
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 +132,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,34 +184,45 @@ 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;
|
||||
if (!this.state.isPublic) {
|
||||
let microcopy;
|
||||
if (privateShouldBeEncrypted()) {
|
||||
microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet.");
|
||||
if (this.state.canChangeEncryption) {
|
||||
microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet.");
|
||||
} else {
|
||||
microcopy = _t("Your server requires encryption to be enabled in private rooms.");
|
||||
}
|
||||
} else {
|
||||
microcopy = _t("Your server admin has disabled end-to-end encryption by default " +
|
||||
"in private rooms & Direct Messages.");
|
||||
|
@ -207,12 +233,30 @@ export default createReactClass({
|
|||
onChange={this.onEncryptedChange}
|
||||
value={this.state.isEncrypted}
|
||||
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
|
||||
disabled={!this.state.canChangeEncryption}
|
||||
/>
|
||||
<p>{ microcopy }</p>
|
||||
</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 +271,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 +288,5 @@ export default createReactClass({
|
|||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
167
src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
Normal file
167
src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
19
src/components/views/dialogs/IDialogProps.ts
Normal file
19
src/components/views/dialogs/IDialogProps.ts
Normal 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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,11 +32,14 @@ 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";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -327,7 +330,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,7 +347,7 @@ 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
|
||||
|
@ -548,7 +551,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
if (this.state.filterText.startsWith('@')) {
|
||||
// Assume mxid
|
||||
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
|
||||
} else {
|
||||
} else if (SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
// Assume email
|
||||
newMember = new ThreepidMember(this.state.filterText);
|
||||
}
|
||||
|
@ -733,7 +736,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
this.setState({tryingIdentityServer: true});
|
||||
return;
|
||||
}
|
||||
if (term.indexOf('@') > 0 && Email.looksValid(term)) {
|
||||
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
// Start off by suggesting the plain email while we try and resolve it
|
||||
// to a real account.
|
||||
this.setState({
|
||||
|
@ -909,12 +912,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 +1007,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>
|
||||
|
@ -1024,7 +1039,9 @@ export default class InviteDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
_renderIdentityServerWarning() {
|
||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) {
|
||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
||||
!SettingsStore.getValue(UIFeature.IdentityServer)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1073,30 +1090,92 @@ export default class InviteDialog extends React.PureComponent {
|
|||
let buttonText;
|
||||
let goButtonFn;
|
||||
|
||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
if (this.props.kind === KIND_DM) {
|
||||
title = _t("Direct Messages");
|
||||
helpText = _t(
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||
{},
|
||||
{userId: () => {
|
||||
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
|
||||
}},
|
||||
);
|
||||
|
||||
if (identityServersEnabled) {
|
||||
helpText = _t(
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||
{},
|
||||
{userId: () => {
|
||||
return (
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
|
||||
);
|
||||
}},
|
||||
);
|
||||
} else {
|
||||
helpText = _t(
|
||||
"Start a conversation with someone using their name or username (like <userId/>).",
|
||||
{},
|
||||
{userId: () => {
|
||||
return (
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
|
||||
);
|
||||
}},
|
||||
);
|
||||
}
|
||||
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
const inviteText = _t("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>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
helpText = <React.Fragment>
|
||||
{ helpText } {inviteText}
|
||||
</React.Fragment>;
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else { // KIND_INVITE
|
||||
title = _t("Invite to this room");
|
||||
helpText = _t(
|
||||
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
},
|
||||
);
|
||||
|
||||
if (identityServersEnabled) {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, username (like <userId/>), email address or " +
|
||||
"<a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ import Modal from '../../../Modal';
|
|||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
|
||||
|
||||
export default class LogoutDialog extends React.Component {
|
||||
defaultProps = {
|
||||
|
@ -73,7 +74,7 @@ export default class LogoutDialog extends React.Component {
|
|||
|
||||
_onExportE2eKeysClicked() {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
|
@ -93,14 +94,13 @@ export default class LogoutDialog extends React.Component {
|
|||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import * as sdk from "../../../index";
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
|
||||
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
|
||||
|
@ -96,12 +97,14 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
));
|
||||
}
|
||||
|
||||
tabs.push(new Tab(
|
||||
ROOM_ADVANCED_TAB,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
|
||||
tabs.push(new Tab(
|
||||
ROOM_ADVANCED_TAB,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,307 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import { Key } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// 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: {
|
||||
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 {
|
||||
// The entered username
|
||||
username: '',
|
||||
// Indicate ongoing work on the username
|
||||
usernameBusy: false,
|
||||
// Indicate error with username
|
||||
usernameError: '',
|
||||
// Assume the homeserver supports username checking until "M_UNRECOGNIZED"
|
||||
usernameCheckSupport: true,
|
||||
|
||||
// Whether the auth UI is currently being used
|
||||
doingUIAuth: false,
|
||||
// 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() {
|
||||
this._input_value.current.select();
|
||||
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
onValueChange: function(ev) {
|
||||
this.setState({
|
||||
username: ev.target.value,
|
||||
usernameBusy: true,
|
||||
usernameError: '',
|
||||
}, () => {
|
||||
if (!this.state.username || !this.state.usernameCheckSupport) {
|
||||
this.setState({
|
||||
usernameBusy: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce the username check to limit number of requests sent
|
||||
if (this._usernameCheckTimeout) {
|
||||
clearTimeout(this._usernameCheckTimeout);
|
||||
}
|
||||
this._usernameCheckTimeout = setTimeout(() => {
|
||||
this._doUsernameCheck().finally(() => {
|
||||
this.setState({
|
||||
usernameBusy: false,
|
||||
});
|
||||
});
|
||||
}, USERNAME_CHECK_DEBOUNCE_MS);
|
||||
});
|
||||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
if (ev.key === Key.ENTER) {
|
||||
this.onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
if (this._uiAuth.current) {
|
||||
this._uiAuth.current.tryContinue();
|
||||
}
|
||||
this.setState({
|
||||
doingUIAuth: true,
|
||||
});
|
||||
},
|
||||
|
||||
_doUsernameCheck: function() {
|
||||
// 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)) {
|
||||
this.setState({
|
||||
usernameError: _t("A username can only contain lower case letters, numbers and '=_-./'"),
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
// Check if username is available
|
||||
return this._matrixClient.isUsernameAvailable(this.state.username).then(
|
||||
(isAvailable) => {
|
||||
if (isAvailable) {
|
||||
this.setState({usernameError: ''});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
// Indicate whether the homeserver supports username checking
|
||||
const newState = {
|
||||
usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
|
||||
};
|
||||
console.error('Error whilst checking username availability: ', err);
|
||||
switch (err.errcode) {
|
||||
case "M_USER_IN_USE":
|
||||
newState.usernameError = _t('Username not available');
|
||||
break;
|
||||
case "M_INVALID_USERNAME":
|
||||
newState.usernameError = _t(
|
||||
'Username invalid: %(errMessage)s',
|
||||
{ errMessage: err.message},
|
||||
);
|
||||
break;
|
||||
case "M_UNRECOGNIZED":
|
||||
// This homeserver doesn't support username checking, assume it's
|
||||
// fine and rely on the error appearing in registration step.
|
||||
newState.usernameError = '';
|
||||
break;
|
||||
case undefined:
|
||||
newState.usernameError = _t('Something went wrong!');
|
||||
break;
|
||||
default:
|
||||
newState.usernameError = _t(
|
||||
'An error occurred: %(error_string)s',
|
||||
{ error_string: err.message },
|
||||
);
|
||||
break;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
_generatePassword: function() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
},
|
||||
|
||||
_makeRegisterRequest: function(auth) {
|
||||
// Not upgrading - changing mxids
|
||||
const guestAccessToken = null;
|
||||
if (!this._generatedPassword) {
|
||||
this._generatedPassword = this._generatePassword();
|
||||
}
|
||||
return this._matrixClient.register(
|
||||
this.state.username,
|
||||
this._generatedPassword,
|
||||
undefined, // session id: included in the auth dict already
|
||||
auth,
|
||||
{},
|
||||
guestAccessToken,
|
||||
);
|
||||
},
|
||||
|
||||
_onUIAuthFinished: function(success, response) {
|
||||
this.setState({
|
||||
doingUIAuth: false,
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
this.setState({ authError: response.message });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFinished(true, {
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token,
|
||||
password: this._generatedPassword,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
|
||||
let auth;
|
||||
if (this.state.doingUIAuth) {
|
||||
auth = <InteractiveAuth
|
||||
matrixClient={this._matrixClient}
|
||||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={{}}
|
||||
poll={true}
|
||||
ref={this._uiAuth}
|
||||
continueIsManaged={true}
|
||||
/>;
|
||||
}
|
||||
const inputClasses = classnames({
|
||||
"mx_SetMxIdDialog_input": true,
|
||||
"error": Boolean(this.state.usernameError),
|
||||
});
|
||||
|
||||
let usernameIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameIndicator = <div>{_t("Checking...")}</div>;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
const usernameIndicatorClasses = classnames({
|
||||
"error": Boolean(this.state.usernameError),
|
||||
"success": usernameAvailable,
|
||||
});
|
||||
usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
|
||||
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let authErrorIndicator = null;
|
||||
if (this.state.authError) {
|
||||
authErrorIndicator = <div className="error" role="alert">
|
||||
{ this.state.authError }
|
||||
</div>;
|
||||
}
|
||||
const canContinue = this.state.username &&
|
||||
!this.state.usernameError &&
|
||||
!this.state.usernameBusy;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetMxIdDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('To get started, please pick a username!')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_SetMxIdDialog_input_group">
|
||||
<input type="text" ref={this._input_value} value={this.state.username}
|
||||
autoFocus={true}
|
||||
onChange={this.onValueChange}
|
||||
onKeyUp={this.onKeyUp}
|
||||
size="30"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _t(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
{},
|
||||
{
|
||||
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
{ authErrorIndicator }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input className="mx_Dialog_primary"
|
||||
type="submit"
|
||||
value={_t("Continue")}
|
||||
onClick={this.onSubmit}
|
||||
disabled={!canContinue}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
const WarmFuzzy = function(props) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
let title = _t('You have successfully set a password!');
|
||||
if (props.didSetEmail) {
|
||||
title = _t('You have successfully set a password and an email address!');
|
||||
}
|
||||
const advice = _t('You can now return to your account after signing out, and sign in on other devices.');
|
||||
let extraAdvice = null;
|
||||
if (!props.didSetEmail) {
|
||||
extraAdvice = _t('Remember, you can always set an email address in user settings if you change your mind.');
|
||||
}
|
||||
|
||||
return <BaseDialog className="mx_SetPasswordDialog"
|
||||
onFinished={props.onFinished}
|
||||
title={ title }
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ advice }
|
||||
</p>
|
||||
<p>
|
||||
{ extraAdvice }
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
className="mx_Dialog_primary"
|
||||
autoFocus={true}
|
||||
onClick={props.onFinished}>
|
||||
{ _t('Continue') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt the user to set a password
|
||||
*
|
||||
* On success, `onFinished()` when finished
|
||||
*/
|
||||
export default createReactClass({
|
||||
displayName: 'SetPasswordDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
console.info('SetPasswordDialog component did mount');
|
||||
},
|
||||
|
||||
_onPasswordChanged: function(res) {
|
||||
Modal.createDialog(WarmFuzzy, {
|
||||
didSetEmail: res.didSetEmail,
|
||||
onFinished: () => {
|
||||
this.props.onFinished();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onPasswordChangeError: function(err) {
|
||||
let errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
errMsg = _t('Failed to change password. Is your password correct?');
|
||||
} else if (err.httpStatus) {
|
||||
errMsg += ' ' + _t(
|
||||
'(HTTP status %(httpStatus)s)',
|
||||
{ httpStatus: err.httpStatus },
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
error: errMsg,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const ChangePassword = sdk.getComponent('views.settings.ChangePassword');
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetPasswordDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={ _t('Please set a password!') }
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t('This will allow you to return to your account after signing out, and sign in on other sessions.') }
|
||||
</p>
|
||||
<ChangePassword
|
||||
className="mx_SetPasswordDialog_change_password"
|
||||
rowClassName=""
|
||||
buttonClassNames="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
|
||||
buttonKind="primary"
|
||||
confirm={false}
|
||||
autoFocusNewPasswordInput={true}
|
||||
shouldAskForEmail={true}
|
||||
onError={this._onPasswordChangeError}
|
||||
onFinished={this._onPasswordChanged} />
|
||||
<div className="error">
|
||||
{ this.state.error }
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -31,6 +31,9 @@ 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";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
|
@ -60,8 +63,7 @@ const socials = [
|
|||
},
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
interface IProps extends IDialogProps {
|
||||
target: Room | User | Group | RoomMember | MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
@ -186,8 +188,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,34 +199,18 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
const matrixToUrl = this.getUrl();
|
||||
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}
|
||||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
>
|
||||
{ matrixToUrl }
|
||||
</a>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onCopyClick}
|
||||
className="mx_ShareDialog_matrixto_copy"
|
||||
/>
|
||||
</div>
|
||||
{ checkbox }
|
||||
<hr />
|
||||
const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode);
|
||||
const showSocials = SettingsStore.getValue(UIFeature.ShareSocial);
|
||||
|
||||
let qrSocialSection;
|
||||
if (showQrCode || showSocials) {
|
||||
qrSocialSection = <>
|
||||
<hr />
|
||||
<div className="mx_ShareDialog_split">
|
||||
<div className="mx_ShareDialog_qrcode_container">
|
||||
{ showQrCode && <div className="mx_ShareDialog_qrcode_container">
|
||||
<QRCode data={matrixToUrl} width={256} />
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
</div> }
|
||||
{ showSocials && <div className="mx_ShareDialog_social_container">
|
||||
{ socials.map((social) => (
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
|
@ -237,8 +223,35 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
<img src={social.img} alt={social.name} height={64} width={64} />
|
||||
</a>
|
||||
)) }
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
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"
|
||||
>
|
||||
{ matrixToUrl }
|
||||
</a>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onCopyClick}
|
||||
className="mx_ShareDialog_matrixto_copy"
|
||||
/>
|
||||
</div>
|
||||
{ checkbox }
|
||||
{ qrSocialSection }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export default ({onFinished}) => {
|
|||
|
||||
const categories = {};
|
||||
Commands.forEach(cmd => {
|
||||
if (!cmd.isEnabled()) return;
|
||||
if (!categories[cmd.category]) {
|
||||
categories[cmd.category] = [];
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
|
|||
import * as sdk from "../../../index";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
|
||||
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
|
||||
|
@ -86,12 +87,14 @@ export default class UserSettingsDialog extends React.Component {
|
|||
"mx_UserSettingsDialog_appearanceIcon",
|
||||
<AppearanceUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
USER_FLAIR_TAB,
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairUserSettingsTab />,
|
||||
));
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
tabs.push(new Tab(
|
||||
USER_FLAIR_TAB,
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairUserSettingsTab />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
USER_NOTIFICATIONS_TAB,
|
||||
_td("Notifications"),
|
||||
|
@ -104,12 +107,16 @@ export default class UserSettingsDialog extends React.Component {
|
|||
"mx_UserSettingsDialog_preferencesIcon",
|
||||
<PreferencesUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
USER_VOICE_TAB,
|
||||
_td("Voice & Video"),
|
||||
"mx_UserSettingsDialog_voiceIcon",
|
||||
<VoiceUserSettingsTab />,
|
||||
));
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
tabs.push(new Tab(
|
||||
USER_VOICE_TAB,
|
||||
_td("Voice & Video"),
|
||||
"mx_UserSettingsDialog_voiceIcon",
|
||||
<VoiceUserSettingsTab />,
|
||||
));
|
||||
}
|
||||
|
||||
tabs.push(new Tab(
|
||||
USER_SECURITY_TAB,
|
||||
_td("Security & Privacy"),
|
||||
|
|
|
@ -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";
|
||||
|
@ -289,7 +289,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
content = <div>
|
||||
<p>{_t("Use your Security Key to continue.")}</p>
|
||||
|
||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext} spellCheck={false}>
|
||||
<form
|
||||
className="mx_AccessSecretStorageDialog_primaryContainer"
|
||||
onSubmit={this._onRecoveryKeyNext}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
|
||||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
|
||||
<Field
|
||||
|
@ -298,6 +303,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
value={this.state.recoveryKey}
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
forceValidity={this.state.recoveryKeyCorrect}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import * as sdk from "../../../../index";
|
||||
|
||||
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
||||
static propTypes = {
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import Modal from '../../../../Modal';
|
||||
import { SSOAuthEntry } from '../../auth/InteractiveAuthEntryComponents';
|
||||
import DialogButtons from '../../elements/DialogButtons';
|
||||
import BaseDialog from '../BaseDialog';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating a cross-signing keys. In most
|
||||
* cases, only a spinner is shown, but for more complex auth like SSO, the user
|
||||
* may need to complete some steps to proceed.
|
||||
*/
|
||||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
accountPassword: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
// Does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: null,
|
||||
accountPassword: props.accountPassword || "",
|
||||
};
|
||||
|
||||
if (this.state.accountPassword) {
|
||||
// If we have an account password in memory, let's simplify and
|
||||
// assume it means password auth is also supported for device
|
||||
// signing key upload as well. This avoids hitting the server to
|
||||
// test auth flows, which may be slow under high load.
|
||||
this.state.canUploadKeysWithPasswordOnly = true;
|
||||
} else {
|
||||
this._queryKeyUploadAuth();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._bootstrapCrossSigning();
|
||||
}
|
||||
|
||||
async _queryKeyUploadAuth() {
|
||||
try {
|
||||
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
||||
// We should never get here: the server should always require
|
||||
// UI auth to upload device signing keys. If we do, we upload
|
||||
// no keys which would be a no-op.
|
||||
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||
} catch (error) {
|
||||
if (!error.data || !error.data.flows) {
|
||||
console.log("uploadDeviceSigningKeys advertised no flows!");
|
||||
return;
|
||||
}
|
||||
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
|
||||
return f.stages.length === 1 && f.stages[0] === 'm.login.password';
|
||||
});
|
||||
this.setState({
|
||||
canUploadKeysWithPasswordOnly,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_doBootstrapUIAuth = async (makeRequest) => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
type: 'm.id.user',
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
},
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
} else {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
body: _t("To continue, use Single Sign On to prove your identity."),
|
||||
continueText: _t("Single Sign On"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("Confirm encryption setup"),
|
||||
body: _t("Click the button below to confirm setting up encryption."),
|
||||
continueText: _t("Confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Setting up keys"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
makeRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
},
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bootstrapCrossSigning = async () => {
|
||||
this.setState({
|
||||
error: null,
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
try {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping cross-signing", e);
|
||||
}
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = <div>
|
||||
<p>{_t("Unable to set up keys")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapCrossSigning}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
content = <div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_CreateCrossSigningDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Setting up keys")}
|
||||
hasCancel={false}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ import * as sdk from '../../../../index';
|
|||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
import { accessSecretStorage } from '../../../../SecurityManager';
|
||||
|
||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
|
@ -16,16 +16,16 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
|
||||
import BaseDialog from './BaseDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { SetupEncryptionStore, PHASE_DONE } from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
|
||||
import BaseDialog from '../BaseDialog';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
|
||||
|
||||
function iconFromPhase(phase) {
|
||||
if (phase === PHASE_DONE) {
|
||||
return require("../../../../res/img/e2e/verified.svg");
|
||||
return require("../../../../../res/img/e2e/verified.svg");
|
||||
} else {
|
||||
return require("../../../../res/img/e2e/warning.svg");
|
||||
return require("../../../../../res/img/e2e/warning.svg");
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,8 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
};
|
||||
|
||||
render() {
|
||||
const {title, tooltip, children, tooltipClassName, ...props} = this.props;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {title, tooltip, children, tooltipClassName, forceHide, ...props} = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import url from 'url';
|
||||
import qs from 'qs';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -34,35 +32,16 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import classNames from 'classnames';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement from "./PersistedElement";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
||||
/**
|
||||
* Does template substitution on a URL (or any string). Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
function uriFromTemplate(uriTemplate, variables) {
|
||||
let out = uriTemplate;
|
||||
for (const [key, val] of Object.entries(variables)) {
|
||||
out = out.replace(
|
||||
'$' + key, encodeURIComponent(val),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||
import {MatrixCapabilities} from "matrix-widget-api";
|
||||
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -70,11 +49,14 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = 'widget_' + this.props.app.id;
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this.iframe = null; // ref to the iframe (callback style)
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
this._onAction = this._onAction.bind(this);
|
||||
this._onLoaded = this._onLoaded.bind(this);
|
||||
this._onEditClick = this._onEditClick.bind(this);
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this._onRevokeClicked = this._onRevokeClicked.bind(this);
|
||||
|
@ -87,7 +69,6 @@ export default class AppTile extends React.Component {
|
|||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||
|
||||
this._contextMenuButton = createRef();
|
||||
this._appFrame = createRef();
|
||||
this._menu_bar = createRef();
|
||||
}
|
||||
|
||||
|
@ -100,16 +81,16 @@ export default class AppTile extends React.Component {
|
|||
_getNewState(newProps) {
|
||||
// This is a function to make the impact of calling SettingsStore slightly less
|
||||
const hasPermissionToLoad = () => {
|
||||
if (this._usingLocalWidget()) return true;
|
||||
|
||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
|
||||
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
||||
};
|
||||
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
// True while the iframe content is loading
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||
widgetUrl: this._addWurlParams(newProps.app.url),
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
|
||||
|
@ -120,43 +101,6 @@ export default class AppTile extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the widget support a given capability
|
||||
* @param {string} capability Capability to check for
|
||||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
* Properties passed to widget instance:
|
||||
* - widgetId
|
||||
* - origin / parent URL
|
||||
* @param {string} urlString Url string to modify
|
||||
* @return {string}
|
||||
* Url string with parameters appended.
|
||||
* If url can not be parsed, it is returned unmodified.
|
||||
*/
|
||||
_addWurlParams(urlString) {
|
||||
try {
|
||||
const parsed = new URL(urlString);
|
||||
|
||||
// TODO: Replace these with proper widget params
|
||||
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
|
||||
parsed.searchParams.set('widgetId', this.props.app.id);
|
||||
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
|
||||
|
||||
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
|
||||
// in HTTP, but URL parsers encode them anyways.
|
||||
return parsed.toString().replace(/%24/g, '$');
|
||||
} catch (e) {
|
||||
console.error("Failed to add widget URL params:", e);
|
||||
return urlString;
|
||||
}
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.app.url);
|
||||
|
@ -172,7 +116,7 @@ export default class AppTile extends React.Component {
|
|||
componentDidMount() {
|
||||
// Only fetch IM token on mount if we're showing and have permission to load
|
||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
this._startWidget();
|
||||
}
|
||||
|
||||
// Widget action listeners
|
||||
|
@ -186,93 +130,45 @@ export default class AppTile extends React.Component {
|
|||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
|
||||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Generify the name of this function. It's not just scalar tokens.
|
||||
/**
|
||||
* Adds a scalar token to the widget URL, if required
|
||||
* Component initialisation is only complete when this function has resolved
|
||||
*/
|
||||
setScalarToken() {
|
||||
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
|
||||
console.warn('Widget does not match integration manager, refusing to set auth token', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
_resetWidget(newProps) {
|
||||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
}
|
||||
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
if (!managers.hasManager()) {
|
||||
console.warn("No integration manager - not setting scalar token", url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Pick the right manager for the widget
|
||||
|
||||
const defaultManager = managers.getPrimaryManager();
|
||||
if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
|
||||
console.warn('Unknown integration manager, refusing to set auth token', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the token before loading the iframe as we need it to mangle the URL
|
||||
if (!this._scalarClient) {
|
||||
this._scalarClient = defaultManager.getScalarClient();
|
||||
}
|
||||
this._scalarClient.getScalarToken().then((token) => {
|
||||
// Append scalar_token as a query param if not already present
|
||||
this._scalarClient.scalarToken = token;
|
||||
const u = url.parse(this._addWurlParams(this.props.app.url));
|
||||
const params = qs.parse(u.query);
|
||||
if (!params.scalar_token) {
|
||||
params.scalar_token = encodeURIComponent(token);
|
||||
// u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: u.format(),
|
||||
initialising: false,
|
||||
});
|
||||
|
||||
// Fetch page title from remote content if not already set
|
||||
if (!this.state.widgetPageTitle && params.url) {
|
||||
this._fetchWidgetTitle(params.url);
|
||||
}
|
||||
}, (err) => {
|
||||
console.error("Failed to get scalar_token", err);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
initialising: false,
|
||||
});
|
||||
_startWidget() {
|
||||
this._sgWidget.prepare().then(() => {
|
||||
this.setState({initialising: false});
|
||||
});
|
||||
}
|
||||
|
||||
_iframeRefChange = (ref) => {
|
||||
this.iframe = ref;
|
||||
if (ref) {
|
||||
this._sgWidget.start(ref);
|
||||
} else {
|
||||
this._resetWidget(this.props);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
if (nextProps.app.url !== this.props.app.url) {
|
||||
this._getNewState(nextProps);
|
||||
// Fetch IM token for new URL if we're showing and have permission to load
|
||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
this._resetWidget(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,9 +179,9 @@ export default class AppTile extends React.Component {
|
|||
loading: true,
|
||||
});
|
||||
}
|
||||
// Fetch IM token now that we're showing if we already have permission to load
|
||||
// Start the widget now that we're showing if we already have permission to load
|
||||
if (this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
this._startWidget();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,35 +206,19 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
// TODO: Open the right manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
this.props.room,
|
||||
'type_' + this.props.app.type,
|
||||
this.props.app.id,
|
||||
);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
this.props.room,
|
||||
'type_' + this.props.app.type,
|
||||
this.props.app.id,
|
||||
);
|
||||
}
|
||||
WidgetUtils.editWidget(this.props.room, this.props.app);
|
||||
}
|
||||
}
|
||||
|
||||
_onSnapshotClick() {
|
||||
console.log("Requesting widget snapshot");
|
||||
ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
.then((screenshot) => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: screenshot,
|
||||
}, true);
|
||||
this._sgWidget.widgetApi.takeScreenshot().then(data => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: data.screenshot,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error("Failed to take screenshot: ", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -346,35 +226,28 @@ export default class AppTile extends React.Component {
|
|||
* @private
|
||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||
*/
|
||||
_endWidgetActions() {
|
||||
let terminationPromise;
|
||||
|
||||
if (this._hasCapability(Capability.ReceiveTerminate)) {
|
||||
// Wait for widget to terminate within a timeout
|
||||
const timeout = 2000;
|
||||
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
|
||||
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
|
||||
} else {
|
||||
terminationPromise = Promise.resolve();
|
||||
async _endWidgetActions() { // widget migration dev note: async to maintain signature
|
||||
// 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/element-web/issues/7351
|
||||
if (this.iframe) {
|
||||
// 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 Element instance is located.
|
||||
this.iframe.src = 'about:blank';
|
||||
}
|
||||
|
||||
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/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 Element instance is located.
|
||||
this._appFrame.current.src = 'about:blank';
|
||||
}
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
dis.dispatch({action: 'hangup_conference'});
|
||||
}
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
});
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
|
||||
this._sgWidget.stop({forceDestroy: true});
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
|
@ -419,101 +292,47 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onUnpinClicked = () => {
|
||||
WidgetStore.instance.unpinWidget(this.props.app.id);
|
||||
}
|
||||
|
||||
_onRevokeClicked() {
|
||||
console.info("Revoke widget permissions - %s", this.props.app.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
// Destroy the old widget messaging before starting it back up again. Some widgets
|
||||
// have startup routines that run when they are loaded, so we just need to reinitialize
|
||||
// the messaging for them.
|
||||
ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
|
||||
this._setupWidgetMessaging();
|
||||
|
||||
ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
|
||||
_onWidgetPrepared = () => {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
};
|
||||
|
||||
_setupWidgetMessaging() {
|
||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
const widgetMessaging = new WidgetMessaging(
|
||||
this.props.app.id,
|
||||
this.props.app.url,
|
||||
this._getRenderedUrl(),
|
||||
this.props.userWidget,
|
||||
this._appFrame.current.contentWindow,
|
||||
);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
let requestedWhitelistCapabilies = [];
|
||||
|
||||
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
|
||||
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
|
||||
return this.indexOf(e)>=0;
|
||||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
|
||||
requestedWhitelistCapabilies,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
|
||||
ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
}
|
||||
|
||||
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
|
||||
// using this custom extension to the widget API.
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
widgetMessaging.flagReadyToContinue();
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
|
||||
});
|
||||
}
|
||||
_onWidgetReady = () => {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||
}
|
||||
};
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.widgetId === this.props.app.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
break;
|
||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
break;
|
||||
|
||||
case Action.AppTileDelete:
|
||||
this._onDeleteClick();
|
||||
break;
|
||||
|
||||
case Action.AppTileRevoke:
|
||||
this._onRevokeClicked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
* @param {string} url Url to check for title
|
||||
*/
|
||||
_fetchWidgetTitle(url) {
|
||||
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
|
||||
if (widgetPageTitle) {
|
||||
this.setState({widgetPageTitle: widgetPageTitle});
|
||||
}
|
||||
}, (err) =>{
|
||||
console.error("Failed to get page title", err);
|
||||
});
|
||||
}
|
||||
|
||||
_grantWidgetPermission() {
|
||||
const roomId = this.props.room.roomId;
|
||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||
|
@ -523,7 +342,7 @@ export default class AppTile extends React.Component {
|
|||
this.setState({hasPermissionToLoad: true});
|
||||
|
||||
// Fetch a token for the integration manager, now that we're allowed to
|
||||
this.setScalarToken();
|
||||
this._startWidget();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
|
@ -542,6 +361,7 @@ export default class AppTile extends React.Component {
|
|||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
this._sgWidget.stop();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
|
@ -571,6 +391,9 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.show) {
|
||||
// if we were being shown, end the widget as we're about to be minimized.
|
||||
this._endWidgetActions();
|
||||
} else {
|
||||
// restart the widget actions
|
||||
this._resetWidget(this.props);
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
|
@ -580,94 +403,19 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
/**
|
||||
* Replace the widget template variables in a url with their values
|
||||
*
|
||||
* @param {string} u The URL with template variables
|
||||
* @param {string} widgetType The widget's type
|
||||
*
|
||||
* @returns {string} url with temlate variables replaced
|
||||
* Whether we're using a local version of the widget rather than loading the
|
||||
* actual widget URL
|
||||
* @returns {bool} true If using a local version of the widget
|
||||
*/
|
||||
_templatedUrl(u, widgetType: string) {
|
||||
const targetData = {};
|
||||
if (WidgetType.JITSI.matches(widgetType)) {
|
||||
targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
|
||||
}
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUser = MatrixClientPeg.get().getUser(myUserId);
|
||||
const vars = Object.assign(targetData, this.props.app.data, {
|
||||
'matrix_user_id': myUserId,
|
||||
'matrix_room_id': this.props.room.roomId,
|
||||
'matrix_display_name': myUser ? myUser.displayName : myUserId,
|
||||
'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
|
||||
|
||||
// TODO: Namespace themes through some standard
|
||||
'theme': SettingsStore.getValue("theme"),
|
||||
});
|
||||
|
||||
if (vars.conferenceId === undefined) {
|
||||
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
|
||||
const parsedUrl = new URL(this.props.app.url);
|
||||
vars.conferenceId = parsedUrl.searchParams.get("confId");
|
||||
}
|
||||
|
||||
return uriFromTemplate(u, vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL used in the iframe
|
||||
* In cases where we supply our own UI for a widget, this is an internal
|
||||
* URL different to the one used if the widget is popped out to a separate
|
||||
* tab / browser
|
||||
*
|
||||
* @returns {string} url
|
||||
*/
|
||||
_getRenderedUrl() {
|
||||
let url;
|
||||
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
console.log("Replacing Jitsi widget URL with local wrapper");
|
||||
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
|
||||
url = this._addWurlParams(url);
|
||||
} else {
|
||||
url = this._getSafeUrl(this.state.widgetUrl);
|
||||
}
|
||||
return this._templatedUrl(url, this.props.app.type);
|
||||
}
|
||||
|
||||
_getPopoutUrl() {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
return this._templatedUrl(
|
||||
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
|
||||
this.props.app.type,
|
||||
);
|
||||
} else {
|
||||
// use app.url, not state.widgetUrl, because we want the one without
|
||||
// the wURL params for the popped-out version.
|
||||
return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
|
||||
}
|
||||
}
|
||||
|
||||
_getSafeUrl(u) {
|
||||
const parsedWidgetUrl = url.parse(u, true);
|
||||
if (ENABLE_REACT_PERF) {
|
||||
parsedWidgetUrl.search = null;
|
||||
parsedWidgetUrl.query.react_perf = true;
|
||||
}
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
|
||||
// Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
|
||||
// We also need the dollar signs in-tact for variable substitution.
|
||||
return safeWidgetUrl.replace(/%24/g, '$');
|
||||
_usingLocalWidget() {
|
||||
return WidgetType.JITSI.matches(this.props.app.type);
|
||||
}
|
||||
|
||||
_getTileTitle() {
|
||||
const name = this.formatAppTileName();
|
||||
const titleSpacer = <span> - </span>;
|
||||
let title = '';
|
||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
|
||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle !== this.formatAppTileName()) {
|
||||
title = this.state.widgetPageTitle;
|
||||
}
|
||||
|
||||
|
@ -690,9 +438,9 @@ export default class AppTile extends React.Component {
|
|||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
|
||||
this._endWidgetActions().then(() => {
|
||||
if (this._appFrame.current) {
|
||||
if (this.iframe) {
|
||||
// Reload iframe
|
||||
this._appFrame.current.src = this._getRenderedUrl();
|
||||
this.iframe.src = this._sgWidget.embedUrl;
|
||||
this.setState({});
|
||||
}
|
||||
});
|
||||
|
@ -700,13 +448,13 @@ export default class AppTile extends React.Component {
|
|||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
{ target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
|
||||
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick() {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this._appFrame.current.src = this._appFrame.current.src;
|
||||
this.iframe.src = this.iframe.src;
|
||||
}
|
||||
|
||||
_onContextMenuClick = () => {
|
||||
|
@ -735,7 +483,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 ' : ' ');
|
||||
|
||||
|
@ -752,7 +500,7 @@ export default class AppTile extends React.Component {
|
|||
<AppPermission
|
||||
roomId={this.props.room.roomId}
|
||||
creatorUserId={this.props.creatorUserId}
|
||||
url={this.state.widgetUrl}
|
||||
url={this._sgWidget.embedUrl}
|
||||
isRoomEncrypted={isEncrypted}
|
||||
onPermissionGranted={this._grantWidgetPermission}
|
||||
/>
|
||||
|
@ -777,11 +525,11 @@ export default class AppTile extends React.Component {
|
|||
{ this.state.loading && loadingElement }
|
||||
<iframe
|
||||
allow={iframeFeatures}
|
||||
ref={this._appFrame}
|
||||
src={this._getRenderedUrl()}
|
||||
ref={this._iframeRefChange}
|
||||
src={this._sgWidget.embedUrl}
|
||||
allowFullScreen={true}
|
||||
sandbox={sandboxFlags}
|
||||
onLoad={this._onLoaded} />
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
// if the widget would be allowed to remain on screen, we must put it in
|
||||
|
@ -804,14 +552,16 @@ export default class AppTile extends React.Component {
|
|||
const showMinimiseButton = this.props.showMinimise && this.props.show;
|
||||
const showMaximiseButton = this.props.showMinimise && !this.props.show;
|
||||
|
||||
let appTileClass;
|
||||
let appTileClasses;
|
||||
if (this.props.miniMode) {
|
||||
appTileClass = 'mx_AppTile_mini';
|
||||
appTileClasses = {mx_AppTile_mini: true};
|
||||
} else if (this.props.fullWidth) {
|
||||
appTileClass = 'mx_AppTileFullWidth';
|
||||
appTileClasses = {mx_AppTileFullWidth: true};
|
||||
} else {
|
||||
appTileClass = 'mx_AppTile';
|
||||
appTileClasses = {mx_AppTile: true};
|
||||
}
|
||||
appTileClasses.mx_AppTile_minimised = !this.props.show;
|
||||
appTileClasses = classNames(appTileClasses);
|
||||
|
||||
const menuBarClasses = classNames({
|
||||
mx_AppTileMenuBar: true,
|
||||
|
@ -823,14 +573,18 @@ export default class AppTile extends React.Component {
|
|||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
||||
|
||||
const canUserModify = this._canUserModify();
|
||||
const showEditButton = Boolean(this._scalarClient && canUserModify);
|
||||
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
|
||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
const showPictureSnapshotButton = this.props.show && this._sgWidget.widgetApi &&
|
||||
this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots);
|
||||
|
||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
|
||||
<WidgetContextMenu
|
||||
onUnpinClicked={
|
||||
ActiveWidgetStore.getWidgetPersistence(this.props.app.id) ? null : this._onUnpinClicked
|
||||
}
|
||||
onRevokeClicked={this._onRevokeClicked}
|
||||
onEditClicked={showEditButton ? this._onEditClick : undefined}
|
||||
onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}
|
||||
|
@ -843,20 +597,20 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className={appTileClass} id={this.props.app.id}>
|
||||
<div className={appTileClasses} id={this.props.app.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
{ /* Minimise widget */ }
|
||||
{ showMinimiseButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
|
||||
title={_t('Minimize apps')}
|
||||
title={_t('Minimize widget')}
|
||||
onClick={this._onMinimiseClick}
|
||||
/> }
|
||||
{ /* Maximise widget */ }
|
||||
{ showMaximiseButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_maximise"
|
||||
title={_t('Maximize apps')}
|
||||
title={_t('Maximize widget')}
|
||||
onClick={this._onMinimiseClick}
|
||||
/> }
|
||||
{ /* Title */ }
|
||||
|
@ -930,9 +684,6 @@ AppTile.propTypes = {
|
|||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
whitelistCapabilities: PropTypes.array,
|
||||
// Optional function to be called on widget capability request
|
||||
// Called with an array of the requested capabilities
|
||||
onCapabilityRequest: PropTypes.func,
|
||||
// Is this an instance of a user widget
|
||||
userWidget: PropTypes.bool,
|
||||
};
|
||||
|
|
77
src/components/views/elements/DesktopBuildsNotice.tsx
Normal file
77
src/components/views/elements/DesktopBuildsNotice.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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 EventIndexPeg from "../../../indexing/EventIndexPeg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import React from "react";
|
||||
|
||||
export enum WarningKind {
|
||||
Files,
|
||||
Search,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
isRoomEncrypted: boolean;
|
||||
kind: WarningKind;
|
||||
}
|
||||
|
||||
export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
|
||||
if (!isRoomEncrypted) return null;
|
||||
if (EventIndexPeg.get()) return null;
|
||||
|
||||
const {desktopBuilds, brand} = SdkConfig.get();
|
||||
|
||||
let text = null;
|
||||
let logo = null;
|
||||
if (desktopBuilds.available) {
|
||||
logo = <img src={desktopBuilds.logo} />;
|
||||
switch (kind) {
|
||||
case WarningKind.Files:
|
||||
text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
|
||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
|
||||
});
|
||||
break;
|
||||
case WarningKind.Search:
|
||||
text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
|
||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (kind) {
|
||||
case WarningKind.Files:
|
||||
text = _t("This version of %(brand)s does not support viewing some encrypted files", {brand});
|
||||
break;
|
||||
case WarningKind.Search:
|
||||
text = _t("This version of %(brand)s does not support searching encrypted messages", {brand});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// for safety
|
||||
if (!text) {
|
||||
console.warn("Unknown desktop builds warning kind: ", kind);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DesktopBuildsNotice">
|
||||
{logo}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)} />;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
/**
|
||||
* This error boundary component can be used to wrap large content areas and
|
||||
|
@ -73,9 +74,10 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
if (this.state.error) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
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>
|
||||
|
||||
let bugReportSection;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReportSection = <React.Fragment>
|
||||
<p>{_t(
|
||||
"Please <newIssueLink>create a new issue</newIssueLink> " +
|
||||
"on GitHub so that we can investigate this bug.", {}, {
|
||||
|
@ -94,6 +96,13 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
<AccessibleButton onClick={this._onBugReport} kind='primary'>
|
||||
{_t("Submit debug logs")}
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_ErrorBoundary">
|
||||
<div className="mx_ErrorBoundary_body">
|
||||
<h1>{_t("Something went wrong!")}</h1>
|
||||
{ bugReportSection }
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,15 +14,41 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {ReactChildren, useEffect} from 'react';
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixEvent, RoomMember} from "matrix-js-sdk";
|
||||
import {useStateToggle} from "../../../hooks/useStateToggle";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
|
||||
interface IProps {
|
||||
// An array of member events to summarise
|
||||
events: MatrixEvent[];
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold?: number;
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded?: boolean,
|
||||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers?: RoomMember[],
|
||||
// The text to show as the summary of this event list
|
||||
summaryText?: string,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: ReactChildren,
|
||||
// Called when the event list expansion is toggled
|
||||
onToggle?(): void;
|
||||
}
|
||||
|
||||
const EventListSummary: React.FC<IProps> = ({
|
||||
events,
|
||||
children,
|
||||
threshold = 3,
|
||||
onToggle,
|
||||
startExpanded,
|
||||
summaryMembers = [],
|
||||
summaryText,
|
||||
}) => {
|
||||
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
|
||||
|
||||
// Whenever expanded changes call onToggle
|
||||
|
@ -75,22 +101,4 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
|
|||
);
|
||||
};
|
||||
|
||||
EventListSummary.propTypes = {
|
||||
// An array of member events to summarise
|
||||
events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: PropTypes.arrayOf(PropTypes.element).isRequired,
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold: PropTypes.number,
|
||||
// Called when the event list expansion is toggled
|
||||
onToggle: PropTypes.func,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded: PropTypes.bool,
|
||||
|
||||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers: PropTypes.arrayOf(PropTypes.instanceOf(RoomMember)),
|
||||
// The text to show as the summary of this event list
|
||||
summaryText: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EventListSummary;
|
|
@ -21,6 +21,8 @@ import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
|||
import * as Avatar from '../../../Avatar';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
|
@ -39,11 +41,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,48 +67,50 @@ 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",
|
||||
"sender": "${userId}",
|
||||
"content": {
|
||||
"m.new_content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatar_url}"
|
||||
},
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatar_url}"
|
||||
/* eslint-disable quote-props */
|
||||
const rawEvent = {
|
||||
type: "m.room.message",
|
||||
sender: userId,
|
||||
content: {
|
||||
"m.new_content": {
|
||||
msgtype: "m.text",
|
||||
body: this.props.message,
|
||||
displayname: displayname,
|
||||
avatar_url: avatarUrl,
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 97
|
||||
},
|
||||
"event_id": "$9999999999999999999999999999999999999999999",
|
||||
"room_id": "!999999999999999999:matrix.org"
|
||||
}`));
|
||||
msgtype: "m.text",
|
||||
body: this.props.message,
|
||||
displayname: displayname,
|
||||
avatar_url: avatarUrl,
|
||||
},
|
||||
unsigned: {
|
||||
age: 97,
|
||||
},
|
||||
event_id: "$9999999999999999999999999999999999999999999",
|
||||
room_id: "!999999999999999999:example.org",
|
||||
};
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
/* eslint-enable quote-props */
|
||||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
name: displayname,
|
||||
userId: userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return avatar_url;
|
||||
return avatarUrl;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -114,16 +120,17 @@ 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} />
|
||||
<EventTile
|
||||
mxEvent={event}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
72
src/components/views/elements/InfoTooltip.tsx
Normal file
72
src/components/views/elements/InfoTooltip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,14 +15,11 @@ 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 || "";
|
||||
|
@ -45,5 +42,5 @@ export default createReactClass({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
export default class ManageIntegsButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onManageIntegrations = (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
if (!managers.hasManager()) {
|
||||
managers.openNoManagerDialog();
|
||||
} else {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
managers.openAll(this.props.room);
|
||||
} else {
|
||||
managers.getPrimaryManager().open(this.props.room);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let integrationsButton = <div />;
|
||||
if (IntegrationManagers.sharedInstance().hasManager()) {
|
||||
integrationsButton = (
|
||||
<AccessibleTooltipButton
|
||||
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
|
||||
title={_t("Manage Integrations")}
|
||||
onClick={this.onManageIntegrations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return integrationsButton;
|
||||
}
|
||||
}
|
||||
|
||||
ManageIntegsButton.propTypes = {
|
||||
room: PropTypes.object.isRequired,
|
||||
};
|
|
@ -16,44 +16,67 @@ 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 React, { ReactChildren } from 'react';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import EventListSummary from "./EventListSummary";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberEventListSummary',
|
||||
interface IProps {
|
||||
// An array of member events to summarise
|
||||
events: MatrixEvent[];
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength?: number;
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength?: number;
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold?: number,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded?: boolean,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: ReactChildren;
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle?(): void,
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
// An array of member events to summarise
|
||||
events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: PropTypes.array.isRequired,
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength: PropTypes.number,
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength: PropTypes.number,
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold: PropTypes.number,
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle: PropTypes.func,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded: PropTypes.bool,
|
||||
},
|
||||
interface IUserEvents {
|
||||
// The original event
|
||||
mxEvent: MatrixEvent;
|
||||
// The display name of the user (if not, then user ID)
|
||||
displayName: string;
|
||||
// The original index of the event in this.props.events
|
||||
index: number;
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
summaryLength: 1,
|
||||
threshold: 3,
|
||||
avatarsMaxLength: 5,
|
||||
};
|
||||
},
|
||||
enum TransitionType {
|
||||
Joined = "joined",
|
||||
Left = "left",
|
||||
JoinedAndLeft = "joined_and_left",
|
||||
LeftAndJoined = "left_and_joined",
|
||||
InviteReject = "invite_reject",
|
||||
InviteWithdrawal = "invite_withdrawal",
|
||||
Invited = "invited",
|
||||
Banned = "banned",
|
||||
Unbanned = "unbanned",
|
||||
Kicked = "kicked",
|
||||
ChangedName = "changed_name",
|
||||
ChangedAvatar = "changed_avatar",
|
||||
NoChange = "no_change",
|
||||
}
|
||||
|
||||
shouldComponentUpdate: function(nextProps) {
|
||||
const SEP = ",";
|
||||
|
||||
export default class MemberEventListSummary extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
summaryLength: 1,
|
||||
threshold: 3,
|
||||
avatarsMaxLength: 5,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Update if
|
||||
// - The number of summarised events has changed
|
||||
// - or if the summary is about to toggle to become collapsed
|
||||
|
@ -62,35 +85,33 @@ 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
|
||||
* the sequences are ordered by `orderedTransitionSequences`.
|
||||
* @param {object[]} eventAggregates a map of transition sequence to array of user display names
|
||||
* @param {object} eventAggregates a map of transition sequence to array of user display names
|
||||
* or user IDs.
|
||||
* @param {string[]} orderedTransitionSequences an array which is some ordering of
|
||||
* `Object.keys(eventAggregates)`.
|
||||
* @returns {string} the textual summary of the aggregated events that occurred.
|
||||
*/
|
||||
_generateSummary: function(eventAggregates, orderedTransitionSequences) {
|
||||
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
|
||||
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||
const userNames = eventAggregates[transitions];
|
||||
const nameList = this._renderNameList(userNames);
|
||||
const nameList = this.renderNameList(userNames);
|
||||
|
||||
const splitTransitions = transitions.split(',');
|
||||
const splitTransitions = transitions.split(SEP) as TransitionType[];
|
||||
|
||||
// Some neighbouring transitions are common, so canonicalise some into "pair"
|
||||
// transitions
|
||||
const canonicalTransitions = this._getCanonicalTransitions(splitTransitions);
|
||||
const canonicalTransitions = MemberEventListSummary.getCanonicalTransitions(splitTransitions);
|
||||
// Transform into consecutive repetitions of the same transition (like 5
|
||||
// consecutive 'joined_and_left's)
|
||||
const coalescedTransitions = this._coalesceRepeatedTransitions(
|
||||
canonicalTransitions,
|
||||
);
|
||||
const coalescedTransitions = MemberEventListSummary.coalesceRepeatedTransitions(canonicalTransitions);
|
||||
|
||||
const descs = coalescedTransitions.map((t) => {
|
||||
return this._getDescriptionForTransition(
|
||||
return MemberEventListSummary.getDescriptionForTransition(
|
||||
t.transitionType, userNames.length, t.repeats,
|
||||
);
|
||||
});
|
||||
|
@ -105,7 +126,7 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
return summaries.join(", ");
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} users an array of user display names or user IDs.
|
||||
|
@ -113,9 +134,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) {
|
||||
private renderNameList(users: string[]) {
|
||||
return formatCommaSeparatedList(users, this.props.summaryLength);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalise an array of transitions such that some pairs of transitions become
|
||||
|
@ -124,22 +145,22 @@ export default createReactClass({
|
|||
* @param {string[]} transitions an array of transitions.
|
||||
* @returns {string[]} an array of transitions.
|
||||
*/
|
||||
_getCanonicalTransitions: function(transitions) {
|
||||
private static getCanonicalTransitions(transitions: TransitionType[]): TransitionType[] {
|
||||
const modMap = {
|
||||
'joined': {
|
||||
'after': 'left',
|
||||
'newTransition': 'joined_and_left',
|
||||
[TransitionType.Joined]: {
|
||||
after: TransitionType.Left,
|
||||
newTransition: TransitionType.JoinedAndLeft,
|
||||
},
|
||||
'left': {
|
||||
'after': 'joined',
|
||||
'newTransition': 'left_and_joined',
|
||||
[TransitionType.Left]: {
|
||||
after: TransitionType.Joined,
|
||||
newTransition: TransitionType.LeftAndJoined,
|
||||
},
|
||||
// $currentTransition : {
|
||||
// 'after' : $nextTransition,
|
||||
// 'newTransition' : 'new_transition_type',
|
||||
// },
|
||||
};
|
||||
const res = [];
|
||||
const res: TransitionType[] = [];
|
||||
|
||||
for (let i = 0; i < transitions.length; i++) {
|
||||
const t = transitions[i];
|
||||
|
@ -155,7 +176,7 @@ export default createReactClass({
|
|||
res.push(transition);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an array of transitions into an array of transitions and how many times
|
||||
|
@ -171,8 +192,12 @@ export default createReactClass({
|
|||
* @param {string[]} transitions the array of transitions to transform.
|
||||
* @returns {object[]} an array of coalesced transitions.
|
||||
*/
|
||||
_coalesceRepeatedTransitions: function(transitions) {
|
||||
const res = [];
|
||||
private static coalesceRepeatedTransitions(transitions: TransitionType[]) {
|
||||
const res: {
|
||||
transitionType: TransitionType;
|
||||
repeats: number;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < transitions.length; i++) {
|
||||
if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
|
||||
res[res.length - 1].repeats += 1;
|
||||
|
@ -184,7 +209,7 @@ export default createReactClass({
|
|||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* For a certain transition, t, describe what happened to the users that
|
||||
|
@ -194,7 +219,7 @@ export default createReactClass({
|
|||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written Human Readable equivalent of the transition.
|
||||
*/
|
||||
_getDescriptionForTransition(t, userCount, repeats) {
|
||||
private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
|
||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
|
@ -222,12 +247,18 @@ export default createReactClass({
|
|||
break;
|
||||
case "invite_reject":
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)srejected their invitations %(count)s times", { severalUsers: "", count: repeats })
|
||||
? _t("%(severalUsers)srejected their invitations %(count)s times", {
|
||||
severalUsers: "",
|
||||
count: repeats,
|
||||
})
|
||||
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invite_withdrawal":
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", { severalUsers: "", count: repeats })
|
||||
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", {
|
||||
severalUsers: "",
|
||||
count: repeats,
|
||||
})
|
||||
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invited":
|
||||
|
@ -268,11 +299,11 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
}
|
||||
|
||||
_getTransitionSequence: function(events) {
|
||||
return events.map(this._getTransition);
|
||||
},
|
||||
private static getTransitionSequence(events: MatrixEvent[]) {
|
||||
return events.map(MemberEventListSummary.getTransition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Label a given membership event, `e`, where `getContent().membership` has
|
||||
|
@ -282,60 +313,60 @@ 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) {
|
||||
private static getTransition(e: MatrixEvent): TransitionType {
|
||||
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)) {
|
||||
return 'invite_withdrawal';
|
||||
return TransitionType.InviteWithdrawal;
|
||||
}
|
||||
return 'invited';
|
||||
return TransitionType.Invited;
|
||||
}
|
||||
|
||||
switch (e.mxEvent.getContent().membership) {
|
||||
case 'invite': return 'invited';
|
||||
case 'ban': return 'banned';
|
||||
case 'invite': return TransitionType.Invited;
|
||||
case 'ban': return TransitionType.Banned;
|
||||
case 'join':
|
||||
if (e.mxEvent.getPrevContent().membership === 'join') {
|
||||
if (e.mxEvent.getContent().displayname !==
|
||||
e.mxEvent.getPrevContent().displayname) {
|
||||
return 'changed_name';
|
||||
return TransitionType.ChangedName;
|
||||
} else if (e.mxEvent.getContent().avatar_url !==
|
||||
e.mxEvent.getPrevContent().avatar_url) {
|
||||
return 'changed_avatar';
|
||||
return TransitionType.ChangedAvatar;
|
||||
}
|
||||
// console.log("MELS ignoring duplicate membership join event");
|
||||
return 'no_change';
|
||||
return TransitionType.NoChange;
|
||||
} else {
|
||||
return 'joined';
|
||||
return TransitionType.Joined;
|
||||
}
|
||||
case 'leave':
|
||||
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
||||
switch (e.mxEvent.getPrevContent().membership) {
|
||||
case 'invite': return 'invite_reject';
|
||||
default: return 'left';
|
||||
case 'invite': return TransitionType.InviteReject;
|
||||
default: return TransitionType.Left;
|
||||
}
|
||||
}
|
||||
switch (e.mxEvent.getPrevContent().membership) {
|
||||
case 'invite': return 'invite_withdrawal';
|
||||
case 'ban': return 'unbanned';
|
||||
case 'invite': return TransitionType.InviteWithdrawal;
|
||||
case 'ban': return TransitionType.Unbanned;
|
||||
// sender is not target and made the target leave, if not from invite/ban then this is a kick
|
||||
default: return 'kicked';
|
||||
default: return TransitionType.Kicked;
|
||||
}
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_getAggregate: function(userEvents) {
|
||||
getAggregate(userEvents: Record<string, IUserEvents[]>) {
|
||||
// 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
|
||||
// sequence during eventsToRender.
|
||||
const aggregate = {
|
||||
const aggregate: Record<string, string[]> = {
|
||||
// $aggregateType : []:string
|
||||
};
|
||||
// A map of aggregate types to the indices that order them (the index of
|
||||
// the first event for a given transition sequence)
|
||||
const aggregateIndices = {
|
||||
const aggregateIndices: Record<string, number> = {
|
||||
// $aggregateType : int
|
||||
};
|
||||
|
||||
|
@ -345,7 +376,7 @@ export default createReactClass({
|
|||
const firstEvent = userEvents[userId][0];
|
||||
const displayName = firstEvent.displayName;
|
||||
|
||||
const seq = this._getTransitionSequence(userEvents[userId]);
|
||||
const seq = MemberEventListSummary.getTransitionSequence(userEvents[userId]).join(SEP);
|
||||
if (!aggregate[seq]) {
|
||||
aggregate[seq] = [];
|
||||
aggregateIndices[seq] = -1;
|
||||
|
@ -354,8 +385,9 @@ export default createReactClass({
|
|||
aggregate[seq].push(displayName);
|
||||
|
||||
if (aggregateIndices[seq] === -1 ||
|
||||
firstEvent.index < aggregateIndices[seq]) {
|
||||
aggregateIndices[seq] = firstEvent.index;
|
||||
firstEvent.index < aggregateIndices[seq]
|
||||
) {
|
||||
aggregateIndices[seq] = firstEvent.index;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -364,30 +396,26 @@ export default createReactClass({
|
|||
names: aggregate,
|
||||
indices: aggregateIndices,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const eventsToRender = this.props.events;
|
||||
|
||||
// Map user IDs to an array of objects:
|
||||
const userEvents = {
|
||||
// $userId : [{
|
||||
// // The original event
|
||||
// mxEvent: e,
|
||||
// // The display name of the user (if not, then user ID)
|
||||
// displayName: e.target.name || userId,
|
||||
// // The original index of the event in this.props.events
|
||||
// index: index,
|
||||
// }]
|
||||
};
|
||||
// Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
|
||||
// so this works perfectly for us to match event order whilst storing the latest Avatar Member
|
||||
const latestUserAvatarMember = new Map<string, RoomMember>();
|
||||
|
||||
const avatarMembers = [];
|
||||
// Object mapping user IDs to an array of IUserEvents
|
||||
const userEvents: Record<string, IUserEvents[]> = {};
|
||||
eventsToRender.forEach((e, index) => {
|
||||
const userId = e.getStateKey();
|
||||
// Initialise a user's events
|
||||
if (!userEvents[userId]) {
|
||||
userEvents[userId] = [];
|
||||
if (e.target) avatarMembers.push(e.target);
|
||||
}
|
||||
|
||||
if (e.target) {
|
||||
latestUserAvatarMember.set(userId, e.target);
|
||||
}
|
||||
|
||||
let displayName = userId;
|
||||
|
@ -404,21 +432,20 @@ export default createReactClass({
|
|||
});
|
||||
});
|
||||
|
||||
const aggregate = this._getAggregate(userEvents);
|
||||
const aggregate = this.getAggregate(userEvents);
|
||||
|
||||
// Sort types by order of lowest event index within sequence
|
||||
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
|
||||
(seq1, seq2) => aggregate.indices[seq1] - aggregate.indices[seq2],
|
||||
);
|
||||
|
||||
const EventListSummary = sdk.getComponent("views.elements.EventListSummary");
|
||||
return <EventListSummary
|
||||
events={this.props.events}
|
||||
threshold={this.props.threshold}
|
||||
onToggle={this.props.onToggle}
|
||||
startExpanded={this.props.startExpanded}
|
||||
children={this.props.children}
|
||||
summaryMembers={avatarMembers}
|
||||
summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />;
|
||||
},
|
||||
});
|
||||
summaryMembers={[...latestUserAvatarMember.values()]}
|
||||
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {throttle} from "lodash";
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -156,7 +156,7 @@ export default class PersistedElement extends React.Component {
|
|||
child.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
updateChildPosition(child, parent) {
|
||||
updateChildPosition = throttle((child, parent) => {
|
||||
if (!child || !parent) return;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
@ -167,9 +167,9 @@ export default class PersistedElement extends React.Component {
|
|||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
});
|
||||
}
|
||||
}, 100, {trailing: true, leading: true});
|
||||
|
||||
render() {
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
return <div ref={this.collectChildContainer} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,53 +16,53 @@ 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) {
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
|
||||
// Sanity check the room - the widget may have been destroyed between render cycles, and
|
||||
// thus no room is associated anymore.
|
||||
if (!persistentWidgetInRoom) return null;
|
||||
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
|
@ -81,16 +81,17 @@ export default createReactClass({
|
|||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
show={true}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
showDelete={false}
|
||||
showMinimise={false}
|
||||
miniMode={true}
|
||||
showMenubar={false}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -30,29 +29,31 @@ import {Action} from "../../../dispatcher/actions";
|
|||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
|
||||
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;
|
||||
|
|
|
@ -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,20 @@ 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) {
|
||||
this._initStateFromProps(newProps);
|
||||
},
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this._initStateFromProps(this.props);
|
||||
}
|
||||
|
||||
_initStateFromProps: function(newProps) {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
this._initStateFromProps(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 +90,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 +100,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 +125,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 +164,5 @@ export default createReactClass({
|
|||
{ picker }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 /> }
|
||||
|
|
|
@ -28,6 +28,8 @@ import escapeHtml from "escape-html";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
|
||||
|
||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
||||
|
@ -45,8 +47,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
|
||||
|
@ -61,6 +63,12 @@ export default class ReplyThread extends React.Component {
|
|||
err: false,
|
||||
};
|
||||
|
||||
this.unmounted = false;
|
||||
this.context.on("Event.replaced", this.onEventReplaced);
|
||||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||
this.room.on("Room.redaction", this.onRoomRedaction);
|
||||
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
|
||||
|
||||
this.onQuoteClick = this.onQuoteClick.bind(this);
|
||||
this.canCollapse = this.canCollapse.bind(this);
|
||||
this.collapse = this.collapse.bind(this);
|
||||
|
@ -105,6 +113,9 @@ export default class ReplyThread extends React.Component {
|
|||
{
|
||||
allowedTags: false, // false means allow everything
|
||||
allowedAttributes: false,
|
||||
// we somehow can't allow all schemes, so we allow all that we
|
||||
// know of and mxc (for img tags)
|
||||
allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'],
|
||||
exclusiveFilter: (frame) => frame.tag === "mx-reply",
|
||||
},
|
||||
);
|
||||
|
@ -212,11 +223,6 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||
this.room.on("Room.redaction", this.onRoomRedaction);
|
||||
// same event handler as Room.redaction as for both we just do forceUpdate
|
||||
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
|
@ -226,21 +232,36 @@ export default class ReplyThread extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener("Event.replaced", this.onEventReplaced);
|
||||
if (this.room) {
|
||||
this.room.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
|
||||
}
|
||||
}
|
||||
|
||||
onRoomRedaction = (ev, room) => {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// If one of the events we are rendering gets redacted, force a re-render
|
||||
if (this.state.events.some(event => event.getId() === ev.getId())) {
|
||||
updateForEventId = (eventId) => {
|
||||
if (this.state.events.some(event => event.getId() === eventId)) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onEventReplaced = (ev) => {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// If one of the events we are rendering gets replaced, force a re-render
|
||||
this.updateForEventId(ev.getId());
|
||||
};
|
||||
|
||||
onRoomRedaction = (ev) => {
|
||||
if (this.unmounted) return;
|
||||
|
||||
const eventId = ev.getAssociatedId();
|
||||
if (!eventId) return;
|
||||
|
||||
// If one of the events we are rendering gets redacted, force a re-render
|
||||
this.updateForEventId(eventId);
|
||||
};
|
||||
|
||||
async initialize() {
|
||||
const {parentEv} = this.props;
|
||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||
|
@ -331,8 +352,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>;
|
||||
|
@ -360,6 +387,8 @@ export default class ReplyThread extends React.Component {
|
|||
isRedacted={ev.isRedacted()}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
replacingEventId={ev.replacingEventId()}
|
||||
/>
|
||||
</blockquote>;
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -27,39 +26,35 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
|
|||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
|
||||
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 class for a child of GroupFilterPanel (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,12 +134,15 @@ 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);
|
||||
const badge = GroupFilterOrderStore.getGroupBadge(this.props.tag);
|
||||
let badgeElement;
|
||||
if (badge && !this.state.hover && !this.props.menuDisplayed) {
|
||||
const badgeClasses = classNames({
|
||||
|
@ -185,5 +185,5 @@ export default createReactClass({
|
|||
{badgeElement}
|
||||
</div>
|
||||
</AccessibleTooltipButton>;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
85
src/components/views/elements/UserTagTile.tsx
Normal file
85
src/components/views/elements/UserTagTile.tsx
Normal 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 GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
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: GroupFilterOrderStore.getSelectedTags().length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.tagStoreRef.remove();
|
||||
}
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
const selected = GroupFilterOrderStore.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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,18 +21,19 @@ import classNames from "classnames";
|
|||
|
||||
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||
|
||||
interface IRule<T> {
|
||||
interface IRule<T, D = void> {
|
||||
key: string;
|
||||
final?: boolean;
|
||||
skip?(this: T, data: Data): boolean;
|
||||
test(this: T, data: Data): boolean | Promise<boolean>;
|
||||
valid?(this: T): string;
|
||||
invalid?(this: T): string;
|
||||
skip?(this: T, data: Data, derivedData: D): boolean;
|
||||
test(this: T, data: Data, derivedData: D): boolean | Promise<boolean>;
|
||||
valid?(this: T, derivedData: D): string;
|
||||
invalid?(this: T, derivedData: D): string;
|
||||
}
|
||||
|
||||
interface IArgs<T> {
|
||||
rules: IRule<T>[];
|
||||
description(this: T): React.ReactChild;
|
||||
interface IArgs<T, D = void> {
|
||||
rules: IRule<T, D>[];
|
||||
description(this: T, derivedData: D): React.ReactChild;
|
||||
deriveData?(data: Data): Promise<D>;
|
||||
}
|
||||
|
||||
export interface IFieldState {
|
||||
|
@ -53,6 +54,10 @@ export interface IValidationResult {
|
|||
* @param {Function} description
|
||||
* Function that returns a string summary of the kind of value that will
|
||||
* meet the validation rules. Shown at the top of the validation feedback.
|
||||
* @param {Function} deriveData
|
||||
* Optional function that returns a Promise to an object of generic type D.
|
||||
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
|
||||
* Useful for doing calculations per-value update once rather than in each of the above rule methods.
|
||||
* @param {Object} rules
|
||||
* An array of rules describing how to check to input value. Each rule in an object
|
||||
* and may have the following properties:
|
||||
|
@ -66,7 +71,7 @@ export interface IValidationResult {
|
|||
* A validation function that takes in the current input value and returns
|
||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
*/
|
||||
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
|
||||
export default function withValidation<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
|
||||
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
||||
if (!value && allowEmpty) {
|
||||
return {
|
||||
|
@ -75,6 +80,9 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
};
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
const derivedData = deriveData ? await deriveData(data) : undefined;
|
||||
|
||||
const results = [];
|
||||
let valid = true;
|
||||
if (rules && rules.length) {
|
||||
|
@ -87,20 +95,18 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
continue;
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
|
||||
if (rule.skip && rule.skip.call(this, data)) {
|
||||
if (rule.skip && rule.skip.call(this, data, derivedData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const ruleValid = await rule.test.call(this, data);
|
||||
const ruleValid = await rule.test.call(this, data, derivedData);
|
||||
valid = valid && ruleValid;
|
||||
if (ruleValid && rule.valid) {
|
||||
// If the rule's result is valid and has text to show for
|
||||
// the valid state, show it.
|
||||
const text = rule.valid.call(this);
|
||||
const text = rule.valid.call(this, derivedData);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
@ -112,7 +118,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
} else if (!ruleValid && rule.invalid) {
|
||||
// If the rule's result is invalid and has text to show for
|
||||
// the invalid state, show it.
|
||||
const text = rule.invalid.call(this);
|
||||
const text = rule.invalid.call(this, derivedData);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
@ -153,7 +159,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
if (description) {
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const content = description.call(this);
|
||||
const content = description.call(this, derivedData);
|
||||
summary = <div className="mx_Validation_description">{content}</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,32 +15,53 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {RefObject} from 'react';
|
||||
|
||||
import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker";
|
||||
import * as sdk from '../../../index';
|
||||
import LazyRenderList from "../elements/LazyRenderList";
|
||||
import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji";
|
||||
import Emoji from './Emoji';
|
||||
|
||||
const OVERFLOW_ROWS = 3;
|
||||
|
||||
class Category extends React.PureComponent {
|
||||
static propTypes = {
|
||||
emojis: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
};
|
||||
export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent";
|
||||
|
||||
_renderEmojiRow = (rowIndex) => {
|
||||
export interface ICategory {
|
||||
id: CategoryKey;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
visible: boolean;
|
||||
ref: RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
id: string;
|
||||
name: string;
|
||||
emojis: IEmoji[];
|
||||
selectedEmojis: Set<string>;
|
||||
heightBefore: number;
|
||||
viewportHeight: number;
|
||||
scrollTop: number;
|
||||
onClick(emoji: IEmoji): void;
|
||||
onMouseEnter(emoji: IEmoji): void;
|
||||
onMouseLeave(emoji: IEmoji): void;
|
||||
}
|
||||
|
||||
class Category extends React.PureComponent<IProps> {
|
||||
private renderEmojiRow = (rowIndex: number) => {
|
||||
const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
|
||||
const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
|
||||
const Emoji = sdk.getComponent("emojipicker.Emoji");
|
||||
return (<div key={rowIndex}>{
|
||||
emojisForRow.map(emoji =>
|
||||
<Emoji key={emoji.hexcode} emoji={emoji} selectedEmojis={selectedEmojis}
|
||||
onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />)
|
||||
emojisForRow.map(emoji => ((
|
||||
<Emoji
|
||||
key={emoji.hexcode}
|
||||
emoji={emoji}
|
||||
selectedEmojis={selectedEmojis}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
)))
|
||||
}</div>);
|
||||
};
|
||||
|
||||
|
@ -52,7 +74,6 @@ class Category extends React.PureComponent {
|
|||
for (let counter = 0; counter < rows.length; ++counter) {
|
||||
rows[counter] = counter;
|
||||
}
|
||||
const LazyRenderList = sdk.getComponent('elements.LazyRenderList');
|
||||
|
||||
const viewportTop = scrollTop;
|
||||
const viewportBottom = viewportTop + viewportHeight;
|
||||
|
@ -84,7 +105,7 @@ class Category extends React.PureComponent {
|
|||
height={localHeight}
|
||||
overflowItems={OVERFLOW_ROWS}
|
||||
overflowMargin={0}
|
||||
renderItem={this._renderEmojiRow}>
|
||||
renderItem={this.renderEmojiRow}>
|
||||
</LazyRenderList>
|
||||
</section>
|
||||
);
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,18 +16,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import {IEmoji} from "../../../emoji";
|
||||
|
||||
class Emoji extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
onMouseEnter: PropTypes.func,
|
||||
onMouseLeave: PropTypes.func,
|
||||
emoji: PropTypes.object.isRequired,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
};
|
||||
interface IProps {
|
||||
emoji: IEmoji;
|
||||
selectedEmojis?: Set<string>;
|
||||
onClick(emoji: IEmoji): void;
|
||||
onMouseEnter(emoji: IEmoji): void;
|
||||
onMouseLeave(emoji: IEmoji): void;
|
||||
}
|
||||
|
||||
class Emoji extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
|
||||
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,25 +16,43 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import * as recent from '../../../emojipicker/recent';
|
||||
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
|
||||
import {DATA_BY_CATEGORY, getEmojiFromUnicode, IEmoji} from "../../../emoji";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import Header from "./Header";
|
||||
import Search from "./Search";
|
||||
import Preview from "./Preview";
|
||||
import QuickReactions from "./QuickReactions";
|
||||
import Category, {ICategory, CategoryKey} from "./Category";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 22;
|
||||
export const EMOJI_HEIGHT = 37;
|
||||
export const EMOJIS_PER_ROW = 8;
|
||||
|
||||
class EmojiPicker extends React.Component {
|
||||
static propTypes = {
|
||||
onChoose: PropTypes.func.isRequired,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
showQuickReactions: PropTypes.bool,
|
||||
};
|
||||
interface IProps {
|
||||
selectedEmojis: Set<string>;
|
||||
showQuickReactions?: boolean;
|
||||
onChoose(unicode: string): boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
filter: string;
|
||||
previewEmoji?: IEmoji;
|
||||
scrollTop: number;
|
||||
// initial estimation of height, dialog is hardcoded to 450px height.
|
||||
// should be enough to never have blank rows of emojis as
|
||||
// 3 rows of overflow are also rendered. The actual value is updated on scroll.
|
||||
viewportHeight: number;
|
||||
}
|
||||
|
||||
class EmojiPicker extends React.Component<IProps, IState> {
|
||||
private readonly recentlyUsed: IEmoji[];
|
||||
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
|
||||
private readonly categories: ICategory[];
|
||||
|
||||
private bodyRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -42,9 +61,6 @@ class EmojiPicker extends React.Component {
|
|||
filter: "",
|
||||
previewEmoji: null,
|
||||
scrollTop: 0,
|
||||
// initial estimation of height, dialog is hardcoded to 450px height.
|
||||
// should be enough to never have blank rows of emojis as
|
||||
// 3 rows of overflow are also rendered. The actual value is updated on scroll.
|
||||
viewportHeight: 280,
|
||||
};
|
||||
|
||||
|
@ -110,18 +126,9 @@ class EmojiPicker extends React.Component {
|
|||
visible: false,
|
||||
ref: React.createRef(),
|
||||
}];
|
||||
|
||||
this.bodyRef = React.createRef();
|
||||
|
||||
this.onChangeFilter = this.onChangeFilter.bind(this);
|
||||
this.onHoverEmoji = this.onHoverEmoji.bind(this);
|
||||
this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this);
|
||||
this.onClickEmoji = this.onClickEmoji.bind(this);
|
||||
this.scrollToCategory = this.scrollToCategory.bind(this);
|
||||
this.updateVisibility = this.updateVisibility.bind(this);
|
||||
}
|
||||
|
||||
onScroll = () => {
|
||||
private onScroll = () => {
|
||||
const body = this.bodyRef.current;
|
||||
this.setState({
|
||||
scrollTop: body.scrollTop,
|
||||
|
@ -130,7 +137,7 @@ class EmojiPicker extends React.Component {
|
|||
this.updateVisibility();
|
||||
};
|
||||
|
||||
updateVisibility() {
|
||||
private updateVisibility = () => {
|
||||
const body = this.bodyRef.current;
|
||||
const rect = body.getBoundingClientRect();
|
||||
for (const cat of this.categories) {
|
||||
|
@ -147,21 +154,21 @@ class EmojiPicker extends React.Component {
|
|||
// We update this here instead of through React to avoid re-render on scroll.
|
||||
if (cat.visible) {
|
||||
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
|
||||
cat.ref.current.setAttribute("aria-selected", true);
|
||||
cat.ref.current.setAttribute("tabindex", 0);
|
||||
cat.ref.current.setAttribute("aria-selected", "true");
|
||||
cat.ref.current.setAttribute("tabindex", "0");
|
||||
} else {
|
||||
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
|
||||
cat.ref.current.setAttribute("aria-selected", false);
|
||||
cat.ref.current.setAttribute("tabindex", -1);
|
||||
cat.ref.current.setAttribute("aria-selected", "false");
|
||||
cat.ref.current.setAttribute("tabindex", "-1");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scrollToCategory(category) {
|
||||
private scrollToCategory = (category: string) => {
|
||||
this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
onChangeFilter(filter) {
|
||||
private onChangeFilter = (filter: string) => {
|
||||
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
|
||||
for (const cat of this.categories) {
|
||||
let emojis;
|
||||
|
@ -181,27 +188,34 @@ class EmojiPicker extends React.Component {
|
|||
// Header underlines need to be updated, but updating requires knowing
|
||||
// where the categories are, so we wait for a tick.
|
||||
setTimeout(this.updateVisibility, 0);
|
||||
}
|
||||
};
|
||||
|
||||
onHoverEmoji(emoji) {
|
||||
private onEnterFilter = () => {
|
||||
const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
|
||||
if (btn) {
|
||||
btn.click();
|
||||
}
|
||||
};
|
||||
|
||||
private onHoverEmoji = (emoji: IEmoji) => {
|
||||
this.setState({
|
||||
previewEmoji: emoji,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onHoverEmojiEnd(emoji) {
|
||||
private onHoverEmojiEnd = (emoji: IEmoji) => {
|
||||
this.setState({
|
||||
previewEmoji: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClickEmoji(emoji) {
|
||||
private onClickEmoji = (emoji: IEmoji) => {
|
||||
if (this.props.onChoose(emoji.unicode) !== false) {
|
||||
recent.add(emoji.unicode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_categoryHeightForEmojiCount(count) {
|
||||
private static categoryHeightForEmojiCount(count: number) {
|
||||
if (count === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -209,25 +223,37 @@ class EmojiPicker extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const Header = sdk.getComponent("emojipicker.Header");
|
||||
const Search = sdk.getComponent("emojipicker.Search");
|
||||
const Category = sdk.getComponent("emojipicker.Category");
|
||||
const Preview = sdk.getComponent("emojipicker.Preview");
|
||||
const QuickReactions = sdk.getComponent("emojipicker.QuickReactions");
|
||||
let heightBefore = 0;
|
||||
return (
|
||||
<div className="mx_EmojiPicker">
|
||||
<Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory} />
|
||||
<Search query={this.state.filter} onChange={this.onChangeFilter} />
|
||||
<AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={e => this.bodyRef.current = e} onScroll={this.onScroll}>
|
||||
<Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
|
||||
<Search query={this.state.filter} onChange={this.onChangeFilter} onEnter={this.onEnterFilter} />
|
||||
<AutoHideScrollbar
|
||||
className="mx_EmojiPicker_body"
|
||||
wrappedRef={ref => {
|
||||
// @ts-ignore - AutoHideScrollbar should accept a RefObject or fall back to its own instead
|
||||
this.bodyRef.current = ref
|
||||
}}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{this.categories.map(category => {
|
||||
const emojis = this.memoizedDataByCategory[category.id];
|
||||
const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
|
||||
heightBefore={heightBefore} viewportHeight={this.state.viewportHeight}
|
||||
scrollTop={this.state.scrollTop} emojis={emojis} onClick={this.onClickEmoji}
|
||||
onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
|
||||
selectedEmojis={this.props.selectedEmojis} />);
|
||||
const height = this._categoryHeightForEmojiCount(emojis.length);
|
||||
const categoryElement = ((
|
||||
<Category
|
||||
key={category.id}
|
||||
id={category.id}
|
||||
name={category.name}
|
||||
heightBefore={heightBefore}
|
||||
viewportHeight={this.state.viewportHeight}
|
||||
scrollTop={this.state.scrollTop}
|
||||
emojis={emojis}
|
||||
onClick={this.onClickEmoji}
|
||||
onMouseEnter={this.onHoverEmoji}
|
||||
onMouseLeave={this.onHoverEmojiEnd}
|
||||
selectedEmojis={this.props.selectedEmojis}
|
||||
/>
|
||||
));
|
||||
const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
|
||||
heightBefore += height;
|
||||
return categoryElement;
|
||||
})}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,19 +16,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from "classnames";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {CategoryKey, ICategory} from "./Category";
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onAnchorClick: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
categories: ICategory[];
|
||||
onAnchorClick(id: CategoryKey): void
|
||||
}
|
||||
|
||||
findNearestEnabled(index, delta) {
|
||||
class Header extends React.PureComponent<IProps> {
|
||||
private findNearestEnabled(index: number, delta: number) {
|
||||
index += this.props.categories.length;
|
||||
const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
|
||||
|
||||
|
@ -37,12 +38,12 @@ class Header extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
changeCategoryRelative(delta) {
|
||||
private changeCategoryRelative(delta: number) {
|
||||
const current = this.props.categories.findIndex(c => c.visible);
|
||||
this.changeCategoryAbsolute(current + delta, delta);
|
||||
}
|
||||
|
||||
changeCategoryAbsolute(index, delta=1) {
|
||||
private changeCategoryAbsolute(index: number, delta=1) {
|
||||
const category = this.props.categories[this.findNearestEnabled(index, delta)];
|
||||
if (category) {
|
||||
this.props.onAnchorClick(category.id);
|
||||
|
@ -52,7 +53,7 @@ class Header extends React.PureComponent {
|
|||
|
||||
// Implements ARIA Tabs with Automatic Activation pattern
|
||||
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
|
||||
onKeyDown = (ev) => {
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
|
@ -80,7 +81,12 @@ class Header extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}>
|
||||
<nav
|
||||
className="mx_EmojiPicker_header"
|
||||
role="tablist"
|
||||
aria-label={_t("Categories")}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{this.props.categories.map(category => {
|
||||
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
|
||||
mx_EmojiPicker_anchor_visible: category.visible,
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,19 +16,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Preview extends React.PureComponent {
|
||||
static propTypes = {
|
||||
emoji: PropTypes.object,
|
||||
};
|
||||
import {IEmoji} from "../../../emoji";
|
||||
|
||||
interface IProps {
|
||||
emoji: IEmoji;
|
||||
}
|
||||
|
||||
class Preview extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
const {
|
||||
unicode = "",
|
||||
annotation = "",
|
||||
shortcodes: [shortcode = ""],
|
||||
} = this.props.emoji || {};
|
||||
|
||||
return (
|
||||
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
|
||||
<div className="mx_EmojiPicker_preview_emoji">
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,11 +16,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {getEmojiFromUnicode} from "../../../emoji";
|
||||
import {getEmojiFromUnicode, IEmoji} from "../../../emoji";
|
||||
import Emoji from "./Emoji";
|
||||
|
||||
// We use the variation-selector Heart in Quick Reactions for some reason
|
||||
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
|
||||
|
@ -30,36 +30,36 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀
|
|||
return data;
|
||||
});
|
||||
|
||||
class QuickReactions extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
selectedEmojis: PropTypes.instanceOf(Set),
|
||||
};
|
||||
interface IProps {
|
||||
selectedEmojis?: Set<string>;
|
||||
onClick(emoji: IEmoji): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover?: IEmoji;
|
||||
}
|
||||
|
||||
class QuickReactions extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: null,
|
||||
};
|
||||
this.onMouseEnter = this.onMouseEnter.bind(this);
|
||||
this.onMouseLeave = this.onMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
onMouseEnter(emoji) {
|
||||
private onMouseEnter = (emoji: IEmoji) => {
|
||||
this.setState({
|
||||
hover: emoji,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMouseLeave() {
|
||||
private onMouseLeave = () => {
|
||||
this.setState({
|
||||
hover: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const Emoji = sdk.getComponent("emojipicker.Emoji");
|
||||
|
||||
return (
|
||||
<section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
|
||||
<h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
|
||||
|
@ -72,10 +72,16 @@ class QuickReactions extends React.Component {
|
|||
}
|
||||
</h2>
|
||||
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
|
||||
{QUICK_REACTIONS.map(emoji => <Emoji
|
||||
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
|
||||
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
|
||||
selectedEmojis={this.props.selectedEmojis} />)}
|
||||
{QUICK_REACTIONS.map(emoji => ((
|
||||
<Emoji
|
||||
key={emoji.hexcode}
|
||||
emoji={emoji}
|
||||
onClick={this.props.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
selectedEmojis={this.props.selectedEmojis}
|
||||
/>
|
||||
)))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,26 +16,29 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
class ReactionPicker extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
reactions: PropTypes.object,
|
||||
};
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
reactions: any; // TODO type this once js-sdk is more typescripted
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selectedEmojis: Set<string>;
|
||||
}
|
||||
|
||||
class ReactionPicker extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedEmojis: new Set(Object.keys(this.getReactions())),
|
||||
};
|
||||
this.onChoose = this.onChoose.bind(this);
|
||||
this.onReactionsChange = this.onReactionsChange.bind(this);
|
||||
this.addListeners();
|
||||
}
|
||||
|
||||
|
@ -45,7 +49,7 @@ class ReactionPicker extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
addListeners() {
|
||||
private addListeners() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
|
@ -55,22 +59,13 @@ class ReactionPicker extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.removeListener("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.removeListener("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
getReactions() {
|
||||
private getReactions() {
|
||||
if (!this.props.reactions) {
|
||||
return {};
|
||||
}
|
||||
|
@ -81,13 +76,13 @@ class ReactionPicker extends React.Component {
|
|||
.map(event => [event.getRelation().key, event.getId()]));
|
||||
}
|
||||
|
||||
onReactionsChange() {
|
||||
private onReactionsChange = () => {
|
||||
this.setState({
|
||||
selectedEmojis: new Set(Object.keys(this.getReactions())),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onChoose(reaction) {
|
||||
onChoose = (reaction: string) => {
|
||||
this.componentWillUnmount();
|
||||
this.props.onFinished();
|
||||
const myReactions = this.getReactions();
|
||||
|
@ -109,7 +104,7 @@ class ReactionPicker extends React.Component {
|
|||
dis.dispatch({action: "message_sent"});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <EmojiPicker
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Tulir Asokan <tulir@maunium.net>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,32 +16,41 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
class Search extends React.PureComponent {
|
||||
static propTypes = {
|
||||
query: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
query: string;
|
||||
onChange(value: string): void;
|
||||
onEnter(): void;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
class Search extends React.PureComponent<IProps> {
|
||||
private inputRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
componentDidMount() {
|
||||
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
|
||||
setTimeout(() => this.inputRef.current.focus(), 0);
|
||||
}
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
if (ev.key === Key.ENTER) {
|
||||
this.props.onEnter();
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let rightButton;
|
||||
if (this.props.query) {
|
||||
rightButton = (
|
||||
<button onClick={() => this.props.onChange("")}
|
||||
className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear"
|
||||
title={_t("Cancel search")} />
|
||||
<button
|
||||
onClick={() => this.props.onChange("")}
|
||||
className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear"
|
||||
title={_t("Cancel search")}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
rightButton = <span className="mx_EmojiPicker_search_icon" />;
|
||||
|
@ -48,8 +58,15 @@ class Search extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className="mx_EmojiPicker_search">
|
||||
<input autoFocus type="text" placeholder="Search" value={this.props.query}
|
||||
onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} />
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
value={this.props.query}
|
||||
onChange={ev => this.props.onChange(ev.target.value)}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.inputRef}
|
||||
/>
|
||||
{rightButton}
|
||||
</div>
|
||||
);
|
|
@ -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>;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue