Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into spaces-jump-to-room
This commit is contained in:
commit
746b11b24d
471 changed files with 19932 additions and 8649 deletions
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,9 +14,9 @@ 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 classnames from 'classnames';
|
||||
import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import Spinner from "../elements/Spinner";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { LocalisedPolicy, Policies } from '../../../Terms';
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -74,36 +73,72 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
* focus: set the input focus appropriately in the form.
|
||||
*/
|
||||
|
||||
enum AuthType {
|
||||
Password = "m.login.password",
|
||||
Recaptcha = "m.login.recaptcha",
|
||||
Terms = "m.login.terms",
|
||||
Email = "m.login.email.identity",
|
||||
Msisdn = "m.login.msisdn",
|
||||
Sso = "m.login.sso",
|
||||
SsoUnstable = "org.matrix.login.sso",
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IAuthDict {
|
||||
type?: AuthType;
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user?: string;
|
||||
identifier?: any;
|
||||
password?: string;
|
||||
response?: string;
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds?: any;
|
||||
threepidCreds?: any;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export const DEFAULT_PHASE = 0;
|
||||
|
||||
@replaceableComponent("views.auth.PasswordAuthEntry")
|
||||
export class PasswordAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.password";
|
||||
interface IAuthEntryProps {
|
||||
matrixClient: MatrixClient;
|
||||
loginType: string;
|
||||
authSessionId: string;
|
||||
errorText?: string;
|
||||
// Is the auth logic currently waiting for something to happen?
|
||||
busy?: boolean;
|
||||
onPhaseChange: (phase: number) => void;
|
||||
submitAuthDict: (auth: IAuthDict) => void;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
// is the auth logic currently waiting for something to
|
||||
// happen?
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IPasswordAuthEntryState {
|
||||
password: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.PasswordAuthEntry")
|
||||
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Password;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
password: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
state = {
|
||||
password: "",
|
||||
};
|
||||
|
||||
_onSubmit = e => {
|
||||
private onSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (this.props.busy) return;
|
||||
|
||||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
type: AuthType.Password,
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user: this.props.matrixClient.credentials.userId,
|
||||
|
@ -115,7 +150,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onPasswordFieldChange = ev => {
|
||||
private onPasswordFieldChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.setState({
|
||||
password: ev.target.value,
|
||||
|
@ -123,7 +158,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const passwordBoxClass = classnames({
|
||||
const passwordBoxClass = classNames({
|
||||
"error": this.props.errorText,
|
||||
});
|
||||
|
||||
|
@ -155,7 +190,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
className={passwordBoxClass}
|
||||
type="password"
|
||||
|
@ -163,7 +198,7 @@ export class PasswordAuthEntry extends React.Component {
|
|||
label={_t('Password')}
|
||||
autoFocus={true}
|
||||
value={this.state.password}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
onChange={this.onPasswordFieldChange}
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
{ submitButtonOrSpinner }
|
||||
|
@ -175,26 +210,26 @@ export class PasswordAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
||||
export class RecaptchaAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.recaptcha";
|
||||
|
||||
static propTypes = {
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
/* eslint-disable camelcase */
|
||||
interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
|
||||
stageParams?: {
|
||||
public_key?: string;
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
||||
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
|
||||
static LOGIN_TYPE = AuthType.Recaptcha;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
_onCaptchaResponse = response => {
|
||||
private onCaptchaResponse = (response: string) => {
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
||||
this.props.submitAuthDict({
|
||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
||||
type: AuthType.Recaptcha,
|
||||
response: response,
|
||||
});
|
||||
};
|
||||
|
@ -230,7 +265,7 @@ export class RecaptchaAuthEntry extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<CaptchaForm sitePublicKey={sitePublicKey}
|
||||
onCaptchaResponse={this._onCaptchaResponse}
|
||||
onCaptchaResponse={this.onCaptchaResponse}
|
||||
/>
|
||||
{ errorSection }
|
||||
</div>
|
||||
|
@ -238,18 +273,28 @@ export class RecaptchaAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.TermsAuthEntry")
|
||||
export class TermsAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.terms";
|
||||
|
||||
static propTypes = {
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
showContinue: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
interface ITermsAuthEntryProps extends IAuthEntryProps {
|
||||
stageParams?: {
|
||||
policies?: Policies;
|
||||
};
|
||||
showContinue: boolean;
|
||||
}
|
||||
|
||||
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ITermsAuthEntryState {
|
||||
policies: LocalisedPolicyWithId[];
|
||||
toggledPolicies: {
|
||||
[policy: string]: boolean;
|
||||
};
|
||||
errorText?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.TermsAuthEntry")
|
||||
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Terms;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -294,8 +339,11 @@ export class TermsAuthEntry extends React.Component {
|
|||
|
||||
initToggles[policyId] = false;
|
||||
|
||||
langPolicy.id = policyId;
|
||||
pickedPolicies.push(langPolicy);
|
||||
pickedPolicies.push({
|
||||
id: policyId,
|
||||
name: langPolicy.name,
|
||||
url: langPolicy.url,
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
|
@ -311,11 +359,11 @@ export class TermsAuthEntry extends React.Component {
|
|||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
tryContinue = () => {
|
||||
this._trySubmit();
|
||||
public tryContinue = () => {
|
||||
this.trySubmit();
|
||||
};
|
||||
|
||||
_togglePolicy(policyId) {
|
||||
private togglePolicy(policyId: string) {
|
||||
const newToggles = {};
|
||||
for (const policy of this.state.policies) {
|
||||
let checked = this.state.toggledPolicies[policy.id];
|
||||
|
@ -326,7 +374,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
this.setState({"toggledPolicies": newToggles});
|
||||
}
|
||||
|
||||
_trySubmit = () => {
|
||||
private trySubmit = () => {
|
||||
let allChecked = true;
|
||||
for (const policy of this.state.policies) {
|
||||
const checked = this.state.toggledPolicies[policy.id];
|
||||
|
@ -334,7 +382,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
}
|
||||
|
||||
if (allChecked) {
|
||||
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
||||
this.props.submitAuthDict({type: AuthType.Terms});
|
||||
CountlyAnalytics.instance.track("onboarding_terms_complete");
|
||||
} else {
|
||||
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||
|
@ -356,7 +404,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
checkboxes.push(
|
||||
// XXX: replace with StyledCheckbox
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<input type="checkbox" onChange={() => this.togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
</label>,
|
||||
);
|
||||
|
@ -375,7 +423,7 @@ export class TermsAuthEntry extends React.Component {
|
|||
if (this.props.showContinue !== false) {
|
||||
// XXX: button classes
|
||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -389,21 +437,18 @@ export class TermsAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
||||
export class EmailIdentityAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.email.identity";
|
||||
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
authSessionId: PropTypes.string.isRequired,
|
||||
clientSecret: PropTypes.string.isRequired,
|
||||
inputs: PropTypes.object.isRequired,
|
||||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
|
||||
inputs?: {
|
||||
emailAddress?: string;
|
||||
};
|
||||
stageState?: {
|
||||
emailSid: string;
|
||||
};
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
||||
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
|
||||
static LOGIN_TYPE = AuthType.Email;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
|
@ -427,7 +472,7 @@ export class EmailIdentityAuthEntry extends React.Component {
|
|||
return (
|
||||
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
|
||||
{ emailAddress: <b>{ this.props.inputs.emailAddress }</b> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||
|
@ -437,37 +482,44 @@ export class EmailIdentityAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
interface IMsisdnAuthEntryProps extends IAuthEntryProps {
|
||||
inputs: {
|
||||
phoneCountry: string;
|
||||
phoneNumber: string;
|
||||
};
|
||||
clientSecret: string;
|
||||
fail: (error: Error) => void;
|
||||
}
|
||||
|
||||
interface IMsisdnAuthEntryState {
|
||||
token: string;
|
||||
requestingToken: boolean;
|
||||
errorText: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.MsisdnAuthEntry")
|
||||
export class MsisdnAuthEntry extends React.Component {
|
||||
static LOGIN_TYPE = "m.login.msisdn";
|
||||
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Msisdn;
|
||||
|
||||
static propTypes = {
|
||||
inputs: PropTypes.shape({
|
||||
phoneCountry: PropTypes.string,
|
||||
phoneNumber: PropTypes.string,
|
||||
}),
|
||||
fail: PropTypes.func,
|
||||
clientSecret: PropTypes.func,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
};
|
||||
private submitUrl: string;
|
||||
private sid: string;
|
||||
private msisdn: string;
|
||||
|
||||
state = {
|
||||
token: '',
|
||||
requestingToken: false,
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
token: '',
|
||||
requestingToken: false,
|
||||
errorText: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
|
||||
this._submitUrl = null;
|
||||
this._sid = null;
|
||||
this._msisdn = null;
|
||||
this._tokenBox = null;
|
||||
|
||||
this.setState({requestingToken: true});
|
||||
this._requestMsisdnToken().catch((e) => {
|
||||
this.requestMsisdnToken().catch((e) => {
|
||||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
|
@ -477,26 +529,26 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
/*
|
||||
* Requests a verification token by SMS.
|
||||
*/
|
||||
_requestMsisdnToken() {
|
||||
private requestMsisdnToken(): Promise<void> {
|
||||
return this.props.matrixClient.requestRegisterMsisdnToken(
|
||||
this.props.inputs.phoneCountry,
|
||||
this.props.inputs.phoneNumber,
|
||||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
).then((result) => {
|
||||
this._submitUrl = result.submit_url;
|
||||
this._sid = result.sid;
|
||||
this._msisdn = result.msisdn;
|
||||
this.submitUrl = result.submit_url;
|
||||
this.sid = result.sid;
|
||||
this.msisdn = result.msisdn;
|
||||
});
|
||||
}
|
||||
|
||||
_onTokenChange = e => {
|
||||
private onTokenChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
token: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onFormSubmit = async e => {
|
||||
private onFormSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (this.state.token == '') return;
|
||||
|
||||
|
@ -506,20 +558,20 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
|
||||
try {
|
||||
let result;
|
||||
if (this._submitUrl) {
|
||||
if (this.submitUrl) {
|
||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
||||
this.submitUrl, this.sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else {
|
||||
throw new Error("The registration with MSISDN flow is misconfigured");
|
||||
}
|
||||
if (result.success) {
|
||||
const creds = {
|
||||
sid: this._sid,
|
||||
sid: this.sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
};
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
type: AuthType.Msisdn,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
|
@ -543,7 +595,7 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
return <Loader />;
|
||||
} else {
|
||||
const enableSubmit = Boolean(this.state.token);
|
||||
const submitClasses = classnames({
|
||||
const submitClasses = classNames({
|
||||
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||
mx_GeneralButton: true,
|
||||
});
|
||||
|
@ -558,16 +610,16 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||
{ msisdn: <i>{ this._msisdn }</i> },
|
||||
{ msisdn: <i>{ this.msisdn }</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please enter the code it contains:") }</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<input type="text"
|
||||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||
value={this.state.token}
|
||||
onChange={this._onTokenChange}
|
||||
onChange={this.onTokenChange}
|
||||
aria-label={ _t("Code")}
|
||||
/>
|
||||
<br />
|
||||
|
@ -584,40 +636,40 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.SSOAuthEntry")
|
||||
export class SSOAuthEntry extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
authSessionId: PropTypes.string.isRequired,
|
||||
loginType: PropTypes.string.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
continueText: PropTypes.string,
|
||||
continueKind: PropTypes.string,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
interface ISSOAuthEntryProps extends IAuthEntryProps {
|
||||
continueText?: string;
|
||||
continueKind?: string;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
static LOGIN_TYPE = "m.login.sso";
|
||||
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
|
||||
interface ISSOAuthEntryState {
|
||||
phase: number;
|
||||
attemptFailed: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.SSOAuthEntry")
|
||||
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
|
||||
static LOGIN_TYPE = AuthType.Sso;
|
||||
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
|
||||
|
||||
static PHASE_PREAUTH = 1; // button to start SSO
|
||||
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
||||
|
||||
_ssoUrl: string;
|
||||
private ssoUrl: string;
|
||||
private popupWindow: Window;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// We actually send the user through fallback auth so we don't have to
|
||||
// deal with a redirect back to us, losing application context.
|
||||
this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
||||
this.ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
this.popupWindow = null;
|
||||
window.addEventListener("message", this.onReceiveMessage);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
|
@ -625,44 +677,44 @@ export class SSOAuthEntry extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
window.removeEventListener("message", this.onReceiveMessage);
|
||||
if (this.popupWindow) {
|
||||
this.popupWindow.close();
|
||||
this.popupWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
attemptFailed = () => {
|
||||
public attemptFailed = () => {
|
||||
this.setState({
|
||||
attemptFailed: true,
|
||||
});
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
private onReceiveMessage = (event: MessageEvent) => {
|
||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
if (this.popupWindow) {
|
||||
this.popupWindow.close();
|
||||
this.popupWindow = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onStartAuthClick = () => {
|
||||
private onStartAuthClick = () => {
|
||||
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
||||
// certainly will need to open the thing in a new tab to avoid losing application
|
||||
// context.
|
||||
|
||||
this._popupWindow = window.open(this._ssoUrl, "_blank");
|
||||
this.popupWindow = window.open(this.ssoUrl, "_blank");
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
|
||||
onConfirmClick = () => {
|
||||
private onConfirmClick = () => {
|
||||
this.props.submitAuthDict({});
|
||||
};
|
||||
|
||||
|
@ -716,46 +768,37 @@ export class SSOAuthEntry extends React.Component {
|
|||
}
|
||||
|
||||
@replaceableComponent("views.auth.FallbackAuthEntry")
|
||||
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,
|
||||
};
|
||||
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||
private popupWindow: Window;
|
||||
private fallbackButton = createRef<HTMLAnchorElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// 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();
|
||||
this.popupWindow = null;
|
||||
window.addEventListener("message", this.onReceiveMessage);
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
window.removeEventListener("message", this.onReceiveMessage);
|
||||
if (this.popupWindow) {
|
||||
this.popupWindow.close();
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
if (this._fallbackButton.current) {
|
||||
this._fallbackButton.current.focus();
|
||||
public focus = () => {
|
||||
if (this.fallbackButton.current) {
|
||||
this.fallbackButton.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onShowFallbackClick = e => {
|
||||
private onShowFallbackClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -763,10 +806,10 @@ export class FallbackAuthEntry extends React.Component {
|
|||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url, "_blank");
|
||||
this.popupWindow = window.open(url, "_blank");
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
private onReceiveMessage = (event: MessageEvent) => {
|
||||
if (
|
||||
event.data === "authDone" &&
|
||||
event.origin === this.props.matrixClient.getHomeserverUrl()
|
||||
|
@ -786,27 +829,31 @@ export class FallbackAuthEntry extends React.Component {
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
|
||||
_t("Start authentication")
|
||||
}</a>
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const AuthEntryComponents = [
|
||||
PasswordAuthEntry,
|
||||
RecaptchaAuthEntry,
|
||||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
TermsAuthEntry,
|
||||
SSOAuthEntry,
|
||||
];
|
||||
|
||||
export default function getEntryComponentForLoginType(loginType) {
|
||||
for (const c of AuthEntryComponents) {
|
||||
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
|
||||
return c;
|
||||
}
|
||||
export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
|
||||
switch (loginType) {
|
||||
case AuthType.Password:
|
||||
return PasswordAuthEntry;
|
||||
case AuthType.Recaptcha:
|
||||
return RecaptchaAuthEntry;
|
||||
case AuthType.Email:
|
||||
return EmailIdentityAuthEntry;
|
||||
case AuthType.Msisdn:
|
||||
return MsisdnAuthEntry;
|
||||
case AuthType.Terms:
|
||||
return TermsAuthEntry;
|
||||
case AuthType.Sso:
|
||||
case AuthType.SsoUnstable:
|
||||
return SSOAuthEntry;
|
||||
default:
|
||||
return FallbackAuthEntry;
|
||||
}
|
||||
return FallbackAuthEntry;
|
||||
}
|
|
@ -17,15 +17,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { toPx } from "../../../utils/units";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
interface IProps {
|
||||
|
@ -44,12 +46,12 @@ interface IProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
const calculateUrls = (url, urls) => {
|
||||
const calculateUrls = (url, urls, lowBandwidth) => {
|
||||
// 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")) {
|
||||
if (!lowBandwidth) {
|
||||
_urls = urls || [];
|
||||
|
||||
if (url) {
|
||||
|
@ -63,7 +65,13 @@ const calculateUrls = (url, urls) => {
|
|||
};
|
||||
|
||||
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
|
||||
// Since this is a hot code path and the settings store can be slow, we
|
||||
// use the cached lowBandwidth value from the room context if it exists
|
||||
const roomContext = useContext(RoomContext);
|
||||
const lowBandwidth = roomContext ?
|
||||
roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
|
||||
|
||||
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
|
||||
const [urlsIndex, setIndex] = useState<number>(0);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
|
@ -71,7 +79,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setUrls(calculateUrls(url, urls));
|
||||
setUrls(calculateUrls(url, urls, lowBandwidth));
|
||||
setIndex(0);
|
||||
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
|
@ -179,7 +187,7 @@ const BaseAvatar = (props: IProps) => {
|
|||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
title={title} alt={_t("Avatar")}
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
|
|
|
@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { TagID } from '../../../stores/room-list/models';
|
||||
import RoomAvatar from "./RoomAvatar";
|
||||
import NotificationBadge from '../rooms/NotificationBadge';
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
|
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
interface IProps {
|
||||
room: Room;
|
||||
avatarSize: number;
|
||||
tag: TagID;
|
||||
displayBadge?: boolean;
|
||||
forceCount?: boolean;
|
||||
oobData?: object;
|
||||
|
@ -121,7 +119,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
if (this.props.room.roomId !== room.roomId) return;
|
||||
|
||||
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
||||
this.setState({icon: this.calculateIcon()});
|
||||
const newIcon = this.calculateIcon();
|
||||
if (newIcon !== this.state.icon) {
|
||||
this.setState({icon: newIcon});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -15,10 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
export interface IProps {
|
||||
groupId?: string;
|
||||
|
|
|
@ -16,14 +16,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||
member: RoomMember;
|
||||
|
|
|
@ -13,17 +13,17 @@ 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, {ComponentProps} from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
|
|
116
src/components/views/beta/BetaCard.tsx
Normal file
116
src/components/views/beta/BetaCard.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2021 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 {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import Modal from "../../../Modal";
|
||||
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
featureId: string;
|
||||
}
|
||||
|
||||
export const BetaPill = ({ onClick }: { onClick?: () => void }) => {
|
||||
if (onClick) {
|
||||
return <TextWithTooltip
|
||||
class={classNames("mx_BetaCard_betaPill", {
|
||||
mx_BetaCard_betaPill_clickable: !!onClick,
|
||||
})}
|
||||
tooltip={<div>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ _t("Spaces is a beta feature") }
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ _t("Tap for more info") }
|
||||
</div>
|
||||
</div>}
|
||||
onClick={onClick}
|
||||
tooltipProps={{ yOffset: -10 }}
|
||||
>
|
||||
{ _t("Beta") }
|
||||
</TextWithTooltip>;
|
||||
}
|
||||
|
||||
return <span
|
||||
className={classNames("mx_BetaCard_betaPill", {
|
||||
mx_BetaCard_betaPill_clickable: !!onClick,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
{ _t("Beta") }
|
||||
</span>;
|
||||
};
|
||||
|
||||
const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
||||
const info = SettingsStore.getBetaInfo(featureId);
|
||||
if (!info) return null; // Beta is invalid/disabled
|
||||
|
||||
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
|
||||
const value = SettingsStore.getValue(featureId);
|
||||
|
||||
let feedbackButton;
|
||||
if (value && feedbackLabel && feedbackSubheading && SdkConfig.get().bug_report_endpoint_url) {
|
||||
feedbackButton = <AccessibleButton
|
||||
onClick={() => {
|
||||
Modal.createTrackedDialog("Beta Feedback", featureId, BetaFeedbackDialog, { featureId });
|
||||
}}
|
||||
kind="primary"
|
||||
>
|
||||
{ _t("Feedback") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_BetaCard">
|
||||
<div className="mx_BetaCard_columns">
|
||||
<div>
|
||||
<h3 className="mx_BetaCard_title">
|
||||
{ titleOverride || _t(title) }
|
||||
<BetaPill />
|
||||
</h3>
|
||||
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
|
||||
<div className="mx_BetaCard_buttons">
|
||||
{ feedbackButton }
|
||||
<AccessibleButton
|
||||
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
|
||||
kind={feedbackButton ? "primary_outline" : "primary"}
|
||||
>
|
||||
{ value ? _t("Leave the beta") : _t("Join the beta") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ disclaimer && <div className="mx_BetaCard_disclaimer">
|
||||
{ disclaimer(value) }
|
||||
</div> }
|
||||
</div>
|
||||
<img src={image} alt="" />
|
||||
</div>
|
||||
{ extraSettings && <div className="mx_BetaCard_relatedSettings">
|
||||
{ extraSettings.map(key => (
|
||||
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
|
||||
)) }
|
||||
</div> }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default BetaCard;
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import Field from "../elements/Field";
|
||||
import Dialpad from '../voip/DialPad';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -44,13 +45,21 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
|||
this.setState({value: this.state.value + digit});
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.setState({value: ev.target.value});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <ContextMenu {...this.props}>
|
||||
<div className="mx_DialPadContextMenu_header">
|
||||
<div>
|
||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div>
|
||||
<Field className="mx_DialPadContextMenu_dialled"
|
||||
value={this.state.value} autoFocus={true}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_horizSep" />
|
||||
<div className="mx_DialPadContextMenu_dialPad">
|
||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -28,9 +28,12 @@ import Resend from '../../../Resend';
|
|||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
export function canCancel(eventStatus) {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
|
@ -82,7 +85,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
||||
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
|
||||
|
||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
||||
|
@ -92,7 +95,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
|
||||
_isPinned() {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
||||
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
|
||||
if (!pinnedEvent) return false;
|
||||
const content = pinnedEvent.getContent();
|
||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||
|
@ -156,34 +159,32 @@ export default class MessageContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
onForwardClick = () => {
|
||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
event: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
});
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
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.
|
||||
if (e.errcode === "M_NOT_FOUND") return null;
|
||||
throw e;
|
||||
})
|
||||
.then((event) => {
|
||||
const eventIds = (event ? event.pinned : []) || [];
|
||||
if (!eventIds.includes(this.props.mxEvent.getId())) {
|
||||
// Not pinned - add
|
||||
eventIds.push(this.props.mxEvent.getId());
|
||||
} else {
|
||||
// Pinned - remove
|
||||
eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1);
|
||||
}
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
|
||||
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
|
||||
if (pinnedIds.includes(eventId)) {
|
||||
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
||||
} else {
|
||||
pinnedIds.push(eventId);
|
||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
|
||||
eventId,
|
||||
],
|
||||
});
|
||||
}
|
||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
|
@ -200,7 +201,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
|
||||
onQuoteClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'quote',
|
||||
action: Action.ComposerInsert,
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
|
@ -256,55 +257,68 @@ export default class MessageContextMenu extends React.Component {
|
|||
let externalURLButton;
|
||||
let quoteButton;
|
||||
let collapseReplyThread;
|
||||
let redactItemList;
|
||||
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
if (!mxEvent.isRedacted()) {
|
||||
if (unsentReactionsCount !== 0) {
|
||||
resendReactionsButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
||||
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconResend"
|
||||
label={ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||
onClick={this.onResendReactionsClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconRedact"
|
||||
label={_t("Remove")}
|
||||
onClick={this.onRedactClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isContentActionable(mxEvent)) {
|
||||
forwardButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconForward"
|
||||
label={_t("Forward")}
|
||||
onClick={this.onForwardClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPin"
|
||||
label={ this._isPinned() ? _t('Unpin') : _t('Pin') }
|
||||
onClick={this.onPinClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const viewSourceButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
{ _t('View Source') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconSource"
|
||||
label={_t("View source")}
|
||||
onClick={this.onViewSourceClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps) {
|
||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||
unhidePreviewButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
{ _t('Unhide Preview') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
|
||||
label={_t("Show preview")}
|
||||
onClick={this.onUnhidePreviewClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -315,77 +329,97 @@ export default class MessageContextMenu extends React.Component {
|
|||
}
|
||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||
const permalinkButton = (
|
||||
<MenuItem
|
||||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPermalink"
|
||||
onClick={this.onPermalinkClick}
|
||||
label= {_t('Share')}
|
||||
element="a"
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
</MenuItem>
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps) { // this event is rendered using TextualBody
|
||||
quoteButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
{ _t('Quote') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconQuote"
|
||||
label={_t("Quote")}
|
||||
onClick={this.onQuoteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Bridges can provide a 'external_url' to link back to the source.
|
||||
if (
|
||||
typeof(mxEvent.event.content.external_url) === "string" &&
|
||||
if (typeof (mxEvent.event.content.external_url) === "string" &&
|
||||
isUrlPermitted(mxEvent.event.content.external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<MenuItem
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconLink"
|
||||
onClick={this.closeMenu}
|
||||
label={ _t('Source URL') }
|
||||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
onClick={this.closeMenu}
|
||||
href={mxEvent.event.content.external_url}
|
||||
>
|
||||
{ _t('Source URL') }
|
||||
</MenuItem>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.collapseReplyThread) {
|
||||
collapseReplyThread = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
{ _t('Collapse Reply Thread') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconCollapse"
|
||||
label={_t("Collapse reply thread")}
|
||||
onClick={this.onCollapseReplyThreadClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
|
||||
{ _t('Report Content') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconReport"
|
||||
label={_t("Report")}
|
||||
onClick={this.onReportEventClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const commonItemsList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ quoteButton }
|
||||
{ forwardButton }
|
||||
{ pinButton }
|
||||
{ permalinkButton }
|
||||
{ reportEventButton }
|
||||
{ externalURLButton }
|
||||
{ unhidePreviewButton }
|
||||
{ viewSourceButton }
|
||||
{ resendReactionsButton }
|
||||
{ collapseReplyThread }
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
|
||||
if (redactButton) {
|
||||
redactItemList = (
|
||||
<IconizedContextMenuOptionList red>
|
||||
{ redactButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MessageContextMenu">
|
||||
{ resendReactionsButton }
|
||||
{ redactButton }
|
||||
{ forwardButton }
|
||||
{ pinButton }
|
||||
{ viewSourceButton }
|
||||
{ unhidePreviewButton }
|
||||
{ permalinkButton }
|
||||
{ quoteButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ reportEventButton }
|
||||
</div>
|
||||
<IconizedContextMenu
|
||||
{...this.props}
|
||||
className="mx_MessageContextMenu"
|
||||
compact={true}
|
||||
>
|
||||
{ commonItemsList }
|
||||
{ redactItemList }
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions';
|
|||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
|
||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
|
||||
this._onRemoveClick = this._onRemoveClick.bind(this);
|
||||
}
|
||||
|
||||
_onViewCommunityClick() {
|
||||
_onViewCommunityClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.tag,
|
||||
});
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
_onRemoveClick() {
|
||||
_onRemoveClick = () => {
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
_onMoveUp = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onMoveDown = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
render() {
|
||||
let moveUp;
|
||||
let moveDown;
|
||||
if (this.props.index > 0) {
|
||||
moveUp = (
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
|
||||
{ _t("Move up") }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
|
||||
moveDown = (
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
|
||||
{ _t("Move down") }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||
{ _t('View Community') }
|
||||
</MenuItem>
|
||||
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" role="separator" /> : null }
|
||||
{ moveUp }
|
||||
{ moveDown }
|
||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||
{ _t('Hide') }
|
||||
{ _t("Unpin") }
|
||||
</MenuItem>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
|||
showUnpin?: boolean;
|
||||
// override delete handler
|
||||
onDeleteClick?(): void;
|
||||
// override edit handler
|
||||
onEditClick?(): void;
|
||||
}
|
||||
|
||||
const WidgetContextMenu: React.FC<IProps> = ({
|
||||
|
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
app,
|
||||
userWidget,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
showUnpin,
|
||||
...props
|
||||
}) => {
|
||||
|
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
|
||||
let editButton;
|
||||
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
||||
const onEditClick = () => {
|
||||
WidgetUtils.editWidget(room, app);
|
||||
const _onEditClick = () => {
|
||||
if (onEditClick) {
|
||||
onEditClick();
|
||||
} else {
|
||||
WidgetUtils.editWidget(room, app);
|
||||
}
|
||||
onFinished();
|
||||
};
|
||||
|
||||
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
|
||||
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
|
||||
}
|
||||
|
||||
let snapshotButton;
|
||||
|
@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
|
||||
let deleteButton;
|
||||
if (onDeleteClick || canModify) {
|
||||
const onDeleteClickDefault = () => {
|
||||
// Show delete confirmation dialog
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||
},
|
||||
});
|
||||
const _onDeleteClick = () => {
|
||||
if (onDeleteClick) {
|
||||
onDeleteClick();
|
||||
} else {
|
||||
// Show delete confirmation dialog
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onFinished();
|
||||
};
|
||||
|
||||
deleteButton = <IconizedContextMenuOption
|
||||
onClick={onDeleteClick || onDeleteClickDefault}
|
||||
onClick={_onDeleteClick}
|
||||
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useMemo, useState} from "react";
|
||||
import React, {ReactNode, useContext, useMemo, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
@ -36,6 +36,12 @@ import StyledCheckbox from "../elements/StyledCheckbox";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -45,7 +51,10 @@ interface IProps extends IDialogProps {
|
|||
|
||||
const Entry = ({ room, checked, onChange }) => {
|
||||
return <label className="mx_AddExistingToSpace_entry">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
{ room?.isSpaceRoom()
|
||||
? <RoomAvatar room={room} height={32} width={32} />
|
||||
: <DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||
}
|
||||
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
||||
<StyledCheckbox
|
||||
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||
|
@ -57,150 +66,59 @@ const Entry = ({ room, checked, onChange }) => {
|
|||
|
||||
interface IAddExistingToSpaceProps {
|
||||
space: Room;
|
||||
selected: Set<Room>;
|
||||
onChange(checked: boolean, room: Room): void;
|
||||
footerPrompt?: ReactNode;
|
||||
emptySelectionButton?: ReactNode;
|
||||
onFinished(added: boolean): void;
|
||||
}
|
||||
|
||||
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => {
|
||||
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||
space,
|
||||
footerPrompt,
|
||||
emptySelectionButton,
|
||||
onFinished,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
|
||||
const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]);
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const existingSubspacesSet = new Set(existingSubspaces);
|
||||
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
|
||||
|
||||
const joinRule = space.getJoinRule();
|
||||
const [spaces, rooms, dms] = visibleRooms.reduce((arr, room) => {
|
||||
if (room.getMyMembership() !== "join") return arr;
|
||||
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
|
||||
|
||||
if (room.isSpaceRoom()) {
|
||||
if (room !== space && !existingSubspacesSet.has(room)) {
|
||||
arr[0].push(room);
|
||||
}
|
||||
} else if (!existingRoomsSet.has(room)) {
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||
arr[1].push(room);
|
||||
} else if (joinRule !== "public") {
|
||||
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
|
||||
arr[2].push(room);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [[], [], []]);
|
||||
|
||||
return <div className="mx_AddExistingToSpace">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
{ rooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selected.has(space)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, space);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ dms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Direct Messages") }</h3>
|
||||
{ dms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||
|
||||
const [progress, setProgress] = useState<number>(null);
|
||||
const [error, setError] = useState<Error>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase().trim();
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspaces.length > 0) {
|
||||
const options = [space, ...existingSubspaces].map((space) => {
|
||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||
const existingSubspacesSet = useMemo(() => new Set(SpaceStore.instance.getChildSpaces(space.roomId)), [space]);
|
||||
const existingRoomsSet = useMemo(() => new Set(SpaceStore.instance.getChildRooms(space.roomId)), [space]);
|
||||
|
||||
const [spaces, rooms, dms] = useMemo(() => {
|
||||
let rooms = visibleRooms;
|
||||
|
||||
if (lcQuery) {
|
||||
const matcher = new QueryMatcher<Room>(visibleRooms, {
|
||||
keys: ["name"],
|
||||
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
});
|
||||
|
||||
spaceOptionSection = (
|
||||
<Dropdown
|
||||
id="mx_SpaceSelectDropdown"
|
||||
onOptionChange={(key: string) => {
|
||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||
}}
|
||||
value={selectedSpace.roomId}
|
||||
label={_t("Space selection")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
}
|
||||
rooms = matcher.match(lcQuery);
|
||||
}
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
const joinRule = space.getJoinRule();
|
||||
return sortRooms(rooms).reduce((arr, room) => {
|
||||
if (room.isSpaceRoom()) {
|
||||
if (room !== space && !existingSubspacesSet.has(room)) {
|
||||
arr[0].push(room);
|
||||
}
|
||||
} else if (!existingRoomsSet.has(room)) {
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||
arr[1].push(room);
|
||||
} else if (joinRule !== "public") {
|
||||
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
|
||||
arr[2].push(room);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [[], [], []]);
|
||||
}, [visibleRooms, space, lcQuery, existingRoomsSet, existingSubspacesSet]);
|
||||
|
||||
const addRooms = async () => {
|
||||
setError(null);
|
||||
|
@ -264,20 +182,161 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
let button = emptySelectionButton;
|
||||
if (!button || selectedToAdd.size > 0) {
|
||||
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
footer = <>
|
||||
<span>
|
||||
<div>{ _t("Want to add a new room instead?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
{ footerPrompt }
|
||||
</span>
|
||||
|
||||
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
{ button }
|
||||
</>;
|
||||
}
|
||||
|
||||
const onChange = !busy && !error ? (checked, room) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
} : null;
|
||||
|
||||
const [truncateAt, setTruncateAt] = useState(20);
|
||||
function overflowTile(overflowCount, totalCount) {
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={() => setTruncateAt(totalCount)} />
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_AddExistingToSpace">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
<div className="mx_AddExistingToSpace_section_experimental">
|
||||
<div>{ _t("Feeling experimental?") }</div>
|
||||
<div>{ _t("You can add existing spaces to a space.") }</div>
|
||||
</div>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, space);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ dms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Direct Messages") }</h3>
|
||||
{ dms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
|
||||
<div className="mx_AddExistingToSpace_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspaces.length > 0) {
|
||||
const options = [space, ...existingSubspaces].map((space) => {
|
||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
});
|
||||
|
||||
spaceOptionSection = (
|
||||
<Dropdown
|
||||
id="mx_SpaceSelectDropdown"
|
||||
onOptionChange={(key: string) => {
|
||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||
}}
|
||||
value={selectedSpace.roomId}
|
||||
label={_t("Space selection")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
return <BaseDialog
|
||||
title={title}
|
||||
className="mx_AddExistingToSpaceDialog"
|
||||
|
@ -288,21 +347,17 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
<MatrixClientContext.Provider value={cli}>
|
||||
<AddExistingToSpace
|
||||
space={space}
|
||||
selected={selectedToAdd}
|
||||
onChange={!busy && !error ? (checked, room) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
} : null}
|
||||
onFinished={onFinished}
|
||||
footerPrompt={<>
|
||||
<div>{ _t("Want to add a new room instead?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</>}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
|
||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from '../../../email';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
|
|
|
@ -15,39 +15,41 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
unknownProfileUsers: Array<{
|
||||
userId: string;
|
||||
errorText: string;
|
||||
}>;
|
||||
onInviteAnyways: () => void;
|
||||
onGiveUp: () => void;
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
|
||||
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 = () => {
|
||||
export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
||||
private onInviteClicked = (): void => {
|
||||
this.props.onInviteAnyways();
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onInviteNeverWarnClicked = () => {
|
||||
private onInviteNeverWarnClicked = (): void => {
|
||||
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
|
||||
this.props.onInviteAnyways();
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onGiveUpClicked = () => {
|
||||
private onGiveUpClicked = (): void => {
|
||||
this.props.onGiveUp();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const errorList = this.props.unknownProfileUsers
|
||||
|
@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component {
|
|||
|
||||
return (
|
||||
<BaseDialog className='mx_RetryInvitesDialog'
|
||||
onFinished={this._onGiveUpClicked}
|
||||
onFinished={this.onGiveUpClicked}
|
||||
title={_t('The following users may not exist')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
{/* eslint-disable-next-line */}
|
||||
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
|
||||
<ul>
|
||||
{ errorList }
|
||||
|
@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
|
|||
</div>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onGiveUpClicked}>
|
||||
<button onClick={this.onGiveUpClicked}>
|
||||
{ _t('Close') }
|
||||
</button>
|
||||
<button onClick={this._onInviteNeverWarnClicked}>
|
||||
<button onClick={this.onInviteNeverWarnClicked}>
|
||||
{ _t('Invite anyway and never warn me again') }
|
||||
</button>
|
||||
<button onClick={this._onInviteClicked} autoFocus={true}>
|
||||
<button onClick={this.onInviteClicked} autoFocus={true}>
|
||||
{ _t('Invite anyway') }
|
||||
</button>
|
||||
</div>
|
111
src/components/views/dialogs/BetaFeedbackDialog.tsx
Normal file
111
src/components/views/dialogs/BetaFeedbackDialog.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {submitFeedback} from "../../../rageshake/submit-rageshake";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
featureId: string;
|
||||
}
|
||||
|
||||
const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
|
||||
const info = SettingsStore.getBetaInfo(featureId);
|
||||
|
||||
const [comment, setComment] = useState("");
|
||||
const [canContact, setCanContact] = useState(false);
|
||||
|
||||
const sendFeedback = async (ok: boolean) => {
|
||||
if (!ok) return onFinished(false);
|
||||
|
||||
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
|
||||
o[k] = SettingsStore.getValue(k);
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
|
||||
onFinished(true);
|
||||
|
||||
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
|
||||
title: _t("Beta feedback"),
|
||||
description: _t("Thank you for your feedback, we really appreciate it."),
|
||||
button: _t("Done"),
|
||||
hasCloseButton: false,
|
||||
fixedWidth: false,
|
||||
});
|
||||
};
|
||||
|
||||
return (<QuestionDialog
|
||||
className="mx_BetaFeedbackDialog"
|
||||
hasCancelButton={true}
|
||||
title={_t("%(featureName)s beta feedback", { featureName: info.title })}
|
||||
description={<React.Fragment>
|
||||
<div className="mx_BetaFeedbackDialog_subheading">
|
||||
{ _t(info.feedbackSubheading) }
|
||||
|
||||
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.")}
|
||||
|
||||
<AccessibleButton kind="link" onClick={() => {
|
||||
onFinished(false);
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
}}>
|
||||
{ _t("To leave the beta, visit your settings.") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Feedback")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
element="textarea"
|
||||
onChange={(ev) => {
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
<StyledCheckbox
|
||||
checked={canContact}
|
||||
onClick={e => setCanContact((e.target as HTMLInputElement).checked)}
|
||||
>
|
||||
{ _t("You may contact me if you have any follow up questions") }
|
||||
</StyledCheckbox>
|
||||
</React.Fragment>}
|
||||
button={_t("Send feedback")}
|
||||
buttonDisabled={!comment}
|
||||
onFinished={sendFeedback}
|
||||
/>);
|
||||
};
|
||||
|
||||
export default BetaFeedbackDialog;
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -27,8 +26,27 @@ import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-ragesh
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
initialText?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
sendLogs: boolean;
|
||||
busy: boolean;
|
||||
err: string;
|
||||
issueUrl: string;
|
||||
text: string;
|
||||
progress: string;
|
||||
downloadBusy: boolean;
|
||||
downloadProgress: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.BugReportDialog")
|
||||
export default class BugReportDialog extends React.Component {
|
||||
export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||
private unmounted: boolean;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component {
|
|||
downloadBusy: false,
|
||||
downloadProgress: null,
|
||||
};
|
||||
this._unmounted = false;
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onTextChange = this._onTextChange.bind(this);
|
||||
this._onIssueUrlChange = this._onIssueUrlChange.bind(this);
|
||||
this._onSendLogsChange = this._onSendLogsChange.bind(this);
|
||||
this._sendProgressCallback = this._sendProgressCallback.bind(this);
|
||||
this._downloadProgressCallback = this._downloadProgressCallback.bind(this);
|
||||
this.unmounted = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
_onCancel(ev) {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onSubmit(ev) {
|
||||
private onSubmit = (): void => {
|
||||
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
|
||||
this.setState({
|
||||
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
|
||||
|
@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component {
|
|||
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
|
||||
|
||||
this.setState({ busy: true, progress: null, err: null });
|
||||
this._sendProgressCallback(_t("Preparing to send logs"));
|
||||
this.sendProgressCallback(_t("Preparing to send logs"));
|
||||
|
||||
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText,
|
||||
sendLogs: true,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
progressCallback: this.sendProgressCallback,
|
||||
label: this.props.label,
|
||||
}).then(() => {
|
||||
if (!this._unmounted) {
|
||||
if (!this.unmounted) {
|
||||
this.props.onFinished(false);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
||||
|
@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
}, (err) => {
|
||||
if (!this._unmounted) {
|
||||
if (!this.unmounted) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
progress: null,
|
||||
|
@ -101,14 +112,14 @@ export default class BugReportDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onDownload = async (ev) => {
|
||||
private onDownload = async (): Promise<void> => {
|
||||
this.setState({ downloadBusy: true });
|
||||
this._downloadProgressCallback(_t("Preparing to download logs"));
|
||||
this.downloadProgressCallback(_t("Preparing to download logs"));
|
||||
|
||||
try {
|
||||
await downloadBugReport({
|
||||
sendLogs: true,
|
||||
progressCallback: this._downloadProgressCallback,
|
||||
progressCallback: this.downloadProgressCallback,
|
||||
label: this.props.label,
|
||||
});
|
||||
|
||||
|
@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component {
|
|||
downloadProgress: null,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!this._unmounted) {
|
||||
if (!this.unmounted) {
|
||||
this.setState({
|
||||
downloadBusy: false,
|
||||
downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
|
||||
|
@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onTextChange(ev) {
|
||||
this.setState({ text: ev.target.value });
|
||||
private onTextChange = (ev: React.FormEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({ text: ev.currentTarget.value });
|
||||
}
|
||||
|
||||
_onIssueUrlChange(ev) {
|
||||
this.setState({ issueUrl: ev.target.value });
|
||||
private onIssueUrlChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({ issueUrl: ev.currentTarget.value });
|
||||
}
|
||||
|
||||
_onSendLogsChange(ev) {
|
||||
this.setState({ sendLogs: ev.target.checked });
|
||||
}
|
||||
|
||||
_sendProgressCallback(progress) {
|
||||
if (this._unmounted) {
|
||||
private sendProgressCallback = (progress: string): void => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({progress: progress});
|
||||
this.setState({ progress });
|
||||
}
|
||||
|
||||
_downloadProgressCallback(downloadProgress) {
|
||||
if (this._unmounted) {
|
||||
private downloadProgressCallback = (downloadProgress: string): void => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({ downloadProgress });
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this.onCancel}
|
||||
title={_t('Submit debug logs')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
|
@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {
|
|||
</b></p>
|
||||
|
||||
<div className="mx_BugReportDialog_download">
|
||||
<AccessibleButton onClick={this._onDownload} kind="link" disabled={this.state.downloadBusy}>
|
||||
<AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
|
||||
{ _t("Download logs") }
|
||||
</AccessibleButton>
|
||||
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
|
||||
|
@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component {
|
|||
type="text"
|
||||
className="mx_BugReportDialog_field_input"
|
||||
label={_t("GitHub issue")}
|
||||
onChange={this._onIssueUrlChange}
|
||||
onChange={this.onIssueUrlChange}
|
||||
value={this.state.issueUrl}
|
||||
placeholder="https://github.com/vector-im/element-web/issues/..."
|
||||
/>
|
||||
|
@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component {
|
|||
element="textarea"
|
||||
label={_t("Notes")}
|
||||
rows={5}
|
||||
onChange={this._onTextChange}
|
||||
onChange={this.onTextChange}
|
||||
value={this.state.text}
|
||||
placeholder={_t(
|
||||
"If there is additional context that would help in " +
|
||||
|
@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component {
|
|||
{error}
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Send logs")}
|
||||
onPrimaryButtonClick={this._onSubmit}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
focus={true}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BugReportDialog.propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
initialText: PropTypes.string,
|
||||
};
|
|
@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
interface IProps {
|
||||
newVersion: string;
|
||||
version: string;
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
|
||||
|
||||
export default class ChangelogDialog extends React.Component {
|
||||
export default class ChangelogDialog extends React.Component<IProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount() {
|
||||
const version = this.props.newVersion.split('-');
|
||||
const version2 = this.props.version.split('-');
|
||||
if (version == null || version2 == null) return;
|
||||
|
@ -49,7 +54,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_elementsForCommit(commit) {
|
||||
private elementsForCommit(commit): JSX.Element {
|
||||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||
|
@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
|
||||
|
@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
msg: this.state[repo],
|
||||
});
|
||||
} else {
|
||||
content = this.state[repo].map(this._elementsForCommit);
|
||||
content = this.state[repo].map(this.elementsForCommit);
|
||||
}
|
||||
return (
|
||||
<div key={repo}>
|
||||
|
@ -99,9 +104,3 @@ export default class ChangelogDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangelogDialog.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
newVersion: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
|
@ -17,7 +17,17 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
redact: () => Promise<void>;
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isRedacting: boolean;
|
||||
redactionErrorCode: string | number;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
|
@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
* To avoid this, we keep the dialog open as long as /redact is in progress.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
|
||||
export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
||||
export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -41,7 +51,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
onParentFinished = async (proceed) => {
|
||||
public onParentFinished = async (proceed: boolean): Promise<void> => {
|
||||
if (proceed) {
|
||||
this.setState({isRedacting: true});
|
||||
try {
|
||||
|
@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
if (this.state.isRedacting) {
|
||||
if (this.state.redactionErrorCode) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
@ -19,11 +19,15 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
|
||||
export default class ConfirmRedactDialog extends React.Component {
|
||||
export default class ConfirmRedactDialog extends React.Component<IProps> {
|
||||
render() {
|
||||
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
||||
return (
|
|
@ -15,13 +15,31 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: RoomMember;
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType;
|
||||
// needed if a group member is specified
|
||||
matrixClient?: MatrixClient,
|
||||
action: string; // eg. 'Ban'
|
||||
title: string; // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
// be the string entered.
|
||||
askReason?: boolean;
|
||||
danger?: boolean;
|
||||
onFinished: (success: boolean, reason?: string) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
|
@ -32,53 +50,23 @@ import {mediaFromMxc} from "../../../customisations/Media";
|
|||
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
|
||||
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'
|
||||
groupMember: GroupMemberType,
|
||||
// needed if a group member is specified
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
action: PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
// be the string entered.
|
||||
askReason: PropTypes.bool,
|
||||
danger: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
||||
private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
static defaultProps = {
|
||||
danger: false,
|
||||
askReason: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._reasonField = null;
|
||||
}
|
||||
|
||||
onOk = () => {
|
||||
let reason;
|
||||
if (this._reasonField) {
|
||||
reason = this._reasonField.value;
|
||||
}
|
||||
this.props.onFinished(true, reason);
|
||||
public onOk = (): void => {
|
||||
this.props.onFinished(true, this.reasonField.current?.value);
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
public onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
_collectReasonField = e => {
|
||||
this._reasonField = e;
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
|
@ -92,7 +80,7 @@ export default class ConfirmUserActionDialog extends React.Component {
|
|||
<div>
|
||||
<form onSubmit={this.onOk}>
|
||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||
ref={this._collectReasonField}
|
||||
ref={this.reasonField}
|
||||
placeholder={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
|
@ -15,22 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
|
||||
export default class ConfirmWipeDeviceDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
_onConfirm = () => {
|
||||
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
|
||||
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
|
||||
private onConfirm = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onDecline = () => {
|
||||
private onDecline = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
|
@ -55,10 +54,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Clear all data")}
|
||||
onPrimaryButtonClick={this._onConfirm}
|
||||
onPrimaryButtonClick={this.onConfirm}
|
||||
primaryButtonClass="danger"
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onDecline}
|
||||
onCancel={this.onDecline}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -15,44 +15,51 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
||||
export default class CreateGroupDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
state = {
|
||||
interface IState {
|
||||
groupName: string;
|
||||
groupId: string;
|
||||
groupIdError: string;
|
||||
creating: boolean;
|
||||
createError: Error;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
||||
export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||
public state = {
|
||||
groupName: '',
|
||||
groupId: '',
|
||||
groupError: null,
|
||||
groupIdError: '',
|
||||
creating: false,
|
||||
createError: null,
|
||||
};
|
||||
|
||||
_onGroupNameChange = e => {
|
||||
private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupName: e.target.value,
|
||||
groupName: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onGroupIdChange = e => {
|
||||
private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupId: e.target.value,
|
||||
groupId: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onGroupIdBlur = e => {
|
||||
this._checkGroupId();
|
||||
private onGroupIdBlur = (): void => {
|
||||
this.checkGroupId();
|
||||
};
|
||||
|
||||
_checkGroupId(e) {
|
||||
private checkGroupId() {
|
||||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
|
@ -67,12 +74,12 @@ export default class CreateGroupDialog extends React.Component {
|
|||
return error;
|
||||
}
|
||||
|
||||
_onFormSubmit = e => {
|
||||
private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this._checkGroupId()) return;
|
||||
if (this.checkGroupId()) return;
|
||||
|
||||
const profile = {};
|
||||
const profile: any = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
|
@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component {
|
|||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
|
@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
|
|||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
autoFocus={true} size="64"
|
||||
autoFocus={true} size={64}
|
||||
placeholder={_t('Example')}
|
||||
onChange={this._onGroupNameChange}
|
||||
onChange={this.onGroupNameChange}
|
||||
value={this.state.groupName}
|
||||
/>
|
||||
</div>
|
||||
|
@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component {
|
|||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size="32"
|
||||
size={32}
|
||||
placeholder={_t('example')}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
onChange={this.onGroupIdChange}
|
||||
onBlur={this.onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 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,27 +15,47 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import withValidation from '../elements/Validation';
|
||||
import withValidation, { IFieldState } from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import BaseDialog from "../dialogs/BaseDialog";
|
||||
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
interface IProps {
|
||||
defaultPublic?: boolean;
|
||||
defaultName?: string;
|
||||
parentSpace?: Room;
|
||||
onFinished(proceed: boolean, opts?: IOpts): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isPublic: boolean;
|
||||
isEncrypted: boolean;
|
||||
name: string;
|
||||
topic: string;
|
||||
alias: string;
|
||||
detailsOpen: boolean;
|
||||
noFederate: boolean;
|
||||
nameIsValid: boolean;
|
||||
canChangeEncryption: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
||||
export default class CreateRoomDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
defaultPublic: PropTypes.bool,
|
||||
parentSpace: PropTypes.instanceOf(Room),
|
||||
};
|
||||
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
private nameField = createRef<Field>();
|
||||
private aliasField = createRef<RoomAliasField>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -44,7 +64,7 @@ export default class CreateRoomDialog extends React.Component {
|
|||
this.state = {
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: privateShouldBeEncrypted(),
|
||||
name: "",
|
||||
name: this.props.defaultName || "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
detailsOpen: false,
|
||||
|
@ -53,27 +73,26 @@ export default class CreateRoomDialog extends React.Component {
|
|||
canChangeEncryption: true,
|
||||
};
|
||||
|
||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
|
||||
.then(isForced => this.setState({canChangeEncryption: !isForced}));
|
||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat)
|
||||
.then(isForced => this.setState({ canChangeEncryption: !isForced }));
|
||||
}
|
||||
|
||||
_roomCreateOptions() {
|
||||
const opts = {};
|
||||
const createOpts = opts.createOpts = {};
|
||||
private roomCreateOptions() {
|
||||
const opts: IOpts = {};
|
||||
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||
createOpts.name = this.state.name;
|
||||
if (this.state.isPublic) {
|
||||
createOpts.visibility = "public";
|
||||
createOpts.preset = "public_chat";
|
||||
createOpts.visibility = Visibility.Public;
|
||||
createOpts.preset = Preset.PublicChat;
|
||||
opts.guestAccess = false;
|
||||
const {alias} = this.state;
|
||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
||||
createOpts['room_alias_name'] = localPart;
|
||||
const { alias } = this.state;
|
||||
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
||||
}
|
||||
if (this.state.topic) {
|
||||
createOpts.topic = this.state.topic;
|
||||
}
|
||||
if (this.state.noFederate) {
|
||||
createOpts.creation_content = {'m.federate': false};
|
||||
createOpts.creation_content = { 'm.federate': false };
|
||||
}
|
||||
|
||||
if (!this.state.isPublic) {
|
||||
|
@ -98,16 +117,14 @@ export default class CreateRoomDialog extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
|
||||
// move focus to first field when showing dialog
|
||||
this._nameFieldRef.focus();
|
||||
this.nameField.current.focus();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
|
||||
}
|
||||
|
||||
_onKeyDown = event => {
|
||||
private onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === Key.ENTER) {
|
||||
this.onOk();
|
||||
event.preventDefault();
|
||||
|
@ -115,26 +132,26 @@ export default class CreateRoomDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onOk = async () => {
|
||||
const activeElement = document.activeElement;
|
||||
private onOk = async () => {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
await this._nameFieldRef.validate({allowEmpty: false});
|
||||
if (this._aliasFieldRef) {
|
||||
await this._aliasFieldRef.validate({allowEmpty: false});
|
||||
await this.nameField.current.validate({allowEmpty: false});
|
||||
if (this.aliasField.current) {
|
||||
await this.aliasField.current.validate({allowEmpty: false});
|
||||
}
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
|
||||
this.props.onFinished(true, this._roomCreateOptions());
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) {
|
||||
this.props.onFinished(true, this.roomCreateOptions());
|
||||
} else {
|
||||
let field;
|
||||
if (!this.state.nameIsValid) {
|
||||
field = this._nameFieldRef;
|
||||
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
|
||||
field = this._aliasFieldRef;
|
||||
field = this.nameField.current;
|
||||
} else if (this.aliasField.current && !this.aliasField.current.isValid) {
|
||||
field = this.aliasField.current;
|
||||
}
|
||||
if (field) {
|
||||
field.focus();
|
||||
|
@ -143,49 +160,45 @@ export default class CreateRoomDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
onNameChange = ev => {
|
||||
this.setState({name: ev.target.value});
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: ev.target.value });
|
||||
};
|
||||
|
||||
onTopicChange = ev => {
|
||||
this.setState({topic: ev.target.value});
|
||||
private onTopicChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ topic: ev.target.value });
|
||||
};
|
||||
|
||||
onPublicChange = isPublic => {
|
||||
this.setState({isPublic});
|
||||
private onPublicChange = (isPublic: boolean) => {
|
||||
this.setState({ isPublic });
|
||||
};
|
||||
|
||||
onEncryptedChange = isEncrypted => {
|
||||
this.setState({isEncrypted});
|
||||
private onEncryptedChange = (isEncrypted: boolean) => {
|
||||
this.setState({ isEncrypted });
|
||||
};
|
||||
|
||||
onAliasChange = alias => {
|
||||
this.setState({alias});
|
||||
private onAliasChange = (alias: string) => {
|
||||
this.setState({ alias });
|
||||
};
|
||||
|
||||
onDetailsToggled = ev => {
|
||||
this.setState({detailsOpen: ev.target.open});
|
||||
private onDetailsToggled = (ev: SyntheticEvent<HTMLDetailsElement>) => {
|
||||
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
|
||||
};
|
||||
|
||||
onNoFederateChange = noFederate => {
|
||||
this.setState({noFederate});
|
||||
private onNoFederateChange = (noFederate: boolean) => {
|
||||
this.setState({ noFederate });
|
||||
};
|
||||
|
||||
collectDetailsRef = ref => {
|
||||
this._detailsRef = ref;
|
||||
};
|
||||
|
||||
onNameValidate = async fieldState => {
|
||||
const result = await CreateRoomDialog._validateRoomName(fieldState);
|
||||
private onNameValidate = async (fieldState: IFieldState) => {
|
||||
const result = await CreateRoomDialog.validateRoomName(fieldState);
|
||||
this.setState({nameIsValid: result.valid});
|
||||
return result;
|
||||
};
|
||||
|
||||
static _validateRoomName = withValidation({
|
||||
private static validateRoomName = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -196,18 +209,17 @@ export default class CreateRoomDialog extends React.Component {
|
|||
});
|
||||
|
||||
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 aliasField;
|
||||
if (this.state.isPublic) {
|
||||
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} />
|
||||
<RoomAliasField
|
||||
ref={this.aliasField}
|
||||
onChange={this.onAliasChange}
|
||||
domain={domain}
|
||||
value={this.state.alias}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -270,16 +282,34 @@ export default class CreateRoomDialog extends React.Component {
|
|||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
|
||||
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_content">
|
||||
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
||||
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
|
||||
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
||||
<Field
|
||||
ref={this.nameField}
|
||||
label={_t('Name')}
|
||||
onChange={this.onNameChange}
|
||||
onValidate={this.onNameValidate}
|
||||
value={this.state.name}
|
||||
className="mx_CreateRoomDialog_name"
|
||||
/>
|
||||
<Field
|
||||
label={_t('Topic (optional)')}
|
||||
onChange={this.onTopicChange}
|
||||
value={this.state.topic}
|
||||
className="mx_CreateRoomDialog_topic"
|
||||
/>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("Make this room public")}
|
||||
onChange={this.onPublicChange}
|
||||
value={this.state.isPublic}
|
||||
/>
|
||||
{ publicPrivateLabel }
|
||||
{ e2eeSection }
|
||||
{ aliasField }
|
||||
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
||||
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">
|
||||
{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }
|
||||
</summary>
|
||||
<LabelledToggleSwitch
|
||||
label={_t(
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.",
|
|
@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default (props) => {
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
export default (props: IProps) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
const _onLogoutClicked = () => {
|
||||
|
@ -40,7 +44,7 @@ export default (props) => {
|
|||
onFinished: (doLogout) => {
|
||||
if (doLogout) {
|
||||
dis.dispatch({action: 'logout'});
|
||||
props.onFinished();
|
||||
props.onFinished(true);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv
|
|||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
shouldErase: boolean;
|
||||
errStr: string;
|
||||
authData: any; // for UIA
|
||||
authEnabled: boolean; // see usages for information
|
||||
|
||||
// A few strings that are passed to InteractiveAuth for design or are displayed
|
||||
// next to the InteractiveAuth component.
|
||||
bodyText: string;
|
||||
continueText: string;
|
||||
continueKind: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
continueKind: null,
|
||||
};
|
||||
|
||||
this._initAuth(/* shouldErase= */false);
|
||||
this.initAuth(/* shouldErase= */false);
|
||||
}
|
||||
|
||||
_onStagePhaseChange = (stage, phase) => {
|
||||
private onStagePhaseChange = (stage: string, phase: string): void => {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
|
||||
|
@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this.setState({bodyText, continueText, continueKind});
|
||||
};
|
||||
|
||||
_onUIAuthFinished = (success, result, extra) => {
|
||||
private onUIAuthFinished = (success: boolean, result: Error) => {
|
||||
if (success) return; // great! makeRequest() will be called too.
|
||||
|
||||
if (result === ERROR_USER_CANCELLED) {
|
||||
this._onCancel();
|
||||
this.onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Error during UI Auth:", {result, extra});
|
||||
console.error("Error during UI Auth:", { result });
|
||||
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
||||
};
|
||||
|
||||
_onUIAuthComplete = (auth) => {
|
||||
private onUIAuthComplete = (auth: any): void => {
|
||||
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
|
||||
// Deactivation worked - logout & close this dialog
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
|
@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onEraseFieldChange = (ev) => {
|
||||
private onEraseFieldChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
shouldErase: ev.currentTarget.checked,
|
||||
|
||||
// Disable the auth form because we're going to have to reinitialize the auth
|
||||
// information. We do this because we can't modify the parameters in the UIA
|
||||
|
@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
|
||||
// As mentioned above, set up for auth again to get updated UIA session info
|
||||
this._initAuth(/* shouldErase= */ev.target.checked);
|
||||
this.initAuth(/* shouldErase= */ev.currentTarget.checked);
|
||||
};
|
||||
|
||||
_onCancel() {
|
||||
private onCancel(): void {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_initAuth(shouldErase) {
|
||||
private initAuth(shouldErase: boolean): void {
|
||||
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
|
||||
// If we got here, oops. The server didn't require any auth.
|
||||
// Our application lifecycle will catch the error and do the logout bits.
|
||||
|
@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let error = null;
|
||||
|
@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
<InteractiveAuth
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
authData={this.state.authData}
|
||||
makeRequest={this._onUIAuthComplete}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
onStagePhaseChange={this._onStagePhaseChange}
|
||||
makeRequest={this.onUIAuthComplete}
|
||||
onAuthFinished={this.onUIAuthFinished}
|
||||
onStagePhaseChange={this.onStagePhaseChange}
|
||||
continueText={this.state.continueText}
|
||||
continueKind={this.state.continueKind}
|
||||
/>
|
||||
|
@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
<p>
|
||||
<StyledCheckbox
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
onChange={this.onEraseFieldChange}
|
||||
>
|
||||
{_t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
|
@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeactivateAccountDialog.propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2018-2021 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,14 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
||||
import {
|
||||
PHASE_UNSENT,
|
||||
|
@ -30,27 +30,33 @@ import {
|
|||
PHASE_DONE,
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
VerificationRequest,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import {SETTINGS} from "../../../settings/Settings";
|
||||
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
|
||||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { SETTINGS } from "../../../settings/Settings";
|
||||
import SettingsStore, { LEVEL_ORDER } from "../../../settings/SettingsStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { SettingLevel } from '../../../settings/SettingLevel';
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
interface IGenericEditorProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
}
|
||||
interface IGenericEditorState {
|
||||
message?: string;
|
||||
[inputId: string]: boolean | string;
|
||||
}
|
||||
|
||||
onBack() {
|
||||
abstract class GenericEditor<
|
||||
P extends IGenericEditorProps = IGenericEditorProps,
|
||||
S extends IGenericEditorState = IGenericEditorState,
|
||||
> extends React.PureComponent<P, S> {
|
||||
protected onBack = () => {
|
||||
if (this.state.message) {
|
||||
this.setState({ message: null });
|
||||
} else {
|
||||
|
@ -58,47 +64,60 @@ class GenericEditor extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
protected onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
// @ts-ignore: Unsure how to convince TS this is okay when the state
|
||||
// type can be extended.
|
||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||
}
|
||||
|
||||
_buttons() {
|
||||
protected abstract send();
|
||||
|
||||
protected buttons(): React.ReactNode {
|
||||
return <div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||
</div>;
|
||||
}
|
||||
|
||||
textInput(id, label) {
|
||||
protected textInput(id: string, label: string): React.ReactNode {
|
||||
return <Field
|
||||
id={id}
|
||||
label={label}
|
||||
size="42"
|
||||
size={42}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
autoComplete="on"
|
||||
value={this.state[id]}
|
||||
onChange={this._onChange}
|
||||
value={this.state[id] as string}
|
||||
onChange={this.onChange}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export class SendCustomEvent extends GenericEditor {
|
||||
static getLabel() { return _t('Send Custom Event'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
forceStateEvent: PropTypes.bool,
|
||||
forceGeneralEvent: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
interface ISendCustomEventProps extends IGenericEditorProps {
|
||||
room: Room;
|
||||
forceStateEvent?: boolean;
|
||||
forceGeneralEvent?: boolean;
|
||||
inputs?: {
|
||||
eventType?: string;
|
||||
stateKey?: string;
|
||||
evContent?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISendCustomEventState extends IGenericEditorState {
|
||||
isStateEvent: boolean;
|
||||
eventType: string;
|
||||
stateKey: string;
|
||||
evContent: string;
|
||||
}
|
||||
|
||||
export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendCustomEventState> {
|
||||
static getLabel() { return _t('Send Custom Event'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, stateKey, evContent} = Object.assign({
|
||||
eventType: '',
|
||||
|
@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
};
|
||||
}
|
||||
|
||||
send(content) {
|
||||
private doSend(content: object): Promise<void> {
|
||||
const cli = this.context;
|
||||
if (this.state.isStateEvent) {
|
||||
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
||||
|
@ -124,7 +143,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
}
|
||||
}
|
||||
|
||||
async _send() {
|
||||
protected send = async () => {
|
||||
if (this.state.eventType === '') {
|
||||
this.setState({ message: _t('You must specify an event type!') });
|
||||
return;
|
||||
|
@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
let message;
|
||||
try {
|
||||
const content = JSON.parse(this.state.evContent);
|
||||
await this.send(content);
|
||||
await this.doSend(content);
|
||||
message = _t('Event sent!');
|
||||
} catch (e) {
|
||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||
|
@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
|
|||
<div className="mx_Dialog_content">
|
||||
{ this.state.message }
|
||||
</div>
|
||||
{ this._buttons() }
|
||||
{ this.buttons() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -163,35 +182,51 @@ export class SendCustomEvent extends GenericEditor {
|
|||
<br />
|
||||
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||
{ showTglFlip && <div style={{float: "right"}}>
|
||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
|
||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||
type="checkbox"
|
||||
checked={this.state.isStateEvent}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label className="mx_DevTools_tgl-btn"
|
||||
data-tg-off="Event"
|
||||
data-tg-on="State Event"
|
||||
htmlFor="isStateEvent"
|
||||
/>
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class SendAccountData extends GenericEditor {
|
||||
static getLabel() { return _t('Send Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
isRoomAccountData: PropTypes.bool,
|
||||
forceMode: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
interface ISendAccountDataProps extends IGenericEditorProps {
|
||||
room: Room;
|
||||
isRoomAccountData: boolean;
|
||||
forceMode: boolean;
|
||||
inputs?: {
|
||||
eventType?: string;
|
||||
evContent?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISendAccountDataState extends IGenericEditorState {
|
||||
isRoomAccountData: boolean;
|
||||
eventType: string;
|
||||
evContent: string;
|
||||
}
|
||||
|
||||
class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountDataState> {
|
||||
static getLabel() { return _t('Send Account Data'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, evContent} = Object.assign({
|
||||
eventType: '',
|
||||
|
@ -206,7 +241,7 @@ class SendAccountData extends GenericEditor {
|
|||
};
|
||||
}
|
||||
|
||||
send(content) {
|
||||
private doSend(content: object): Promise<void> {
|
||||
const cli = this.context;
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
||||
|
@ -214,7 +249,7 @@ class SendAccountData extends GenericEditor {
|
|||
return cli.setAccountData(this.state.eventType, content);
|
||||
}
|
||||
|
||||
async _send() {
|
||||
protected send = async () => {
|
||||
if (this.state.eventType === '') {
|
||||
this.setState({ message: _t('You must specify an event type!') });
|
||||
return;
|
||||
|
@ -223,7 +258,7 @@ class SendAccountData extends GenericEditor {
|
|||
let message;
|
||||
try {
|
||||
const content = JSON.parse(this.state.evContent);
|
||||
await this.send(content);
|
||||
await this.doSend(content);
|
||||
message = _t('Event sent!');
|
||||
} catch (e) {
|
||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||
|
@ -237,7 +272,7 @@ class SendAccountData extends GenericEditor {
|
|||
<div className="mx_Dialog_content">
|
||||
{ this.state.message }
|
||||
</div>
|
||||
{ this._buttons() }
|
||||
{ this.buttons() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -247,14 +282,23 @@ class SendAccountData extends GenericEditor {
|
|||
<br />
|
||||
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && <div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||
type="checkbox"
|
||||
checked={this.state.isRoomAccountData}
|
||||
disabled={this.props.forceMode}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label className="mx_DevTools_tgl-btn"
|
||||
data-tg-off="Account Data"
|
||||
data-tg-on="Room Data"
|
||||
htmlFor="isRoomAccountData"
|
||||
/>
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -264,17 +308,22 @@ class SendAccountData extends GenericEditor {
|
|||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
query: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
interface IFilteredListProps {
|
||||
children: React.ReactElement[];
|
||||
query: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
static filterChildren(children, query) {
|
||||
interface IFilteredListState {
|
||||
filteredChildren: React.ReactElement[];
|
||||
truncateAt: number;
|
||||
}
|
||||
|
||||
class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredListState> {
|
||||
static filterChildren(children: React.ReactElement[], query: string): React.ReactElement[] {
|
||||
if (!query) return children;
|
||||
const lcQuery = query.toLowerCase();
|
||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
return children.filter((child) => child.key.toString().toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -295,27 +344,27 @@ class FilteredList extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
private showAll = () => {
|
||||
this.setState({
|
||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||
});
|
||||
};
|
||||
|
||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
private createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||
</button>;
|
||||
};
|
||||
|
||||
onQuery = (ev) => {
|
||||
private onQuery = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||
};
|
||||
|
||||
getChildren = (start: number, end: number) => {
|
||||
private getChildren = (start: number, end: number): React.ReactElement[] => {
|
||||
return this.state.filteredChildren.slice(start, end);
|
||||
};
|
||||
|
||||
getChildCount = (): number => {
|
||||
private getChildCount = (): number => {
|
||||
return this.state.filteredChildren.length;
|
||||
};
|
||||
|
||||
|
@ -336,28 +385,31 @@ class FilteredList extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class RoomStateExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Room State'); }
|
||||
interface IExplorerProps {
|
||||
room: Room;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
interface IRoomStateExplorerState {
|
||||
eventType?: string;
|
||||
event?: MatrixEvent;
|
||||
editing: boolean;
|
||||
queryEventType: string;
|
||||
queryStateKey: string;
|
||||
}
|
||||
|
||||
class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateExplorerState> {
|
||||
static getLabel() { return _t('Explore Room State'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
||||
private roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.roomStateEvents = this.props.room.currentState.events;
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
||||
this.onQueryStateKey = this.onQueryStateKey.bind(this);
|
||||
|
||||
this.state = {
|
||||
eventType: null,
|
||||
event: null,
|
||||
|
@ -368,19 +420,19 @@ class RoomStateExplorer extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
browseEventType(eventType) {
|
||||
private browseEventType(eventType: string) {
|
||||
return () => {
|
||||
this.setState({ eventType });
|
||||
};
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
private onViewSourceClick(event: MatrixEvent) {
|
||||
return () => {
|
||||
this.setState({ event });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
private onBack = () => {
|
||||
if (this.state.editing) {
|
||||
this.setState({ editing: false });
|
||||
} else if (this.state.event) {
|
||||
|
@ -392,15 +444,15 @@ class RoomStateExplorer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
editEv() {
|
||||
private editEv = () => {
|
||||
this.setState({ editing: true });
|
||||
}
|
||||
|
||||
onQueryEventType(filterEventType) {
|
||||
private onQueryEventType = (filterEventType: string) => {
|
||||
this.setState({ queryEventType: filterEventType });
|
||||
}
|
||||
|
||||
onQueryStateKey(filterStateKey) {
|
||||
private onQueryStateKey = (filterStateKey: string) => {
|
||||
this.setState({ queryStateKey: filterStateKey });
|
||||
}
|
||||
|
||||
|
@ -472,24 +524,22 @@ class RoomStateExplorer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class AccountDataExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Account Data'); }
|
||||
interface IAccountDataExplorerState {
|
||||
[inputId: string]: boolean | string | any;
|
||||
isRoomAccountData: boolean;
|
||||
event?: MatrixEvent;
|
||||
editing: boolean;
|
||||
queryEventType: string;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
|
||||
static getLabel() { return _t('Explore Account Data'); }
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
||||
|
||||
this.state = {
|
||||
isRoomAccountData: false,
|
||||
event: null,
|
||||
|
@ -499,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
getData() {
|
||||
private getData(): Record<string, MatrixEvent> {
|
||||
if (this.state.isRoomAccountData) {
|
||||
return this.props.room.accountData;
|
||||
}
|
||||
return this.context.store.accountData;
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
private onViewSourceClick(event: MatrixEvent) {
|
||||
return () => {
|
||||
this.setState({ event });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
private onBack = () => {
|
||||
if (this.state.editing) {
|
||||
this.setState({ editing: false });
|
||||
} else if (this.state.event) {
|
||||
|
@ -522,15 +572,15 @@ class AccountDataExplorer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
private onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||
}
|
||||
|
||||
editEv() {
|
||||
private editEv = () => {
|
||||
this.setState({ editing: true });
|
||||
}
|
||||
|
||||
onQueryEventType(queryEventType) {
|
||||
private onQueryEventType = (queryEventType: string) => {
|
||||
this.setState({ queryEventType });
|
||||
}
|
||||
|
||||
|
@ -580,30 +630,39 @@ class AccountDataExplorer extends React.PureComponent {
|
|||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
||||
</div> }
|
||||
<div style={{float: "right"}}>
|
||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||
type="checkbox"
|
||||
checked={this.state.isRoomAccountData}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label className="mx_DevTools_tgl-btn"
|
||||
data-tg-off="Account Data"
|
||||
data-tg-on="Room Data"
|
||||
htmlFor="isRoomAccountData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class ServersInRoomList extends React.PureComponent {
|
||||
interface IServersInRoomListState {
|
||||
query: string;
|
||||
}
|
||||
|
||||
class ServersInRoomList extends React.PureComponent<IExplorerProps, IServersInRoomListState> {
|
||||
static getLabel() { return _t('View Servers in Room'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private servers: React.ReactElement[];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = this.props.room;
|
||||
const servers = new Set();
|
||||
const servers = new Set<string>();
|
||||
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
||||
this.servers = Array.from(servers).map(s =>
|
||||
<button key={s} className="mx_DevTools_ServersInRoomList_button">
|
||||
|
@ -615,7 +674,7 @@ class ServersInRoomList extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
onQuery = (query) => {
|
||||
private onQuery = (query: string) => {
|
||||
this.setState({ query });
|
||||
}
|
||||
|
||||
|
@ -642,7 +701,10 @@ const PHASE_MAP = {
|
|||
[PHASE_CANCELLED]: "cancelled",
|
||||
};
|
||||
|
||||
function VerificationRequest({txnId, request}) {
|
||||
const VerificationRequestExplorer: React.FC<{
|
||||
txnId: string;
|
||||
request: VerificationRequest;
|
||||
}> = ({txnId, request}) => {
|
||||
const [, updateState] = useState();
|
||||
const [timeout, setRequestTimeout] = useState(request.timeout);
|
||||
|
||||
|
@ -679,7 +741,7 @@ function VerificationRequest({txnId, request}) {
|
|||
</div>);
|
||||
}
|
||||
|
||||
class VerificationExplorer extends React.Component {
|
||||
class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||
static getLabel() {
|
||||
return _t("Verification Requests");
|
||||
}
|
||||
|
@ -687,7 +749,7 @@ class VerificationExplorer extends React.Component {
|
|||
/* Ensure this.context is the cli */
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
onNewRequest = () => {
|
||||
private onNewRequest = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
|
@ -704,13 +766,13 @@ class VerificationExplorer extends React.Component {
|
|||
render() {
|
||||
const cli = this.context;
|
||||
const room = this.props.room;
|
||||
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
|
||||
const inRoomChannel = cli.crypto.inRoomVerificationRequests;
|
||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||
|
||||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
||||
<VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -720,7 +782,12 @@ class VerificationExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class WidgetExplorer extends React.Component {
|
||||
interface IWidgetExplorerState {
|
||||
query: string;
|
||||
editWidget?: IApp;
|
||||
}
|
||||
|
||||
class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerState> {
|
||||
static getLabel() {
|
||||
return _t("Active Widgets");
|
||||
}
|
||||
|
@ -734,19 +801,19 @@ class WidgetExplorer extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onWidgetStoreUpdate = () => {
|
||||
private onWidgetStoreUpdate = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onQueryChange = (query) => {
|
||||
private onQueryChange = (query: string) => {
|
||||
this.setState({query});
|
||||
};
|
||||
|
||||
onEditWidget = (widget) => {
|
||||
private onEditWidget = (widget: IApp) => {
|
||||
this.setState({editWidget: widget});
|
||||
};
|
||||
|
||||
onBack = () => {
|
||||
private onBack = () => {
|
||||
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
||||
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
||||
this.setState({editWidget: null});
|
||||
|
@ -769,8 +836,11 @@ class WidgetExplorer extends React.Component {
|
|||
const editWidget = this.state.editWidget;
|
||||
const widgets = WidgetStore.instance.getApps(room.roomId);
|
||||
if (editWidget && widgets.includes(editWidget)) {
|
||||
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values()))
|
||||
.reduce((p, c) => {p.push(...c); return p;}, []);
|
||||
const allState = Array.from(
|
||||
Array.from(room.currentState.events.values()).map((e: Map<string, MatrixEvent>) => {
|
||||
return e.values();
|
||||
}),
|
||||
).reduce((p, c) => { p.push(...c); return p; }, []);
|
||||
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
||||
if (!stateEv) { // "should never happen"
|
||||
return <div>
|
||||
|
@ -811,7 +881,15 @@ class WidgetExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class SettingsExplorer extends React.Component {
|
||||
interface ISettingsExplorerState {
|
||||
query: string;
|
||||
editSetting?: string;
|
||||
viewSetting?: string;
|
||||
explicitValues?: string;
|
||||
explicitRoomValues?: string;
|
||||
}
|
||||
|
||||
class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExplorerState> {
|
||||
static getLabel() {
|
||||
return _t("Settings Explorer");
|
||||
}
|
||||
|
@ -829,19 +907,19 @@ class SettingsExplorer extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onQueryChange = (ev) => {
|
||||
private onQueryChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({query: ev.target.value});
|
||||
};
|
||||
|
||||
onExplValuesEdit = (ev) => {
|
||||
private onExplValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({explicitValues: ev.target.value});
|
||||
};
|
||||
|
||||
onExplRoomValuesEdit = (ev) => {
|
||||
private onExplRoomValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({explicitRoomValues: ev.target.value});
|
||||
};
|
||||
|
||||
onBack = () => {
|
||||
private onBack = () => {
|
||||
if (this.state.editSetting) {
|
||||
this.setState({editSetting: null});
|
||||
} else if (this.state.viewSetting) {
|
||||
|
@ -851,12 +929,12 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onViewClick = (ev, settingId) => {
|
||||
private onViewClick = (ev: MouseEvent, settingId: string) => {
|
||||
ev.preventDefault();
|
||||
this.setState({viewSetting: settingId});
|
||||
};
|
||||
|
||||
onEditClick = (ev, settingId) => {
|
||||
private onEditClick = (ev: MouseEvent, settingId: string) => {
|
||||
ev.preventDefault();
|
||||
this.setState({
|
||||
editSetting: settingId,
|
||||
|
@ -865,7 +943,7 @@ class SettingsExplorer extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onSaveClick = async () => {
|
||||
private onSaveClick = async () => {
|
||||
try {
|
||||
const settingId = this.state.editSetting;
|
||||
const parsedExplicit = JSON.parse(this.state.explicitValues);
|
||||
|
@ -874,7 +952,7 @@ class SettingsExplorer extends React.Component {
|
|||
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
||||
try {
|
||||
const val = parsedExplicit[level];
|
||||
await SettingsStore.setValue(settingId, null, level, val);
|
||||
await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
@ -884,7 +962,7 @@ class SettingsExplorer extends React.Component {
|
|||
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
||||
try {
|
||||
const val = parsedExplicitRoom[level];
|
||||
await SettingsStore.setValue(settingId, roomId, level, val);
|
||||
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
@ -901,7 +979,7 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
renderSettingValue(val) {
|
||||
private renderSettingValue(val: any): string {
|
||||
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
|
||||
const toStringTypes = ['boolean', 'number'];
|
||||
if (toStringTypes.includes(typeof(val))) {
|
||||
|
@ -911,7 +989,7 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
renderExplicitSettingValues(setting, roomId) {
|
||||
private renderExplicitSettingValues(setting: string, roomId: string): string {
|
||||
const vals = {};
|
||||
for (const level of LEVEL_ORDER) {
|
||||
try {
|
||||
|
@ -926,7 +1004,7 @@ class SettingsExplorer extends React.Component {
|
|||
return JSON.stringify(vals, null, 4);
|
||||
}
|
||||
|
||||
renderCanEditLevel(roomId, level) {
|
||||
private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode {
|
||||
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
|
||||
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
|
||||
return <td className={className}><code>{canEdit.toString()}</code></td>;
|
||||
|
@ -1062,27 +1140,37 @@ class SettingsExplorer extends React.Component {
|
|||
|
||||
<div>
|
||||
{_t("Value:")}
|
||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
|
||||
<code>{this.renderSettingValue(
|
||||
SettingsStore.getValue(this.state.viewSetting),
|
||||
)}</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Value in this room:")}
|
||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code>
|
||||
<code>{this.renderSettingValue(
|
||||
SettingsStore.getValue(this.state.viewSetting, room.roomId),
|
||||
)}</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Values at explicit levels:")}
|
||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre>
|
||||
<pre><code>{this.renderExplicitSettingValues(
|
||||
this.state.viewSetting, null,
|
||||
)}</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Values at explicit levels in this room:")}
|
||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre>
|
||||
<pre><code>{this.renderExplicitSettingValues(
|
||||
this.state.viewSetting, room.roomId,
|
||||
)}</code></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button>
|
||||
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{
|
||||
_t("Edit Values")
|
||||
}</button>
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1091,7 +1179,11 @@ class SettingsExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
type DevtoolsDialogEntry = React.JSXElementConstructor<any> & {
|
||||
getLabel: () => string;
|
||||
};
|
||||
|
||||
const Entries: DevtoolsDialogEntry[] = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
SendAccountData,
|
||||
|
@ -1102,43 +1194,36 @@ const Entries = [
|
|||
SettingsExplorer,
|
||||
];
|
||||
|
||||
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onFinished: (finished: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mode?: DevtoolsDialogEntry;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
||||
export default class DevtoolsDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
|
||||
this.state = {
|
||||
mode: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_setMode(mode) {
|
||||
private setMode(mode: DevtoolsDialogEntry) {
|
||||
return () => {
|
||||
this.setState({ mode });
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
if (this.prevMode) {
|
||||
this.setState({ mode: this.prevMode });
|
||||
this.prevMode = null;
|
||||
} else {
|
||||
this.setState({ mode: null });
|
||||
}
|
||||
private onBack = () => {
|
||||
this.setState({ mode: null });
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
|
@ -1165,7 +1250,7 @@ export default class DevtoolsDialog extends React.PureComponent {
|
|||
<div className="mx_Dialog_content">
|
||||
{ Entries.map((Entry) => {
|
||||
const label = Entry.getLabel();
|
||||
const onClick = this._setMode(Entry);
|
||||
const onClick = this.setMode(Entry);
|
||||
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
|
||||
}) }
|
||||
</div>
|
|
@ -26,37 +26,37 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.dialogs.ErrorDialog")
|
||||
export default class ErrorDialog extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
title?: string;
|
||||
description?: React.ReactNode;
|
||||
button?: string;
|
||||
focus?: boolean;
|
||||
headerImage?: string;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
interface IState {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.ErrorDialog")
|
||||
export default class ErrorDialog extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
focus: true,
|
||||
title: null,
|
||||
description: null,
|
||||
button: null,
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
private onClick = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog
|
268
src/components/views/dialogs/ForwardDialog.tsx
Normal file
268
src/components/views/dialogs/ForwardDialog.tsx
Normal file
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||
|
||||
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, { useMemo, useState, useEffect } from "react";
|
||||
import classnames from "classnames";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { useSettingValue, useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { avatarUrlForUser } from "../../../Avatar";
|
||||
import EventTile from "../rooms/EventTile";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { Alignment } from '../elements/Tooltip';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
||||
const AVATAR_SIZE = 30;
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
// The event to forward
|
||||
event: MatrixEvent;
|
||||
// We need a permalink creator for the source room to pass through to EventTile
|
||||
// in case the event is a reply (even though the user can't get at the link)
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
interface IEntryProps {
|
||||
room: Room;
|
||||
event: MatrixEvent;
|
||||
matrixClient: MatrixClient;
|
||||
onFinished(success: boolean): void;
|
||||
}
|
||||
|
||||
enum SendState {
|
||||
CanSend,
|
||||
Sending,
|
||||
Sent,
|
||||
Failed,
|
||||
}
|
||||
|
||||
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
|
||||
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
|
||||
|
||||
const jumpToRoom = () => {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
onFinished(true);
|
||||
};
|
||||
const send = async () => {
|
||||
setSendState(SendState.Sending);
|
||||
try {
|
||||
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
|
||||
setSendState(SendState.Sent);
|
||||
} catch (e) {
|
||||
setSendState(SendState.Failed);
|
||||
}
|
||||
};
|
||||
|
||||
let className;
|
||||
let disabled = false;
|
||||
let title;
|
||||
let icon;
|
||||
if (sendState === SendState.CanSend) {
|
||||
className = "mx_ForwardList_canSend";
|
||||
if (room.maySendMessage()) {
|
||||
title = _t("Send");
|
||||
} else {
|
||||
disabled = true;
|
||||
title = _t("You don't have permission to do this");
|
||||
}
|
||||
} else if (sendState === SendState.Sending) {
|
||||
className = "mx_ForwardList_sending";
|
||||
disabled = true;
|
||||
title = _t("Sending");
|
||||
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||
} else if (sendState === SendState.Sent) {
|
||||
className = "mx_ForwardList_sent";
|
||||
disabled = true;
|
||||
title = _t("Sent");
|
||||
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||
} else {
|
||||
className = "mx_ForwardList_sendFailed";
|
||||
disabled = true;
|
||||
title = _t("Failed to send");
|
||||
icon = <NotificationBadge
|
||||
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className="mx_ForwardList_entry">
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ForwardList_roomButton"
|
||||
onClick={jumpToRoom}
|
||||
title={_t("Open link")}
|
||||
yOffset={-20}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
<DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||
<span className="mx_ForwardList_entry_name">{ room.name }</span>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
|
||||
className={`mx_ForwardList_sendButton ${className}`}
|
||||
onClick={send}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
yOffset={-20}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
|
||||
{ icon }
|
||||
</AccessibleTooltipButton>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
|
||||
const userId = cli.getUserId();
|
||||
const [profileInfo, setProfileInfo] = useState<any>({});
|
||||
useEffect(() => {
|
||||
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
|
||||
}, [cli, userId]);
|
||||
|
||||
// For the message preview we fake the sender as ourselves
|
||||
const mockEvent = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: userId,
|
||||
content: event.getContent(),
|
||||
unsigned: {
|
||||
age: 97,
|
||||
},
|
||||
event_id: "$9999999999999999999999999999999999999999999",
|
||||
room_id: event.getRoomId(),
|
||||
});
|
||||
mockEvent.sender = {
|
||||
name: profileInfo.displayname || userId,
|
||||
rawDisplayName: profileInfo.displayname,
|
||||
userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return avatarUrlForUser(
|
||||
{ avatarUrl: profileInfo.avatar_url },
|
||||
AVATAR_SIZE, AVATAR_SIZE, "crop",
|
||||
);
|
||||
},
|
||||
getMxcAvatarUrl: () => profileInfo.avatar_url,
|
||||
} as RoomMember;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const spacesEnabled = useFeatureEnabled("feature_spaces");
|
||||
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||
const previewLayout = useSettingValue<Layout>("layout");
|
||||
|
||||
let rooms = useMemo(() => sortRooms(
|
||||
cli.getVisibleRooms().filter(
|
||||
room => room.getMyMembership() === "join" &&
|
||||
!(spacesEnabled && room.isSpaceRoom()),
|
||||
),
|
||||
), [cli, spacesEnabled]);
|
||||
|
||||
if (lcQuery) {
|
||||
rooms = new QueryMatcher<Room>(rooms, {
|
||||
keys: ["name"],
|
||||
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||
shouldMatchWordsOnly: false,
|
||||
}).match(lcQuery);
|
||||
}
|
||||
|
||||
const [truncateAt, setTruncateAt] = useState(20);
|
||||
function overflowTile(overflowCount, totalCount) {
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={() => setTruncateAt(totalCount)} />
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Forward message")}
|
||||
className="mx_ForwardDialog"
|
||||
contentId="mx_ForwardList"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<h3>{ _t("Message preview") }</h3>
|
||||
<div className={classnames("mx_ForwardDialog_preview", {
|
||||
"mx_IRCLayout": previewLayout == Layout.IRC,
|
||||
"mx_GroupLayout": previewLayout == Layout.Group,
|
||||
})}>
|
||||
<EventTile
|
||||
mxEvent={mockEvent}
|
||||
layout={previewLayout}
|
||||
enableFlair={flairEnabled}
|
||||
permalinkCreator={permalinkCreator}
|
||||
as="div"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="mx_ForwardList" id="mx_ForwardList">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={_t("Search for rooms or people")}
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_ForwardList_results">
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
event={event}
|
||||
matrixClient={cli}
|
||||
onFinished={onFinished}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
) : <span className="mx_ForwardList_noResults">
|
||||
{ _t("No results") }
|
||||
</span> }
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default ForwardDialog;
|
|
@ -15,5 +15,5 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
export interface IDialogProps {
|
||||
onFinished: (bool) => void;
|
||||
onFinished(...args: any): void;
|
||||
}
|
||||
|
|
|
@ -14,43 +14,67 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
|
||||
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
IInvite3PID,
|
||||
canEncryptToAllUsers,
|
||||
ensureDMExists,
|
||||
findDMForUser,
|
||||
privateShouldBeEncrypted,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import {
|
||||
IInviteResult,
|
||||
inviteMultipleToRoom,
|
||||
showAnyInviteErrors,
|
||||
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 { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {getAddressType} from "../../../UserAddress";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { getAddressType } from "../../../UserAddress";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { copyPlaintext, selectText } from "../../../utils/strings";
|
||||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
interface IRecentUser {
|
||||
userId: string,
|
||||
user: RoomMember,
|
||||
lastActive: number,
|
||||
}
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
@ -58,46 +82,45 @@ export const KIND_CALL_TRANSFER = "call_transfer";
|
|||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
||||
// This is the interface that is expected by various components in this file. It is a bit
|
||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
||||
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// for 3PIDs/email addresses.
|
||||
//
|
||||
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
||||
class Member {
|
||||
export abstract class Member {
|
||||
/**
|
||||
* The display name of this Member. For users this should be their profile's display
|
||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||
*/
|
||||
get name(): string { throw new Error("Member class not implemented"); }
|
||||
public abstract get name(): string;
|
||||
|
||||
/**
|
||||
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||
* be the 3PID address (email).
|
||||
*/
|
||||
get userId(): string { throw new Error("Member class not implemented"); }
|
||||
public abstract get userId(): string;
|
||||
|
||||
/**
|
||||
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||
*/
|
||||
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
||||
public abstract getMxcAvatarUrl(): string;
|
||||
}
|
||||
|
||||
class DirectoryMember extends Member {
|
||||
_userId: string;
|
||||
_displayName: string;
|
||||
_avatarUrl: string;
|
||||
private readonly _userId: string;
|
||||
private readonly displayName: string;
|
||||
private readonly avatarUrl: string;
|
||||
|
||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||
// eslint-disable-next-line camelcase
|
||||
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this._displayName = userDirResult.display_name;
|
||||
this._avatarUrl = userDirResult.avatar_url;
|
||||
this.displayName = userDirResult.display_name;
|
||||
this.avatarUrl = userDirResult.avatar_url;
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._displayName || this._userId;
|
||||
return this.displayName || this._userId;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
|
@ -105,32 +128,32 @@ class DirectoryMember extends Member {
|
|||
}
|
||||
|
||||
getMxcAvatarUrl(): string {
|
||||
return this._avatarUrl;
|
||||
return this.avatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
class ThreepidMember extends Member {
|
||||
_id: string;
|
||||
private readonly id: string;
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this._id = id;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
// This is a getter that would be falsey on all other implementations. Until we have
|
||||
// better type support in the react-sdk we can use this trick to determine the kind
|
||||
// of 3PID we're dealing with, if any.
|
||||
get isEmail(): boolean {
|
||||
return this._id.includes('@');
|
||||
return this.id.includes('@');
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._id;
|
||||
return this.id;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
return this._id;
|
||||
return this.id;
|
||||
}
|
||||
|
||||
getMxcAvatarUrl(): string {
|
||||
|
@ -139,12 +162,12 @@ class ThreepidMember extends Member {
|
|||
}
|
||||
|
||||
interface IDMUserTileProps {
|
||||
member: RoomMember;
|
||||
onRemove: (RoomMember) => any;
|
||||
member: Member;
|
||||
onRemove(member: Member): void;
|
||||
}
|
||||
|
||||
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -153,11 +176,8 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const avatarSize = 20;
|
||||
const avatar = this.props.member.isEmail
|
||||
const avatar = (this.props.member as ThreepidMember).isEmail
|
||||
? <img
|
||||
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
|
@ -177,7 +197,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
closeButton = (
|
||||
<AccessibleButton
|
||||
className='mx_InviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
onClick={this.onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
||||
alt={_t('Remove')} width={8} height={8}
|
||||
|
@ -199,15 +219,15 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
}
|
||||
|
||||
interface IDMRoomTileProps {
|
||||
member: RoomMember;
|
||||
member: Member;
|
||||
lastActiveTs: number;
|
||||
onToggle: (RoomMember) => any;
|
||||
onToggle(member: Member): void;
|
||||
highlightWord: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||
_onClick = (e) => {
|
||||
private onClick = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -215,7 +235,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
this.props.onToggle(this.props.member);
|
||||
};
|
||||
|
||||
_highlightName(str: string) {
|
||||
private highlightName(str: string) {
|
||||
if (!this.props.highlightWord) return str;
|
||||
|
||||
// We convert things to lowercase for index searching, but pull substrings from
|
||||
|
@ -252,8 +272,6 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
let timestamp = null;
|
||||
if (this.props.lastActiveTs) {
|
||||
const humanTs = humanizeTime(this.props.lastActiveTs);
|
||||
|
@ -261,7 +279,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
}
|
||||
|
||||
const avatarSize = 36;
|
||||
const avatar = this.props.member.isEmail
|
||||
const avatar = (this.props.member as ThreepidMember).isEmail
|
||||
? <img
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
|
@ -289,15 +307,15 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
</span>
|
||||
);
|
||||
|
||||
const caption = this.props.member.isEmail
|
||||
const caption = (this.props.member as ThreepidMember).isEmail
|
||||
? _t("Invite by email")
|
||||
: this._highlightName(this.props.member.userId);
|
||||
: this.highlightName(this.props.member.userId);
|
||||
|
||||
return (
|
||||
<div className='mx_InviteDialog_roomTile' onClick={this._onClick}>
|
||||
<div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
|
||||
{stackedAvatar}
|
||||
<span className="mx_InviteDialog_roomTile_nameStack">
|
||||
<div className='mx_InviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</div>
|
||||
<div className='mx_InviteDialog_roomTile_name'>{this.highlightName(this.props.member.name)}</div>
|
||||
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
||||
</span>
|
||||
{timestamp}
|
||||
|
@ -308,7 +326,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
|
||||
interface IInviteDialogProps {
|
||||
// Takes an array of user IDs/emails to invite.
|
||||
onFinished: (toInvite?: string[]) => any;
|
||||
onFinished: (toInvite?: string[]) => void;
|
||||
|
||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||
// not provided.
|
||||
|
@ -325,7 +343,7 @@ interface IInviteDialogProps {
|
|||
}
|
||||
|
||||
interface IInviteDialogState {
|
||||
targets: RoomMember[]; // array of Member objects (see interface above)
|
||||
targets: Member[]; // array of Member objects (see interface above)
|
||||
filterText: string;
|
||||
recents: { user: Member, userId: string }[];
|
||||
numRecentsShown: number;
|
||||
|
@ -349,8 +367,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
initialText: "",
|
||||
};
|
||||
|
||||
_debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||
_editorRef: any = null;
|
||||
private closeCopiedTooltip: () => void;
|
||||
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||
private editorRef = createRef<HTMLInputElement>();
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -378,7 +398,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
filterText: this.props.initialText,
|
||||
recents: InviteDialog.buildRecents(alreadyInvited),
|
||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
suggestions: this._buildSuggestions(alreadyInvited),
|
||||
suggestions: this.buildSuggestions(alreadyInvited),
|
||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||
serverResultsMixin: [],
|
||||
threepidResultsMixin: [],
|
||||
|
@ -390,21 +410,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
busy: false,
|
||||
errorText: null,
|
||||
};
|
||||
|
||||
this._editorRef = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.initialText) {
|
||||
this._updateSuggestions(this.props.initialText);
|
||||
this.updateSuggestions(this.props.initialText);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
|
||||
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
|
||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||
}
|
||||
|
||||
private onConsultFirstChange = (ev) => {
|
||||
this.setState({consultFirst: ev.target.checked});
|
||||
}
|
||||
|
||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
||||
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
|
||||
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
|
||||
|
@ -467,7 +492,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return recents;
|
||||
}
|
||||
|
||||
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
||||
private buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
||||
const maxConsideredMembers = 200;
|
||||
const joinedRooms = MatrixClientPeg.get().getRooms()
|
||||
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
||||
|
@ -574,7 +599,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
members.sort((a, b) => {
|
||||
if (a.score === b.score) {
|
||||
if (a.numRooms === b.numRooms) {
|
||||
return a.member.userId.localeCompare(b.member.userId);
|
||||
return compare(a.member.userId, b.member.userId);
|
||||
}
|
||||
|
||||
return b.numRooms - a.numRooms;
|
||||
|
@ -585,22 +610,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||
}
|
||||
|
||||
_shouldAbortAfterInviteError(result): boolean {
|
||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
||||
if (failedUsers.length > 0) {
|
||||
console.log("Failed to invite users: ", result);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
|
||||
csvUsers: failedUsers.join(", "),
|
||||
}),
|
||||
});
|
||||
return true; // abort
|
||||
}
|
||||
return false;
|
||||
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
|
||||
this.setState({ busy: false });
|
||||
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
|
||||
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
|
||||
}
|
||||
|
||||
_convertFilter(): Member[] {
|
||||
private convertFilter(): Member[] {
|
||||
// Check to see if there's anything to convert first
|
||||
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
||||
|
||||
|
@ -617,10 +633,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return newTargets;
|
||||
}
|
||||
|
||||
_startDm = async () => {
|
||||
private startDm = async () => {
|
||||
this.setState({busy: true});
|
||||
const client = MatrixClientPeg.get();
|
||||
const targets = this._convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
|
@ -694,11 +710,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_inviteUsers = async () => {
|
||||
private inviteUsers = async () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
this.setState({busy: true});
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
this.convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -715,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
try {
|
||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
|
@ -749,9 +765,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_transferCall = async () => {
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
private transferCall = async () => {
|
||||
this.convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
if (targetIds.length > 1) {
|
||||
this.setState({
|
||||
|
@ -790,26 +806,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_onKeyDown = (e) => {
|
||||
private onKeyDown = (e) => {
|
||||
if (this.state.busy) return;
|
||||
const value = e.target.value.trim();
|
||||
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
|
||||
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
|
||||
// when the field is empty and the user hits backspace remove the right-most target
|
||||
e.preventDefault();
|
||||
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||
this.removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
||||
// when the user hits enter with something in their field try to convert it
|
||||
e.preventDefault();
|
||||
this._convertFilter();
|
||||
this.convertFilter();
|
||||
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
|
||||
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
|
||||
e.preventDefault();
|
||||
this._convertFilter();
|
||||
this.convertFilter();
|
||||
}
|
||||
};
|
||||
|
||||
_updateSuggestions = async (term) => {
|
||||
private updateSuggestions = async (term) => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
|
@ -918,30 +934,30 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
private updateFilter = (e) => {
|
||||
const term = e.target.value;
|
||||
this.setState({filterText: term});
|
||||
|
||||
// Debounce server lookups to reduce spam. We don't clear the existing server
|
||||
// results because they might still be vaguely accurate, likewise for races which
|
||||
// could happen here.
|
||||
if (this._debounceTimer) {
|
||||
clearTimeout(this._debounceTimer);
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
this._debounceTimer = setTimeout(() => {
|
||||
this._updateSuggestions(term);
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this.updateSuggestions(term);
|
||||
}, 150); // 150ms debounce (human reaction time + some)
|
||||
};
|
||||
|
||||
_showMoreRecents = () => {
|
||||
private showMoreRecents = () => {
|
||||
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_showMoreSuggestions = () => {
|
||||
private showMoreSuggestions = () => {
|
||||
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_toggleMember = (member: Member) => {
|
||||
private toggleMember = (member: Member) => {
|
||||
if (!this.state.busy) {
|
||||
let filterText = this.state.filterText;
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
|
@ -954,13 +970,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
this.setState({targets, filterText});
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
if (this.editorRef && this.editorRef.current) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_removeMember = (member: Member) => {
|
||||
private removeMember = (member: Member) => {
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) {
|
||||
|
@ -968,12 +984,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.setState({targets});
|
||||
}
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
if (this.editorRef && this.editorRef.current) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onPaste = async (e) => {
|
||||
private onPaste = async (e) => {
|
||||
if (this.state.filterText) {
|
||||
// if the user has already typed something, just let them
|
||||
// paste normally.
|
||||
|
@ -1027,6 +1043,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
failed.push(address);
|
||||
}
|
||||
}
|
||||
if (this.unmounted) return;
|
||||
|
||||
if (failed.length > 0) {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
|
@ -1043,17 +1060,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.setState({targets: [...this.state.targets, ...toAdd]});
|
||||
};
|
||||
|
||||
_onClickInputArea = (e) => {
|
||||
private onClickInputArea = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
if (this.editorRef && this.editorRef.current) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onUseDefaultIdentityServerClick = (e) => {
|
||||
private onUseDefaultIdentityServerClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Update the IS in account data. Actually using it may trigger terms.
|
||||
|
@ -1062,21 +1079,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
||||
};
|
||||
|
||||
_onManageSettingsClick = (e) => {
|
||||
private onManageSettingsClick = (e) => {
|
||||
e.preventDefault();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onCommunityInviteClick = (e) => {
|
||||
private onCommunityInviteClick = (e) => {
|
||||
this.props.onFinished();
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
};
|
||||
|
||||
_renderSection(kind: "recents"|"suggestions") {
|
||||
private 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 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;
|
||||
|
@ -1156,7 +1173,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
member={r.user}
|
||||
lastActiveTs={lastActive(r)}
|
||||
key={r.userId}
|
||||
onToggle={this._toggleMember}
|
||||
onToggle={this.toggleMember}
|
||||
highlightWord={this.state.filterText}
|
||||
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||
/>
|
||||
|
@ -1171,32 +1188,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
);
|
||||
}
|
||||
|
||||
_renderEditor() {
|
||||
private renderEditor() {
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
||||
));
|
||||
const input = (
|
||||
<input
|
||||
type="text"
|
||||
onKeyDown={this._onKeyDown}
|
||||
onChange={this._updateFilter}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.updateFilter}
|
||||
value={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
ref={this.editorRef}
|
||||
onPaste={this.onPaste}
|
||||
autoFocus={true}
|
||||
disabled={this.state.busy}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className='mx_InviteDialog_editor' onClick={this._onClickInputArea}>
|
||||
<div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
|
||||
{targets}
|
||||
{input}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderIdentityServerWarning() {
|
||||
private renderIdentityServerWarning() {
|
||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
||||
!SettingsStore.getValue(UIFeature.IdentityServer)
|
||||
) {
|
||||
|
@ -1214,8 +1231,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
|
@ -1225,13 +1242,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
selectText(e.target);
|
||||
}
|
||||
|
||||
private onCopyClick = async e => {
|
||||
e.preventDefault();
|
||||
const target = e.target; // copy target before we go async and React throws it away
|
||||
|
||||
const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId()));
|
||||
const buttonRect = target.getBoundingClientRect();
|
||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t("Copied!") : _t("Failed to copy"),
|
||||
});
|
||||
// Drop a reference to this close handler for componentWillUnmount
|
||||
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
@ -1242,12 +1278,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
spinner = <Spinner w={20} h={20} />;
|
||||
}
|
||||
|
||||
|
||||
let title;
|
||||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn;
|
||||
let consultSection;
|
||||
let extraSection;
|
||||
let footer;
|
||||
let keySharingWarning = <span />;
|
||||
|
||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||
|
@ -1298,7 +1334,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={this._onCommunityInviteClick}
|
||||
onClick={this.onCommunityInviteClick}
|
||||
>{sub}</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
@ -1309,7 +1345,27 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
</React.Fragment>;
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
goButtonFn = this.startDm;
|
||||
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
||||
<span>{ _t("Some suggestions may be hidden for privacy.") }</span>
|
||||
<p>{ _t("If you can't see who you’re looking for, send them your invite link below.") }</p>
|
||||
</div>;
|
||||
const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
|
||||
footer = <div className="mx_InviteDialog_footer">
|
||||
<h3>{ _t("Or send invite link") }</h3>
|
||||
<div className="mx_InviteDialog_footer_link">
|
||||
<a href={link} onClick={this.onLinkClick}>
|
||||
{ link }
|
||||
</a>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onCopyClick}
|
||||
className="mx_InviteDialog_footer_link_copy"
|
||||
>
|
||||
<div />
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
</div>
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||
|
@ -1348,7 +1404,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
});
|
||||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
goButtonFn = this.inviteUsers;
|
||||
|
||||
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
|
@ -1370,8 +1426,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this._transferCall;
|
||||
consultSection = <div>
|
||||
goButtonFn = this.transferCall;
|
||||
footer = <div>
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||
{_t("Consult first")}
|
||||
|
@ -1385,7 +1441,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_InviteDialog'
|
||||
className={classNames("mx_InviteDialog", {
|
||||
mx_InviteDialog_hasFooter: !!footer,
|
||||
})}
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
|
@ -1393,7 +1451,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
<div className='mx_InviteDialog_content'>
|
||||
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||
<div className='mx_InviteDialog_addressBar'>
|
||||
{this._renderEditor()}
|
||||
{this.renderEditor()}
|
||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
|
@ -1407,13 +1465,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
</div>
|
||||
</div>
|
||||
{keySharingWarning}
|
||||
{this._renderIdentityServerWarning()}
|
||||
{this.renderIdentityServerWarning()}
|
||||
<div className='error'>{this.state.errorText}</div>
|
||||
<div className='mx_InviteDialog_userSections'>
|
||||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
{this.renderSection('recents')}
|
||||
{this.renderSection('suggestions')}
|
||||
{extraSection}
|
||||
</div>
|
||||
{consultSection}
|
||||
{footer}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
|
||||
</ScrollPanel>);
|
||||
}
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import * as React from 'react';
|
||||
import BaseDialog from './BaseDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, getUserLanguage } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {
|
||||
ClientWidgetApi,
|
||||
|
@ -39,6 +39,8 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
|||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {ELEMENT_CLIENT_ID} from "../../../identifiers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
|
@ -61,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
|
||||
state: IState = {
|
||||
disabledButtonIds: [],
|
||||
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
|
||||
.map(b => b.id),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -129,6 +132,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||
userDisplayName: OwnProfileStore.instance.displayName,
|
||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||
clientId: ELEMENT_CLIENT_ID,
|
||||
clientTheme: SettingsStore.getValue("theme"),
|
||||
clientLanguage: getUserLanguage(),
|
||||
});
|
||||
|
||||
const parsed = new URL(templated);
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Markdown from '../../../Markdown';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
/*
|
||||
* A dialog for reporting an event.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ReportEventDialog")
|
||||
export default class ReportEventDialog extends PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
reason: "",
|
||||
busy: false,
|
||||
err: null,
|
||||
};
|
||||
}
|
||||
|
||||
_onReasonChange = ({target: {value: reason}}) => {
|
||||
this.setState({ reason });
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
_onSubmit = async () => {
|
||||
if (!this.state.reason || !this.state.reason.trim()) {
|
||||
this.setState({
|
||||
err: _t("Please fill why you're reporting."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
err: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const ev = this.props.mxEvent;
|
||||
await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
err: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Loader = sdk.getComponent('elements.Spinner');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
</div>;
|
||||
}
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const adminMessageMD =
|
||||
SdkConfig.get().reportEvent &&
|
||||
SdkConfig.get().reportEvent.adminMessageMD;
|
||||
let adminMessage;
|
||||
if (adminMessageMD) {
|
||||
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_BugReportDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Report Content to Your Homeserver Administrator')}
|
||||
contentId='mx_ReportEventDialog'
|
||||
>
|
||||
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
|
||||
<p>
|
||||
{
|
||||
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
|
||||
"your homeserver. If messages in this room are encrypted, your homeserver " +
|
||||
"administrator will not be able to read the message text or view any files or images.")
|
||||
}
|
||||
</p>
|
||||
{adminMessage}
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
rows={5}
|
||||
onChange={this._onReasonChange}
|
||||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
onPrimaryButtonClick={this._onSubmit}
|
||||
focus={true}
|
||||
onCancel={this._onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
445
src/components/views/dialogs/ReportEventDialog.tsx
Normal file
445
src/components/views/dialogs/ReportEventDialog.tsx
Normal file
|
@ -0,0 +1,445 @@
|
|||
/*
|
||||
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 * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Markdown from '../../../Markdown';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// A free-form text describing the abuse.
|
||||
reason: string;
|
||||
busy: boolean;
|
||||
err?: string;
|
||||
// If we know it, the nature of the abuse, as specified by MSC3215.
|
||||
nature?: EXTENDED_NATURE;
|
||||
}
|
||||
|
||||
|
||||
const MODERATED_BY_STATE_EVENT_TYPE = [
|
||||
"org.matrix.msc3215.room.moderation.moderated_by",
|
||||
/**
|
||||
* Unprefixed state event. Not ready for prime time.
|
||||
*
|
||||
* "m.room.moderation.moderated_by"
|
||||
*/
|
||||
];
|
||||
|
||||
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
|
||||
|
||||
// Standard abuse natures.
|
||||
enum NATURE {
|
||||
DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
|
||||
TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
|
||||
ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
|
||||
SPAM = "org.matrix.msc3215.abuse.nature.spam",
|
||||
OTHER = "org.matrix.msc3215.abuse.nature.other",
|
||||
}
|
||||
|
||||
enum NON_STANDARD_NATURE {
|
||||
// Non-standard abuse nature.
|
||||
// It should never leave the client - we use it to fallback to
|
||||
// server-wide abuse reporting.
|
||||
ADMIN = "non-standard.abuse.nature.admin"
|
||||
}
|
||||
|
||||
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
|
||||
|
||||
type Moderation = {
|
||||
// The id of the moderation room.
|
||||
moderationRoomId: string;
|
||||
// The id of the bot in charge of forwarding abuse reports to the moderation room.
|
||||
moderationBotUserId: string;
|
||||
}
|
||||
/*
|
||||
* A dialog for reporting an event.
|
||||
*
|
||||
* The actual content of the dialog will depend on two things:
|
||||
*
|
||||
* 1. Is `feature_report_to_moderators` enabled?
|
||||
* 2. Does the room support moderation as per MSC3215, i.e. is there
|
||||
* a well-formed state event `m.room.moderation.moderated_by`
|
||||
* /`org.matrix.msc3215.room.moderation.moderated_by`?
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.ReportEventDialog")
|
||||
export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||
// If the room supports moderation, the moderation information.
|
||||
private moderation?: Moderation;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
let moderatedByRoomId = null;
|
||||
let moderatedByUserId = null;
|
||||
|
||||
if (SettingsStore.getValue("feature_report_to_moderators")) {
|
||||
// The client supports reporting to moderators.
|
||||
// Does the room support it, too?
|
||||
|
||||
// Extract state events to determine whether we should display
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(props.mxEvent.getRoomId());
|
||||
|
||||
for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
|
||||
const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType);
|
||||
if (!stateEvent) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(stateEvent)) {
|
||||
// Internal error.
|
||||
throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` +
|
||||
"should return at most one state event");
|
||||
}
|
||||
const event = stateEvent.event;
|
||||
if (!("content" in event) || typeof event["content"] != "object") {
|
||||
// The room is improperly configured.
|
||||
// Display this debug message for the sake of moderators.
|
||||
console.debug("Moderation error", "state event", stateEventType,
|
||||
"should have an object field `content`, got", event);
|
||||
continue;
|
||||
}
|
||||
const content = event["content"];
|
||||
if (!("room_id" in content) || typeof content["room_id"] != "string") {
|
||||
// The room is improperly configured.
|
||||
// Display this debug message for the sake of moderators.
|
||||
console.debug("Moderation error", "state event", stateEventType,
|
||||
"should have a string field `content.room_id`, got", event);
|
||||
continue;
|
||||
}
|
||||
if (!("user_id" in content) || typeof content["user_id"] != "string") {
|
||||
// The room is improperly configured.
|
||||
// Display this debug message for the sake of moderators.
|
||||
console.debug("Moderation error", "state event", stateEventType,
|
||||
"should have a string field `content.user_id`, got", event);
|
||||
continue;
|
||||
}
|
||||
moderatedByRoomId = content["room_id"];
|
||||
moderatedByUserId = content["user_id"];
|
||||
}
|
||||
|
||||
if (moderatedByRoomId && moderatedByUserId) {
|
||||
// The room supports moderation.
|
||||
this.moderation = {
|
||||
moderationRoomId: moderatedByRoomId,
|
||||
moderationBotUserId: moderatedByUserId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
// A free-form text describing the abuse.
|
||||
reason: "",
|
||||
busy: false,
|
||||
err: null,
|
||||
// If specified, the nature of the abuse, as specified by MSC3215.
|
||||
nature: null,
|
||||
};
|
||||
}
|
||||
|
||||
// The user has written down a freeform description of the abuse.
|
||||
private onReasonChange = ({target: {value: reason}}): void => {
|
||||
this.setState({ reason });
|
||||
};
|
||||
|
||||
// The user has clicked on a nature.
|
||||
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE});
|
||||
};
|
||||
|
||||
// The user has clicked "cancel".
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
// The user has clicked "submit".
|
||||
private onSubmit = async () => {
|
||||
let reason = this.state.reason || "";
|
||||
reason = reason.trim();
|
||||
if (this.moderation) {
|
||||
// This room supports moderation.
|
||||
// We need a nature.
|
||||
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
|
||||
if (!this.state.nature ||
|
||||
((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
|
||||
&& !reason)
|
||||
) {
|
||||
this.setState({
|
||||
err: _t("Please fill why you're reporting."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// This room does not support moderation.
|
||||
// We need a `reason`.
|
||||
if (!reason) {
|
||||
this.setState({
|
||||
err: _t("Please fill why you're reporting."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
err: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const client = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
|
||||
const nature: NATURE = this.state.nature;
|
||||
|
||||
// Report to moderators through to the dedicated bot,
|
||||
// as configured in the room's state events.
|
||||
const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
|
||||
await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
|
||||
event_id: ev.getId(),
|
||||
room_id: ev.getRoomId(),
|
||||
moderated_by_id: this.moderation.moderationRoomId,
|
||||
nature,
|
||||
reporter: client.getUserId(),
|
||||
comment: this.state.reason.trim(),
|
||||
});
|
||||
} else {
|
||||
// Report to homeserver admin through the dedicated Matrix API.
|
||||
await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
err: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Loader = sdk.getComponent('elements.Spinner');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
</div>;
|
||||
}
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const adminMessageMD =
|
||||
SdkConfig.get().reportEvent &&
|
||||
SdkConfig.get().reportEvent.adminMessageMD;
|
||||
let adminMessage;
|
||||
if (adminMessageMD) {
|
||||
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
if (this.moderation) {
|
||||
// Display report-to-moderator dialog.
|
||||
// We let the user pick a nature.
|
||||
const client = MatrixClientPeg.get();
|
||||
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
|
||||
let subtitle;
|
||||
switch (this.state.nature) {
|
||||
case NATURE.DISAGREEMENT:
|
||||
subtitle = _t("What this user is writing is wrong.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NATURE.TOXIC:
|
||||
subtitle = _t("This user is displaying toxic behaviour, " +
|
||||
"for instance by insulting other users or sharing " +
|
||||
" adult-only content in a family-friendly room " +
|
||||
" or otherwise violating the rules of this room.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NATURE.ILLEGAL:
|
||||
subtitle = _t("This user is displaying illegal behaviour, " +
|
||||
"for instance by doxing people or threatening violence.\n" +
|
||||
"This will be reported to the room moderators who may escalate this to legal authorities.");
|
||||
break;
|
||||
case NATURE.SPAM:
|
||||
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NON_STANDARD_NATURE.ADMIN:
|
||||
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
|
||||
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||
"This will be reported to the administrators of %(homeserver)s. " +
|
||||
"The administrators will NOT be able to read the encrypted content of this room.",
|
||||
{ homeserver: homeServerName });
|
||||
} else {
|
||||
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||
" This will be reported to the administrators of %(homeserver)s.",
|
||||
{ homeserver: homeServerName });
|
||||
}
|
||||
break;
|
||||
case NATURE.OTHER:
|
||||
subtitle = _t("Any other reason. Please describe the problem.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
default:
|
||||
subtitle = _t("Please pick a nature and describe what makes this message abusive.");
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ReportEventDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Report Content')}
|
||||
contentId='mx_ReportEventDialog'
|
||||
>
|
||||
<div>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.DISAGREEMENT }
|
||||
checked = { this.state.nature == NATURE.DISAGREEMENT }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Disagree')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.TOXIC }
|
||||
checked = { this.state.nature == NATURE.TOXIC }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Toxic Behaviour')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.ILLEGAL }
|
||||
checked = { this.state.nature == NATURE.ILLEGAL }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Illegal Content')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.SPAM }
|
||||
checked = { this.state.nature == NATURE.SPAM }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Spam or propaganda')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NON_STANDARD_NATURE.ADMIN }
|
||||
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Report the entire room')}
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.OTHER }
|
||||
checked = { this.state.nature == NATURE.OTHER }
|
||||
onChange = { this.onNatureChosen }
|
||||
>
|
||||
{_t('Other')}
|
||||
</StyledRadioButton>
|
||||
<p>
|
||||
{subtitle}
|
||||
</p>
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
rows={5}
|
||||
onChange={this.onReasonChange}
|
||||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
focus={true}
|
||||
onCancel={this.onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
// Report to homeserver admin.
|
||||
// Currently, the API does not support natures.
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ReportEventDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Report Content to Your Homeserver Administrator')}
|
||||
contentId='mx_ReportEventDialog'
|
||||
>
|
||||
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
|
||||
<p>
|
||||
{
|
||||
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
|
||||
"your homeserver. If messages in this room are encrypted, your homeserver " +
|
||||
"administrator will not be able to read the message text or view any files " +
|
||||
"or images.")
|
||||
}
|
||||
</p>
|
||||
{adminMessage}
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
rows={5}
|
||||
onChange={this.onReasonChange}
|
||||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
focus={true}
|
||||
onCancel={this.onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TabbedView, {Tab} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||
|
@ -39,31 +38,36 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
|||
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
|
||||
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onFinished: (success: boolean) => void;
|
||||
initialTabId?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
|
||||
public componentWillUnmount() {
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
private onAction = (payload): void => {
|
||||
// When view changes below us, close the room settings
|
||||
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
||||
if (payload.action === 'view_home_page') {
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
_getTabs() {
|
||||
const tabs = [];
|
||||
private getTabs(): Tab[] {
|
||||
const tabs: Tab[] = [];
|
||||
|
||||
tabs.push(new Tab(
|
||||
ROOM_GENERAL_TAB,
|
||||
|
@ -104,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
ROOM_ADVANCED_TAB,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
|
||||
<AdvancedRoomSettingsTab
|
||||
roomId={this.props.roomId}
|
||||
closeSettingsFn={() => this.props.onFinished(true)}
|
||||
/>,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -123,7 +130,10 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
title={_t("Room Settings - %(roomName)s", {roomName})}
|
||||
>
|
||||
<div className='mx_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
<TabbedView
|
||||
tabs={this.getTabs()}
|
||||
initialTabId={this.props.initialTabId}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -217,6 +217,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
value={this.state.otherHomeserver}
|
||||
validateOnChange={false}
|
||||
validateOnFocus={false}
|
||||
id="mx_homeserverInput"
|
||||
/>
|
||||
</StyledRadioButton>
|
||||
<p>
|
||||
|
|
|
@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import React, { useMemo } from 'react';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DevtoolsDialog from "./DevtoolsDialog";
|
||||
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
|
||||
import {getTopic} from "../elements/RoomTopic";
|
||||
import {avatarUrlForRoom} from "../../../Avatar";
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import TabbedView, { Tab } from "../../structures/TabbedView";
|
||||
import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
|
||||
import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||
|
||||
export enum SpaceSettingsTab {
|
||||
General = "SPACE_GENERAL_TAB",
|
||||
Visibility = "SPACE_VISIBILITY_TAB",
|
||||
Advanced = "SPACE_ADVANCED_TAB",
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -45,59 +48,30 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
}
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
||||
const avatarChanged = newAvatar !== null;
|
||||
|
||||
const [name, setName] = useState<string>(space.name);
|
||||
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
||||
const nameChanged = name !== space.name;
|
||||
|
||||
const currentTopic = getTopic(space);
|
||||
const [topic, setTopic] = useState<string>(currentTopic);
|
||||
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
||||
const topicChanged = topic !== currentTopic;
|
||||
|
||||
const currentJoinRule = space.getJoinRule();
|
||||
const [joinRule, setJoinRule] = useState(currentJoinRule);
|
||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||
const joinRuleChanged = joinRule !== currentJoinRule;
|
||||
|
||||
const onSave = async () => {
|
||||
setBusy(true);
|
||||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
promises.push(cli.setRoomName(space.roomId, name));
|
||||
}
|
||||
|
||||
if (topicChanged) {
|
||||
promises.push(cli.setRoomTopic(space.roomId, topic));
|
||||
}
|
||||
|
||||
if (joinRuleChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||
}
|
||||
|
||||
const results = await allSettled(promises);
|
||||
setBusy(false);
|
||||
const failures = results.filter(r => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error("Failed to save space settings: ", failures);
|
||||
setError(_t("Failed to save space settings."));
|
||||
}
|
||||
};
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
new Tab(
|
||||
SpaceSettingsTab.General,
|
||||
_td("General"),
|
||||
"mx_SpaceSettingsDialog_generalIcon",
|
||||
<SpaceSettingsGeneralTab matrixClient={cli} space={space} onFinished={onFinished} />,
|
||||
),
|
||||
new Tab(
|
||||
SpaceSettingsTab.Visibility,
|
||||
_td("Visibility"),
|
||||
"mx_SpaceSettingsDialog_visibilityIcon",
|
||||
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} />,
|
||||
),
|
||||
SettingsStore.getValue(UIFeature.AdvancedSettings)
|
||||
? new Tab(
|
||||
SpaceSettingsTab.Advanced,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
|
||||
)
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
}, [cli, space, onFinished]);
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Space settings")}
|
||||
|
@ -106,59 +80,14 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
|
||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceBasicSettings
|
||||
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
||||
avatarDisabled={!canSetAvatar}
|
||||
setAvatar={setNewAvatar}
|
||||
name={name}
|
||||
nameDisabled={!canSetName}
|
||||
setName={setName}
|
||||
topic={topic}
|
||||
topicDisabled={!canSetTopic}
|
||||
setTopic={setTopic}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{ _t("Make this space private") }
|
||||
<ToggleSwitch
|
||||
checked={joinRule !== "public"}
|
||||
onChange={checked => setJoinRule(checked ? "invite" : "public")}
|
||||
disabled={!canSetJoinRule}
|
||||
aria-label={_t("Make this space private")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("Leave Space") }
|
||||
</AccessibleButton>
|
||||
|
||||
<div className="mx_SpaceSettingsDialog_buttons">
|
||||
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
|
||||
{ _t("View dev tools") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
||||
{ busy ? _t("Saving...") : _t("Save Changes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div
|
||||
className="mx_SpaceSettingsDialog_content"
|
||||
id="mx_SpaceSettingsDialog"
|
||||
title={_t("Settings - %(spaceName)s", { spaceName: space.name })}
|
||||
>
|
||||
<TabbedView tabs={tabs} />
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsDialog;
|
||||
|
||||
|
|
|
@ -16,22 +16,21 @@ limitations under the License.
|
|||
|
||||
import url from 'url';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t, pickBestLanguage } from '../../../languageHandler';
|
||||
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
||||
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||
|
||||
class TermsCheckbox extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool.isRequired,
|
||||
}
|
||||
interface ITermsCheckboxProps {
|
||||
onChange: (url: string, checked: boolean) => void;
|
||||
url: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.props.onChange(this.props.url, ev.target.checked);
|
||||
class TermsCheckbox extends React.PureComponent<ITermsCheckboxProps> {
|
||||
private onChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.props.onChange(this.props.url, ev.currentTarget.checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
interface ITermsDialogProps {
|
||||
/**
|
||||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: any[],
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls?: string[],
|
||||
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: (success: boolean, agreedUrls?: string[]) => void,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
agreedUrls: any;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.TermsDialog")
|
||||
export default class TermsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
/**
|
||||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: PropTypes.array.isRequired,
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default class TermsDialog extends React.PureComponent<ITermsDialogProps, IState> {
|
||||
constructor(props) {
|
||||
super();
|
||||
super(props);
|
||||
this.state = {
|
||||
// url -> boolean
|
||||
agreedUrls: {},
|
||||
|
@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
private onCancelClick = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onNextClick = () => {
|
||||
private onNextClick = (): void => {
|
||||
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
|
||||
}
|
||||
|
||||
_nameForServiceType(serviceType, host) {
|
||||
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
|
||||
switch (serviceType) {
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
||||
|
@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_summaryForServiceType(serviceType) {
|
||||
private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element {
|
||||
switch (serviceType) {
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>
|
||||
|
@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onTermsCheckboxChange = (url, checked) => {
|
||||
private onTermsCheckboxChange = (url: string, checked: boolean) => {
|
||||
this.setState({
|
||||
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
|
@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent {
|
|||
let serviceName;
|
||||
let summary;
|
||||
if (i === 0) {
|
||||
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
||||
summary = this._summaryForServiceType(
|
||||
serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
||||
summary = this.summaryForServiceType(
|
||||
policiesAndService.service.serviceType,
|
||||
);
|
||||
}
|
||||
|
@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent {
|
|||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a></td>
|
||||
<td>
|
||||
{termDoc[termsLang].name}
|
||||
<a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a>
|
||||
</td>
|
||||
<td><TermsCheckbox
|
||||
url={termDoc[termsLang].url}
|
||||
onChange={this._onTermsCheckboxChange}
|
||||
onChange={this.onTermsCheckboxChange}
|
||||
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
||||
/></td>
|
||||
</tr>);
|
||||
|
@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent {
|
|||
return (
|
||||
<BaseDialog
|
||||
fixedWidth={false}
|
||||
onFinished={this._onCancelClick}
|
||||
onFinished={this.onCancelClick}
|
||||
title={_t("Terms of Service")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
|
@ -197,8 +203,8 @@ export default class TermsDialog extends React.PureComponent {
|
|||
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancelClick}
|
||||
onPrimaryButtonClick={this._onNextClick}
|
||||
onCancel={this.onCancelClick}
|
||||
onPrimaryButtonClick={this.onNextClick}
|
||||
primaryDisabled={!enableSubmit}
|
||||
/>
|
||||
</BaseDialog>
|
73
src/components/views/dialogs/UntrustedDeviceDialog.tsx
Normal file
73
src/components/views/dialogs/UntrustedDeviceDialog.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2019, 2020, 2021 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 { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import { IDevice } from "../right_panel/UserInfo";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
user: User;
|
||||
device: IDevice;
|
||||
}
|
||||
|
||||
const UntrustedDeviceDialog: React.FC<IProps> = ({device, user, onFinished}) => {
|
||||
let askToVerifyText;
|
||||
let newSessionText;
|
||||
|
||||
if (MatrixClientPeg.get().getUserId() === user.userId) {
|
||||
newSessionText = _t("You signed in to a new session without verifying it:");
|
||||
askToVerifyText = _t("Verify your other session using one of the options below.");
|
||||
} else {
|
||||
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
|
||||
{name: user.displayName, userId: user.userId});
|
||||
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
onFinished={onFinished}
|
||||
className="mx_UntrustedDeviceDialog"
|
||||
title={<>
|
||||
<E2EIcon status="warning" size={24} hideTooltip={true} />
|
||||
{ _t("Not Trusted")}
|
||||
</>}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>{newSessionText}</p>
|
||||
<p>{device.getDisplayName()} ({device.deviceId})</p>
|
||||
<p>{askToVerifyText}</p>
|
||||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>
|
||||
{ _t("Manually Verify by Text") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("sas")}>
|
||||
{ _t("Interactively verify by Emoji") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={() => onFinished(false)}>
|
||||
{ _t("Done") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default UntrustedDeviceDialog;
|
|
@ -16,11 +16,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TabbedView, {Tab} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore";
|
||||
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
|
||||
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
|
||||
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
||||
|
@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
|
|||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
|
||||
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
|
||||
export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
|
||||
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
|
||||
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
|
||||
export const USER_VOICE_TAB = "USER_VOICE_TAB";
|
||||
export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
|
||||
export const USER_LABS_TAB = "USER_LABS_TAB";
|
||||
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
|
||||
export const USER_HELP_TAB = "USER_HELP_TAB";
|
||||
export enum UserTab {
|
||||
General = "USER_GENERAL_TAB",
|
||||
Appearance = "USER_APPEARANCE_TAB",
|
||||
Flair = "USER_FLAIR_TAB",
|
||||
Notifications = "USER_NOTIFICATIONS_TAB",
|
||||
Preferences = "USER_PREFERENCES_TAB",
|
||||
Voice = "USER_VOICE_TAB",
|
||||
Security = "USER_SECURITY_TAB",
|
||||
Labs = "USER_LABS_TAB",
|
||||
Mjolnir = "USER_MJOLNIR_TAB",
|
||||
Help = "USER_HELP_TAB",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
initialTabId?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mjolnirEnabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.UserSettingsDialog")
|
||||
export default class UserSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
initialTabId: PropTypes.string,
|
||||
};
|
||||
export default class UserSettingsDialog extends React.Component<IProps, IState> {
|
||||
private mjolnirWatcher: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
|
||||
public componentDidMount(): void {
|
||||
this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
SettingsStore.unwatchSetting(this._mjolnirWatcher);
|
||||
public componentWillUnmount(): void {
|
||||
SettingsStore.unwatchSetting(this.mjolnirWatcher);
|
||||
}
|
||||
|
||||
_mjolnirChanged(settingName, roomId, atLevel, newValue) {
|
||||
private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
|
||||
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
|
||||
this.setState({mjolnirEnabled: newValue});
|
||||
}
|
||||
|
@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component {
|
|||
const tabs = [];
|
||||
|
||||
tabs.push(new Tab(
|
||||
USER_GENERAL_TAB,
|
||||
UserTab.General,
|
||||
_td("General"),
|
||||
"mx_UserSettingsDialog_settingsIcon",
|
||||
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
USER_APPEARANCE_TAB,
|
||||
UserTab.Appearance,
|
||||
_td("Appearance"),
|
||||
"mx_UserSettingsDialog_appearanceIcon",
|
||||
<AppearanceUserSettingsTab />,
|
||||
));
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
tabs.push(new Tab(
|
||||
USER_FLAIR_TAB,
|
||||
UserTab.Flair,
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairUserSettingsTab />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
USER_NOTIFICATIONS_TAB,
|
||||
UserTab.Notifications,
|
||||
_td("Notifications"),
|
||||
"mx_UserSettingsDialog_bellIcon",
|
||||
<NotificationUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
USER_PREFERENCES_TAB,
|
||||
UserTab.Preferences,
|
||||
_td("Preferences"),
|
||||
"mx_UserSettingsDialog_preferencesIcon",
|
||||
<PreferencesUserSettingsTab />,
|
||||
|
@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component {
|
|||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
tabs.push(new Tab(
|
||||
USER_VOICE_TAB,
|
||||
UserTab.Voice,
|
||||
_td("Voice & Video"),
|
||||
"mx_UserSettingsDialog_voiceIcon",
|
||||
<VoiceUserSettingsTab />,
|
||||
|
@ -120,14 +127,17 @@ export default class UserSettingsDialog extends React.Component {
|
|||
}
|
||||
|
||||
tabs.push(new Tab(
|
||||
USER_SECURITY_TAB,
|
||||
UserTab.Security,
|
||||
_td("Security & Privacy"),
|
||||
"mx_UserSettingsDialog_securityIcon",
|
||||
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
if (SdkConfig.get()['showLabsSettings']) {
|
||||
// Show the Labs tab if enabled or if there are any active betas
|
||||
if (SdkConfig.get()['showLabsSettings']
|
||||
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
|
||||
) {
|
||||
tabs.push(new Tab(
|
||||
USER_LABS_TAB,
|
||||
UserTab.Labs,
|
||||
_td("Labs"),
|
||||
"mx_UserSettingsDialog_labsIcon",
|
||||
<LabsUserSettingsTab />,
|
||||
|
@ -135,17 +145,17 @@ export default class UserSettingsDialog extends React.Component {
|
|||
}
|
||||
if (this.state.mjolnirEnabled) {
|
||||
tabs.push(new Tab(
|
||||
USER_MJOLNIR_TAB,
|
||||
UserTab.Mjolnir,
|
||||
_td("Ignored users"),
|
||||
"mx_UserSettingsDialog_mjolnirIcon",
|
||||
<MjolnirUserSettingsTab />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
USER_HELP_TAB,
|
||||
UserTab.Help,
|
||||
_td("Help & About"),
|
||||
"mx_UserSettingsDialog_helpIcon",
|
||||
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
<HelpUserSettingsTab closeSettingsFn={() => this.props.onFinished(true)} />,
|
||||
));
|
||||
|
||||
return tabs;
|
|
@ -345,6 +345,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
|
||||
<input
|
||||
type="password"
|
||||
id="mx_passPhraseInput"
|
||||
className="mx_AccessSecretStorageDialog_passPhraseInput"
|
||||
onChange={this.onPassPhraseChange}
|
||||
value={this.state.passPhrase}
|
||||
|
|
|
@ -15,22 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import * as sdk from "../../../../index";
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
|
||||
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onConfirm = () => {
|
||||
export default class ConfirmDestroyCrossSigningDialog extends React.Component<IProps> {
|
||||
private onConfirm = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onDecline = () => {
|
||||
private onDecline = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
|
@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Clear cross-signing keys")}
|
||||
onPrimaryButtonClick={this._onConfirm}
|
||||
onPrimaryButtonClick={this.onConfirm}
|
||||
primaryButtonClass="danger"
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onDecline}
|
||||
onCancel={this.onDecline}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -16,7 +16,6 @@ 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';
|
||||
|
@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons';
|
|||
import BaseDialog from '../BaseDialog';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
accountPassword?: string;
|
||||
tokenLogin?: boolean;
|
||||
onFinished?: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error: Error | null;
|
||||
canUploadKeysWithPasswordOnly?: boolean;
|
||||
accountPassword: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating a cross-signing keys. In most
|
||||
|
@ -33,39 +44,32 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
|||
* may need to complete some steps to proceed.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
|
||||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
export default class CreateCrossSigningDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
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();
|
||||
canUploadKeysWithPasswordOnly: props.accountPassword ? true : null,
|
||||
accountPassword: props.accountPassword || "",
|
||||
};
|
||||
|
||||
if (!this.state.accountPassword) {
|
||||
this.queryKeyUploadAuth();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._bootstrapCrossSigning();
|
||||
public componentDidMount(): void {
|
||||
this.bootstrapCrossSigning();
|
||||
}
|
||||
|
||||
async _queryKeyUploadAuth() {
|
||||
private async queryKeyUploadAuth(): Promise<void> {
|
||||
try {
|
||||
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
||||
// We should never get here: the server should always require
|
||||
|
@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_doBootstrapUIAuth = async (makeRequest) => {
|
||||
private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise<void> => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
type: 'm.login.password',
|
||||
|
@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_bootstrapCrossSigning = async () => {
|
||||
private bootstrapCrossSigning = async (): Promise<void> => {
|
||||
this.setState({
|
||||
error: null,
|
||||
});
|
||||
|
@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
|
||||
try {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
if (this.props.tokenLogin) {
|
||||
// ignore any failures, we are relying on grace period here
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
|
@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
<p>{_t("Unable to set up keys")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapCrossSigning}
|
||||
onCancel={this._onCancel}
|
||||
onPrimaryButtonClick={this.bootstrapCrossSigning}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
|
@ -15,47 +15,52 @@ 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 { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
|
||||
function iconFromPhase(phase) {
|
||||
if (phase === PHASE_DONE) {
|
||||
function iconFromPhase(phase: Phase) {
|
||||
if (phase === Phase.Done) {
|
||||
return require("../../../../../res/img/e2e/verified.svg");
|
||||
} else {
|
||||
return require("../../../../../res/img/e2e/warning.svg");
|
||||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
|
||||
export default class SetupEncryptionDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IState {
|
||||
icon: Phase;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
|
||||
export default class SetupEncryptionDialog extends React.Component<IProps, IState> {
|
||||
private store: SetupEncryptionStore;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.store = SetupEncryptionStore.sharedInstance();
|
||||
this.state = {icon: iconFromPhase(this.store.phase)};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.store.on("update", this._onStoreUpdate);
|
||||
public componentDidMount() {
|
||||
this.store.on("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.store.removeListener("update", this._onStoreUpdate);
|
||||
public componentWillUnmount() {
|
||||
this.store.removeListener("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
_onStoreUpdate = () => {
|
||||
private onStoreUpdate = (): void => {
|
||||
this.setState({icon: iconFromPhase(this.store.phase)});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
return <BaseDialog
|
||||
headerImage={this.state.icon}
|
||||
onFinished={this.props.onFinished}
|
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016, 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.
|
||||
|
@ -16,39 +15,56 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
|
||||
import {
|
||||
ChevronFace,
|
||||
ContextMenu,
|
||||
useContextMenu,
|
||||
ContextMenuButton,
|
||||
MenuItemRadio,
|
||||
MenuItem,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
MenuItemRadio,
|
||||
useContextMenu,
|
||||
} from "../../structures/ContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {useSettingValue} from "../../../hooks/useSettings";
|
||||
import * as sdk from "../../../index";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import Modal from "../../../Modal";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import withValidation from "../elements/Validation";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import TextInputDialog from "../dialogs/TextInputDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { compare } from "../../../utils/strings";
|
||||
|
||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||
|
||||
const SETTING_NAME = "room_directory_servers";
|
||||
|
||||
const inPlaceOf = (elementRect) => ({
|
||||
right: window.innerWidth - elementRect.right,
|
||||
const inPlaceOf = (elementRect: Pick<DOMRect, "right" | "top">) => ({
|
||||
right: UIStore.instance.windowWidth - elementRect.right,
|
||||
top: elementRect.top,
|
||||
chevronOffset: 0,
|
||||
chevronFace: "none",
|
||||
chevronFace: ChevronFace.None,
|
||||
});
|
||||
|
||||
const validServer = withValidation({
|
||||
const validServer = withValidation<undefined, { error?: MatrixError }>({
|
||||
deriveData: async ({ value }) => {
|
||||
try {
|
||||
// check if we can successfully load this server's room directory
|
||||
await MatrixClientPeg.get().publicRooms({
|
||||
limit: 1,
|
||||
server: value,
|
||||
});
|
||||
return {};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -57,34 +73,58 @@ const validServer = withValidation({
|
|||
}, {
|
||||
key: "available",
|
||||
final: true,
|
||||
test: async ({ value }) => {
|
||||
try {
|
||||
const opts = {
|
||||
limit: 1,
|
||||
server: value,
|
||||
};
|
||||
// check if we can successfully load this server's room directory
|
||||
await MatrixClientPeg.get().publicRooms(opts);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
test: async (_, { error }) => !error,
|
||||
valid: () => _t("Looks good"),
|
||||
invalid: () => _t("Can't find this server or its room list"),
|
||||
invalid: ({ error }) => error.errcode === "M_FORBIDDEN"
|
||||
? _t("You are not allowed to view this server's rooms list")
|
||||
: _t("Can't find this server or its room list"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IFieldType {
|
||||
regexp: string;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export interface IInstance {
|
||||
desc: string;
|
||||
icon?: string;
|
||||
fields: object;
|
||||
network_id: string;
|
||||
// XXX: this is undocumented but we rely on it.
|
||||
// we inject a fake entry with a symbolic instance_id.
|
||||
instance_id: string | symbol;
|
||||
}
|
||||
|
||||
export interface IProtocol {
|
||||
user_fields: string[];
|
||||
location_fields: string[];
|
||||
icon: string;
|
||||
field_types: Record<string, IFieldType>;
|
||||
instances: IInstance[];
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export type Protocols = Record<string, IProtocol>;
|
||||
|
||||
interface IProps {
|
||||
protocols: Protocols;
|
||||
selectedServerName: string;
|
||||
selectedInstanceId: string | symbol;
|
||||
onOptionChange(server: string, instanceId?: string | symbol): void;
|
||||
}
|
||||
|
||||
// This dropdown sources homeservers from three places:
|
||||
// + your currently connected homeserver
|
||||
// + homeservers in config.json["roomDirectory"]
|
||||
// + homeservers in SettingsStore["room_directory_servers"]
|
||||
// if a server exists in multiple, only keep the top-most entry.
|
||||
|
||||
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
const _userDefinedServers = useSettingValue(SETTING_NAME);
|
||||
const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, selectedInstanceId }: IProps) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
const _userDefinedServers: string[] = useSettingValue(SETTING_NAME);
|
||||
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
||||
|
||||
const handlerFactory = (server, instanceId) => {
|
||||
|
@ -96,7 +136,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
|
||||
const setUserDefinedServers = servers => {
|
||||
_setUserDefinedServers(servers);
|
||||
SettingsStore.setValue(SETTING_NAME, null, "account", servers);
|
||||
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, servers);
|
||||
};
|
||||
// keep local echo up to date with external changes
|
||||
useEffect(() => {
|
||||
|
@ -110,7 +150,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
const roomDirectory = config.roomDirectory || {};
|
||||
|
||||
const hsName = MatrixClientPeg.getHomeserverName();
|
||||
const configServers = new Set(roomDirectory.servers);
|
||||
const configServers = new Set<string>(roomDirectory.servers);
|
||||
|
||||
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
||||
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
||||
|
@ -134,15 +174,21 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
// add a fake protocol with the ALL_ROOMS symbol
|
||||
protocolsList.push({
|
||||
instances: [{
|
||||
fields: [],
|
||||
network_id: "",
|
||||
instance_id: ALL_ROOMS,
|
||||
desc: _t("All rooms"),
|
||||
}],
|
||||
location_fields: [],
|
||||
user_fields: [],
|
||||
field_types: {},
|
||||
icon: "",
|
||||
});
|
||||
}
|
||||
|
||||
protocolsList.forEach(({instances=[]}) => {
|
||||
[...instances].sort((b, a) => {
|
||||
return a.desc.localeCompare(b.desc);
|
||||
return compare(a.desc, b.desc);
|
||||
}).forEach(({desc, instance_id: instanceId}) => {
|
||||
entries.push(
|
||||
<MenuItemRadio
|
||||
|
@ -170,7 +216,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
if (removableServers.has(server)) {
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
||||
title: _t("Are you sure?"),
|
||||
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
||||
|
@ -189,7 +234,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
setUserDefinedServers(servers.filter(s => s !== server));
|
||||
|
||||
// the selected server is being removed, reset to our HS
|
||||
if (serverSelected === server) {
|
||||
if (serverSelected) {
|
||||
onOptionChange(hsName, undefined);
|
||||
}
|
||||
};
|
||||
|
@ -221,7 +266,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
||||
title: _t("Add a new server"),
|
||||
description: _t("Enter the name of a new server you want to explore."),
|
||||
|
@ -282,9 +326,4 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
|||
</div>;
|
||||
};
|
||||
|
||||
NetworkDropdown.propTypes = {
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
protocols: PropTypes.object,
|
||||
};
|
||||
|
||||
export default NetworkDropdown;
|
|
@ -62,6 +62,8 @@ export default function AccessibleButton({
|
|||
disabled,
|
||||
inputRef,
|
||||
className,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
...restProps
|
||||
}: IProps) {
|
||||
const newProps: IAccessibleButtonProps = restProps;
|
||||
|
@ -83,6 +85,8 @@ export default function AccessibleButton({
|
|||
if (e.key === Key.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
};
|
||||
newProps.onKeyUp = (e) => {
|
||||
|
@ -94,6 +98,8 @@ export default function AccessibleButton({
|
|||
if (e.key === Key.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onKeyUp?.(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip from './Tooltip';
|
||||
import Tooltip, {Alignment} from './Tooltip';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
|
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
|||
tooltipClassName?: string;
|
||||
forceHide?: boolean;
|
||||
yOffset?: number;
|
||||
alignment?: Alignment;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -66,14 +67,15 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
|
||||
render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
|
||||
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||
label={tooltip || title}
|
||||
yOffset={yOffset}
|
||||
/> : <div />;
|
||||
alignment={alignment}
|
||||
/> : null;
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
|
|
|
@ -32,6 +32,7 @@ export default class ActionButton extends React.Component {
|
|||
label: PropTypes.string.isRequired,
|
||||
iconPath: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -79,7 +80,8 @@ export default class ActionButton extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className={classNames.join(" ")}
|
||||
<AccessibleButton
|
||||
className={classNames.join(" ")}
|
||||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
|
@ -87,6 +89,7 @@ export default class ActionButton extends React.Component {
|
|||
>
|
||||
{ icon }
|
||||
{ tooltip }
|
||||
{ this.props.children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { UserAddressType } from '../../../UserAddress.js';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.elements.AddressTile")
|
||||
export default class AddressTile extends React.Component {
|
||||
|
|
|
@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = getPersistKey(this.props.app.id);
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
} catch (e) {
|
||||
console.log("Failed to construct widget", e);
|
||||
this._sgWidget = null;
|
||||
}
|
||||
this.iframe = null; // ref to the iframe (callback style)
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
|
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
|
|||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
this._sgWidget.stop();
|
||||
if (this._sgWidget) this._sgWidget.stop();
|
||||
}
|
||||
|
||||
this.setState({ hasPermissionToLoad });
|
||||
|
@ -117,7 +122,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.state.hasPermissionToLoad) {
|
||||
if (this._sgWidget && this.state.hasPermissionToLoad) {
|
||||
this._startWidget();
|
||||
}
|
||||
|
||||
|
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
|
|||
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();
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
} catch (e) {
|
||||
console.log("Failed to construct widget", e);
|
||||
this._sgWidget = null;
|
||||
}
|
||||
}
|
||||
|
||||
_startWidget() {
|
||||
|
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
|
|||
_iframeRefChange = (ref) => {
|
||||
this.iframe = ref;
|
||||
if (ref) {
|
||||
this._sgWidget.start(ref);
|
||||
if (this._sgWidget) this._sgWidget.start(ref);
|
||||
} else {
|
||||
this._resetWidget(this.props);
|
||||
}
|
||||
|
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
|
|||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
|
||||
this._sgWidget.stop({forceDestroy: true});
|
||||
if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
|
||||
}
|
||||
|
||||
_onWidgetPrepared = () => {
|
||||
|
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
|
|||
<Spinner message={_t("Loading...")} />
|
||||
</div>
|
||||
);
|
||||
if (!this.state.hasPermissionToLoad) {
|
||||
if (this._sgWidget === null) {
|
||||
appTileBody = (
|
||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||
<AppWarning errorMsg={_t("Error loading Widget")} />
|
||||
</div>
|
||||
);
|
||||
} else if (!this.state.hasPermissionToLoad) {
|
||||
// only possible for room widgets, can assert this.props.room here
|
||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
appTileBody = (
|
||||
|
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
|
|||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
<AppWarning errorMsg={_t("Error - Mixed content")} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
|
|||
onFinished={this._closeContextMenu}
|
||||
showUnpin={!this.props.userWidget}
|
||||
userWidget={this.props.userWidget}
|
||||
onEditClick={this.props.onEditClick}
|
||||
onDeleteClick={this.props.onDeleteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
import TagTile from './TagTile';
|
||||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
|
@ -31,32 +30,17 @@ export default function DNDTagTile(props) {
|
|||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
|
||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} />
|
||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} index={props.index} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
return <div>
|
||||
<Draggable
|
||||
key={props.tag}
|
||||
draggableId={props.tag}
|
||||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile
|
||||
{...props}
|
||||
contextMenuButtonRef={handle}
|
||||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
return <>
|
||||
<TagTile
|
||||
{...props}
|
||||
contextMenuButtonRef={handle}
|
||||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
{contextMenu}
|
||||
</div>;
|
||||
</>;
|
||||
}
|
||||
|
|
|
@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import EventIndexPeg from "../../../indexing/EventIndexPeg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import React from "react";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||
|
||||
|
||||
export enum WarningKind {
|
||||
Files,
|
||||
|
@ -33,6 +37,22 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
|
|||
if (!isRoomEncrypted) return null;
|
||||
if (EventIndexPeg.get()) return null;
|
||||
|
||||
if (EventIndexPeg.error) {
|
||||
return <>
|
||||
{_t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
|
||||
a: sub => (<a onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}}>
|
||||
{sub}
|
||||
</a>),
|
||||
})}
|
||||
</>;
|
||||
}
|
||||
|
||||
const {desktopBuilds, brand} = SdkConfig.get();
|
||||
|
||||
let text = null;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017, 2019 New Vector Ltd.
|
||||
Copyright 2017-2021 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,48 +14,48 @@ 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 React from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "./Field";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export class EditableItem extends React.Component {
|
||||
static propTypes = {
|
||||
index: PropTypes.number,
|
||||
value: PropTypes.string,
|
||||
onRemove: PropTypes.func,
|
||||
interface IItemProps {
|
||||
index?: number;
|
||||
value?: string;
|
||||
onRemove?(index: number): void;
|
||||
}
|
||||
|
||||
interface IItemState {
|
||||
verifyRemove: boolean;
|
||||
}
|
||||
|
||||
export class EditableItem extends React.Component<IItemProps, IItemState> {
|
||||
public state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: true});
|
||||
this.setState({ verifyRemove: true });
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
private onDontRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: false});
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
private onActuallyRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||
this.setState({verifyRemove: false});
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
|
|||
{_t("Are you sure?")}
|
||||
</span>
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
onClick={this.onActuallyRemove}
|
||||
kind="primary_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
{_t("Yes")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
onClick={this.onDontRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
|
@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
id: string;
|
||||
items: string[];
|
||||
itemsLabel?: string;
|
||||
noItemsLabel?: string;
|
||||
placeholder?: string;
|
||||
newItem?: string;
|
||||
canEdit?: boolean;
|
||||
canRemove?: boolean;
|
||||
suggestionsListId?: string;
|
||||
onItemAdded?(item: string): void;
|
||||
onItemRemoved?(index: number): void;
|
||||
onNewItemChanged?(item: string): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.EditableItemList")
|
||||
export default class EditableItemList extends React.Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
itemsLabel: PropTypes.string,
|
||||
noItemsLabel: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
newItem: PropTypes.string,
|
||||
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
canRemove: PropTypes.bool,
|
||||
};
|
||||
|
||||
_onItemAdded = (e) => {
|
||||
export default class EditableItemList<P = {}> extends React.PureComponent<IProps & P> {
|
||||
protected onItemAdded = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
};
|
||||
|
||||
_onItemRemoved = (index) => {
|
||||
protected onItemRemoved = (index) => {
|
||||
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
|
||||
};
|
||||
|
||||
_onNewItemChanged = (e) => {
|
||||
protected onNewItemChanged = (e) => {
|
||||
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
protected renderNewItemField() {
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._onItemAdded}
|
||||
onSubmit={this.onItemAdded}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EditableItemList_newItem"
|
||||
>
|
||||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
||||
{_t("Add")}
|
||||
<Field
|
||||
label={this.props.placeholder}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={this.props.newItem || ""}
|
||||
onChange={this.onNewItemChanged}
|
||||
list={this.props.suggestionsListId}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onItemAdded}
|
||||
kind="primary"
|
||||
type="submit"
|
||||
disabled={!this.props.newItem}
|
||||
>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
|
@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
|
|||
key={item}
|
||||
index={index}
|
||||
value={item}
|
||||
onRemove={this._onItemRemoved}
|
||||
onRemove={this.onItemRemoved}
|
||||
/>;
|
||||
});
|
||||
|
||||
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
|
||||
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
|
||||
|
||||
return (<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
return (
|
||||
<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
</div>
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this.renderNewItemField() : <div /> }
|
||||
</div>
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
|
||||
</div>);
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,8 @@
|
|||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
||||
import {CHAT_EFFECTS} from '../../../effects'
|
||||
import { CHAT_EFFECTS } from '../../../effects'
|
||||
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||
|
||||
interface IProps {
|
||||
roomWidth: number;
|
||||
|
@ -37,7 +38,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
|||
effect = new Effect(options);
|
||||
effectsRef.current[name] = effect;
|
||||
} catch (err) {
|
||||
console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err);
|
||||
console.warn(`Unable to load effect module at '../../../effects/${name}.`, err);
|
||||
}
|
||||
}
|
||||
return effect;
|
||||
|
@ -45,8 +46,8 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
|||
|
||||
useEffect(() => {
|
||||
const resize = () => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.height = window.innerHeight;
|
||||
if (canvasRef.current && canvasRef.current?.height !== UIStore.instance.windowHeight) {
|
||||
canvasRef.current.height = UIStore.instance.windowHeight;
|
||||
}
|
||||
};
|
||||
const onAction = (payload: { action: string }) => {
|
||||
|
@ -58,12 +59,12 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
|||
}
|
||||
const dispatcherRef = dis.register(onAction);
|
||||
const canvas = canvasRef.current;
|
||||
canvas.height = window.innerHeight;
|
||||
window.addEventListener('resize', resize, true);
|
||||
canvas.height = UIStore.instance.windowHeight;
|
||||
UIStore.instance.on(UI_EVENTS.Resize, resize);
|
||||
|
||||
return () => {
|
||||
dis.unregister(dispatcherRef);
|
||||
window.removeEventListener('resize', resize);
|
||||
UIStore.instance.off(UI_EVENTS.Resize, resize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
||||
for (const effect in currentEffects) {
|
||||
|
|
|
@ -63,9 +63,9 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
// If we are only given few events then just pass them through
|
||||
if (events.length < threshold) {
|
||||
return (
|
||||
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
{ children }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
|
@ -101,7 +102,8 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
name: this.props.displayName,
|
||||
name: this.props.displayName || this.props.userId,
|
||||
rawDisplayName: this.props.displayName,
|
||||
userId: this.props.userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return Avatar.avatarUrlForUser(
|
||||
|
@ -110,7 +112,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
);
|
||||
},
|
||||
getMxcAvatarUrl: () => this.props.avatarUrl,
|
||||
};
|
||||
} as RoomMember;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
@ -128,6 +130,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
mxEvent={event}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
as="div"
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@ function getId() {
|
|||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
export interface IValidateOpts {
|
||||
focused?: boolean;
|
||||
allowEmpty?: boolean;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id?: string;
|
||||
|
@ -180,7 +185,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
|
||||
public async validate({ focused, allowEmpty = true }: IValidateOpts) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ export default class Flair extends React.Component {
|
|||
|
||||
render() {
|
||||
if (this.state.profiles.length === 0) {
|
||||
return <span className="mx_Flair" />;
|
||||
return null;
|
||||
}
|
||||
const avatars = this.state.profiles.map((profile, index) => {
|
||||
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
export default function FormButton(props) {
|
||||
const {className, label, kind, ...restProps} = props;
|
||||
const newClassName = (className || "") + " mx_FormButton";
|
||||
const allProps = Object.assign({}, restProps,
|
||||
{className: newClassName, kind: kind || "primary", children: [label]});
|
||||
return React.createElement(AccessibleButton, allProps);
|
||||
}
|
||||
|
||||
FormButton.propTypes = AccessibleButton.propTypes;
|
|
@ -19,20 +19,20 @@ limitations under the License.
|
|||
import React, { createRef } from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
|
||||
import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu';
|
||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {formatFullDate} from "../../../DateUtils";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {normalizeWheelEvent} from "../../../utils/Mouse";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
|
@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
private initX = 0;
|
||||
private initY = 0;
|
||||
private lastX = 0;
|
||||
private lastY = 0;
|
||||
private previousX = 0;
|
||||
private previousY = 0;
|
||||
|
||||
|
@ -105,23 +103,35 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// needs to be passive in order to work with Chromium
|
||||
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||
// We want to recalculate zoom whenever the window's size changes
|
||||
window.addEventListener("resize", this.calculateZoom);
|
||||
window.addEventListener("resize", this.recalculateZoom);
|
||||
// After the image loads for the first time we want to calculate the zoom
|
||||
this.image.current.addEventListener("load", this.calculateZoom);
|
||||
this.image.current.addEventListener("load", this.recalculateZoom);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||
window.removeEventListener("resize", this.calculateZoom);
|
||||
this.image.current.removeEventListener("load", this.calculateZoom);
|
||||
window.removeEventListener("resize", this.recalculateZoom);
|
||||
this.image.current.removeEventListener("load", this.recalculateZoom);
|
||||
}
|
||||
|
||||
private calculateZoom = () => {
|
||||
private recalculateZoom = () => {
|
||||
this.setZoomAndRotation();
|
||||
}
|
||||
|
||||
private setZoomAndRotation = (inputRotation?: number) => {
|
||||
const image = this.image.current;
|
||||
const imageWrapper = this.imageWrapper.current;
|
||||
|
||||
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
||||
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
||||
const rotation = inputRotation || this.state.rotation;
|
||||
|
||||
const imageIsNotFlipped = rotation % 180 === 0;
|
||||
|
||||
// If the image is rotated take it into account
|
||||
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
|
||||
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
|
||||
|
||||
const zoomX = imageWrapper.clientWidth / width;
|
||||
const zoomY = imageWrapper.clientHeight / height;
|
||||
|
||||
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||
// display it in its original size
|
||||
|
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
zoom: 1,
|
||||
minZoom: 1,
|
||||
maxZoom: 1,
|
||||
rotation: rotation,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// image by default
|
||||
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
||||
|
||||
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
|
||||
// If zoom is smaller than minZoom don't go below that value
|
||||
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
|
||||
|
||||
this.setState({
|
||||
minZoom: minZoom,
|
||||
maxZoom: 1,
|
||||
rotation: rotation,
|
||||
zoom: zoom,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -157,7 +172,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
if (newZoom >= this.state.maxZoom) {
|
||||
this.setState({zoom: this.state.maxZoom});
|
||||
this.setState({ zoom: this.state.maxZoom });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -170,7 +185,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const {deltaY} = normalizeWheelEvent(ev);
|
||||
const { deltaY } = normalizeWheelEvent(ev);
|
||||
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
|
||||
};
|
||||
|
||||
|
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
private onRotateCounterClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur - 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
this.setZoomAndRotation(cur - 90);
|
||||
};
|
||||
|
||||
private onRotateClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur + 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
this.setZoomAndRotation(cur + 90);
|
||||
};
|
||||
|
||||
private onDownloadClick = () => {
|
||||
|
@ -207,6 +220,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
a.href = this.props.src;
|
||||
a.download = this.props.name;
|
||||
a.target = "_blank";
|
||||
a.rel = "noreferrer noopener";
|
||||
a.click();
|
||||
};
|
||||
|
||||
|
@ -245,15 +259,15 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
// Zoom in if we are completely zoomed out
|
||||
if (this.state.zoom === this.state.minZoom) {
|
||||
this.setState({zoom: this.state.maxZoom});
|
||||
this.setState({ zoom: this.state.maxZoom });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({moving: true});
|
||||
this.setState({ moving: true });
|
||||
this.previousX = this.state.translationX;
|
||||
this.previousY = this.state.translationY;
|
||||
this.initX = ev.pageX - this.lastX;
|
||||
this.initY = ev.pageY - this.lastY;
|
||||
this.initX = ev.pageX - this.state.translationX;
|
||||
this.initY = ev.pageY - this.state.translationY;
|
||||
};
|
||||
|
||||
private onMoving = (ev: React.MouseEvent) => {
|
||||
|
@ -262,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
if (!this.state.moving) return;
|
||||
|
||||
this.lastX = ev.pageX - this.initX;
|
||||
this.lastY = ev.pageY - this.initY;
|
||||
this.setState({
|
||||
translationX: this.lastX,
|
||||
translationY: this.lastY,
|
||||
translationX: ev.pageX - this.initX,
|
||||
translationY: ev.pageY - this.initY,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -282,8 +294,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
translationX: 0,
|
||||
translationY: 0,
|
||||
});
|
||||
this.initX = 0;
|
||||
this.initY = 0;
|
||||
}
|
||||
this.setState({moving: false});
|
||||
this.setState({ moving: false });
|
||||
};
|
||||
|
||||
private renderContextMenu() {
|
||||
|
@ -354,7 +368,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const sender = (
|
||||
<div className="mx_ImageView_info_sender">
|
||||
{senderName}
|
||||
{ senderName }
|
||||
</div>
|
||||
);
|
||||
const messageTimestamp = (
|
||||
|
@ -381,10 +395,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
info = (
|
||||
<div className="mx_ImageView_info_wrapper">
|
||||
{avatar}
|
||||
{ avatar }
|
||||
<div className="mx_ImageView_info">
|
||||
{sender}
|
||||
{messageTimestamp}
|
||||
{ sender }
|
||||
{ messageTimestamp }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -424,7 +438,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||
title={_t("Zoom in")}
|
||||
onClick={ this.onZoomInClick }>
|
||||
onClick={this.onZoomInClick}>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
@ -440,37 +454,42 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
ref={this.focusLock}
|
||||
>
|
||||
<div className="mx_ImageView_panel">
|
||||
{info}
|
||||
{ info }
|
||||
<div className="mx_ImageView_toolbar">
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
title={_t("Rotate Left")}
|
||||
onClick={ this.onRotateCounterClockwiseClick }>
|
||||
</AccessibleTooltipButton>
|
||||
{zoomOutButton}
|
||||
{zoomInButton}
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
{ zoomOutButton }
|
||||
{ zoomInButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("Download")}
|
||||
onClick={ this.onDownloadClick }>
|
||||
</AccessibleTooltipButton>
|
||||
{contextMenuButton}
|
||||
{ contextMenuButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
title={_t("Close")}
|
||||
onClick={ this.props.onFinished }>
|
||||
</AccessibleTooltipButton>
|
||||
{this.renderContextMenu()}
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="mx_ImageView_image_wrapper"
|
||||
ref={this.imageWrapper}>
|
||||
ref={this.imageWrapper}
|
||||
onMouseDown={this.props.onFinished}
|
||||
onMouseMove={this.onMoving}
|
||||
onMouseUp={this.onEndMoving}
|
||||
onMouseLeave={this.onEndMoving}
|
||||
>
|
||||
<img
|
||||
src={this.props.src}
|
||||
title={this.props.name}
|
||||
|
@ -479,9 +498,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
className="mx_ImageView_image"
|
||||
draggable={true}
|
||||
onMouseDown={this.onStartMoving}
|
||||
onMouseMove={this.onMoving}
|
||||
onMouseUp={this.onEndMoving}
|
||||
onMouseLeave={this.onEndMoving}
|
||||
/>
|
||||
</div>
|
||||
</FocusLock>
|
||||
|
|
|
@ -16,32 +16,31 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
w?: number;
|
||||
h?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.InlineSpinner")
|
||||
export default class InlineSpinner extends React.Component {
|
||||
export default class InlineSpinner extends React.PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
w: 16,
|
||||
h: 16,
|
||||
}
|
||||
|
||||
render() {
|
||||
const w = this.props.w || 16;
|
||||
const h = this.props.h || 16;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
|
||||
let imageSource;
|
||||
if (SettingsStore.getValue('feature_new_spinner')) {
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_InlineSpinner">
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClass}
|
||||
<div
|
||||
className="mx_InlineSpinner_icon mx_Spinner_icon"
|
||||
style={{width: this.props.w, height: this.props.h}}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 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,38 +14,33 @@ 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 from "react";
|
||||
|
||||
import ToggleSwitch from "./ToggleSwitch";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// The value for the toggle switch
|
||||
value: boolean;
|
||||
// The translated label for the switch
|
||||
label: string;
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled?: boolean;
|
||||
// True to put the toggle in front of the label
|
||||
// Default false.
|
||||
toggleInFront?: boolean;
|
||||
// Additional class names to append to the switch. Optional.
|
||||
className?: string;
|
||||
// The function to call when the value changes
|
||||
onChange(checked: boolean): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.LabelledToggleSwitch")
|
||||
export default class LabelledToggleSwitch extends React.Component {
|
||||
static propTypes = {
|
||||
// The value for the toggle switch
|
||||
value: PropTypes.bool.isRequired,
|
||||
|
||||
// The function to call when the value changes
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
// The translated label for the switch
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
// True to put the toggle in front of the label
|
||||
// Default false.
|
||||
toggleInFront: PropTypes.bool,
|
||||
|
||||
// Additional class names to append to the switch. Optional.
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
// This is a minimal version of a SettingsFlag
|
||||
|
||||
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
|
||||
let firstPart = <span className="mx_SettingsFlag_label">{ this.props.label }</span>;
|
||||
let secondPart = <ToggleSwitch
|
||||
checked={this.props.value}
|
||||
disabled={this.props.disabled}
|
|
@ -58,13 +58,8 @@ export default class LanguageDropdown extends React.Component {
|
|||
// If no value is given, we start with the first
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
if (language) {
|
||||
this.props.onOptionChange(language);
|
||||
} else {
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
this.props.onOptionChange(language);
|
||||
}
|
||||
const language = languageHandler.getUserLanguage();
|
||||
this.props.onOptionChange(language);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler';
|
|||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import EventListSummary from "./EventListSummary";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// An array of member events to summarise
|
||||
|
@ -303,7 +303,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
return res;
|
||||
}
|
||||
|
||||
private static getTransitionSequence(events: MatrixEvent[]) {
|
||||
private static getTransitionSequence(events: IUserEvents[]) {
|
||||
return events.map(MemberEventListSummary.getTransition);
|
||||
}
|
||||
|
||||
|
@ -315,7 +315,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
* @returns {string?} the transition type given to this event. This defaults to `null`
|
||||
* if a transition is not recognised.
|
||||
*/
|
||||
private static getTransition(e: MatrixEvent): TransitionType {
|
||||
private static getTransition(e: IUserEvents): 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)) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {EventType} from 'matrix-js-sdk/src/@types/event';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Spinner from "./Spinner";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useTimeout} from "../../../hooks/useTimeout";
|
||||
import Analytics from "../../../Analytics";
|
||||
|
@ -88,6 +89,12 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
|||
>
|
||||
{ children }
|
||||
|
||||
<div className="mx_MiniAvatarUploader_indicator">
|
||||
{ busy ?
|
||||
<Spinner w={20} h={20} /> :
|
||||
<div className="mx_MiniAvatarUploader_cameraIcon"></div> }
|
||||
</div>
|
||||
|
||||
<div className={classNames("mx_Tooltip", {
|
||||
"mx_Tooltip_visible": visible,
|
||||
"mx_Tooltip_invisible": !visible,
|
||||
|
|
|
@ -46,6 +46,8 @@ export default class ReplyThread extends React.Component {
|
|||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
// Specifies which layout to use.
|
||||
layout: LayoutPropType,
|
||||
// Whether to always show a timestamp
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -212,9 +214,9 @@ export default class ReplyThread extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
||||
return null;
|
||||
}
|
||||
return <ReplyThread
|
||||
parentEv={parentEv}
|
||||
|
@ -222,6 +224,7 @@ export default class ReplyThread extends React.Component {
|
|||
ref={ref}
|
||||
permalinkCreator={permalinkCreator}
|
||||
layout={layout}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -269,40 +272,32 @@ export default class ReplyThread extends React.Component {
|
|||
const {parentEv} = this.props;
|
||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
||||
|
||||
if (this.unmounted) return;
|
||||
|
||||
if (ev) {
|
||||
const loadedEv = await this.getNextEvent(ev);
|
||||
this.setState({
|
||||
events: [ev],
|
||||
}, this.loadNextEvent);
|
||||
loadedEv,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({err: true});
|
||||
}
|
||||
}
|
||||
|
||||
async loadNextEvent() {
|
||||
if (this.unmounted) return;
|
||||
const ev = this.state.events[0];
|
||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||
|
||||
if (!inReplyToEventId) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const loadedEv = await this.getEvent(inReplyToEventId);
|
||||
if (this.unmounted) return;
|
||||
|
||||
if (loadedEv) {
|
||||
this.setState({loadedEv});
|
||||
} else {
|
||||
this.setState({err: true});
|
||||
async getNextEvent(ev) {
|
||||
try {
|
||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||
return await this.getEvent(inReplyToEventId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getEvent(eventId) {
|
||||
if (!eventId) return null;
|
||||
const event = this.room.findEventById(eventId);
|
||||
if (event) return event;
|
||||
|
||||
|
@ -326,13 +321,18 @@ export default class ReplyThread extends React.Component {
|
|||
this.initialize();
|
||||
}
|
||||
|
||||
onQuoteClick() {
|
||||
async onQuoteClick() {
|
||||
const events = [this.state.loadedEv, ...this.state.events];
|
||||
|
||||
let loadedEv = null;
|
||||
if (events.length > 0) {
|
||||
loadedEv = await this.getNextEvent(events[0]);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadedEv: null,
|
||||
loadedEv,
|
||||
events,
|
||||
}, this.loadNextEvent);
|
||||
});
|
||||
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
@ -390,8 +390,10 @@ export default class ReplyThread extends React.Component {
|
|||
isRedacted={ev.isRedacted()}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
replacingEventId={ev.replacingEventId()}
|
||||
as="div"
|
||||
/>
|
||||
</blockquote>;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 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.
|
||||
|
@ -13,67 +13,78 @@ 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 { _t } from '../../../languageHandler';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import withValidation from './Validation';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field, { IValidateOpts } from "./Field";
|
||||
|
||||
interface IProps {
|
||||
domain: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
onChange?(value: string): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
||||
@replaceableComponent("views.elements.RoomAliasField")
|
||||
export default class RoomAliasField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class RoomAliasField extends React.PureComponent<IProps, IState> {
|
||||
private fieldRef = createRef<Field>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isValid: true};
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
_asFullAlias(localpart) {
|
||||
private asFullAlias(localpart: string): string {
|
||||
return `#${localpart}:${this.props.domain}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
const poundSign = (<span>#</span>);
|
||||
const aliasPostfix = ":" + this.props.domain;
|
||||
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
label={_t("Room address")}
|
||||
label={this.props.label || _t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefixComponent={poundSign}
|
||||
postfixComponent={domain}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
onChange={this._onChange}
|
||||
ref={this.fieldRef}
|
||||
onValidate={this.onValidate}
|
||||
placeholder={this.props.placeholder || _t("e.g. my-room")}
|
||||
onChange={this.onChange}
|
||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onChange = (ev) => {
|
||||
private onChange = (ev) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this._asFullAlias(ev.target.value));
|
||||
this.props.onChange(this.asFullAlias(ev.target.value));
|
||||
}
|
||||
};
|
||||
|
||||
_onValidate = async (fieldState) => {
|
||||
const result = await this._validationRules(fieldState);
|
||||
private onValidate = async (fieldState) => {
|
||||
const result = await this.validationRules(fieldState);
|
||||
this.setState({isValid: result.valid});
|
||||
return result;
|
||||
};
|
||||
|
||||
_validationRules = withValidation({
|
||||
private validationRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "safeLocalpart",
|
||||
|
@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
const fullAlias = this._asFullAlias(value);
|
||||
const fullAlias = this.asFullAlias(value);
|
||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
|
||||
encodeURI(fullAlias) === fullAlias;
|
||||
|
@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}, {
|
||||
key: "required",
|
||||
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Please provide a room address"),
|
||||
invalid: () => _t("Please provide an address"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
|
@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}
|
||||
const client = MatrixClientPeg.get();
|
||||
try {
|
||||
await client.getRoomIdForAlias(this._asFullAlias(value));
|
||||
await client.getRoomIdForAlias(this.asFullAlias(value));
|
||||
// we got a room id, so the alias is taken
|
||||
return false;
|
||||
} catch (err) {
|
||||
|
@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
],
|
||||
});
|
||||
|
||||
get isValid() {
|
||||
public get isValid() {
|
||||
return this.state.isValid;
|
||||
}
|
||||
|
||||
validate(options) {
|
||||
return this._fieldRef.validate(options);
|
||||
public validate(options: IValidateOpts) {
|
||||
return this.fieldRef.current?.validate(options);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._fieldRef.focus();
|
||||
public focus() {
|
||||
this.fieldRef.current?.focus();
|
||||
}
|
||||
}
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => {
|
|||
}, [room]);
|
||||
|
||||
if (children) return children(name);
|
||||
return name || "";
|
||||
return <>{ name || "" }</>;
|
||||
};
|
||||
|
||||
export default RoomName;
|
||||
|
|
|
@ -112,7 +112,7 @@ interface IProps {
|
|||
const MAX_PER_ROW = 6;
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
|
||||
const providers = flow.identity_providers || [];
|
||||
if (providers.length < 2) {
|
||||
return <div className="mx_SSOButtons">
|
||||
<SSOButton
|
||||
|
|
|
@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
public render() {
|
||||
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
|
||||
|
||||
let label = this.props.label;
|
||||
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
else label = _t(label);
|
||||
const label = this.props.label
|
||||
? _t(this.props.label)
|
||||
: SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
const description = SettingsStore.getDescription(this.props.name);
|
||||
|
||||
if (this.props.useCheckbox) {
|
||||
return <StyledCheckbox
|
||||
|
@ -99,6 +100,9 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
/>
|
||||
{ description && <div className="mx_SettingsFlag_microcopy">
|
||||
{ description }
|
||||
</div> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,33 +18,21 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
|
||||
let imageSource;
|
||||
if (SettingsStore.getValue('feature_new_spinner')) {
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
const Spinner = ({w = 32, h = 32, message}) => (
|
||||
<div className="mx_Spinner">
|
||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div> </React.Fragment> }
|
||||
<div
|
||||
className="mx_Spinner_icon"
|
||||
style={{width: w, height: h}}
|
||||
aria-label={_t("Loading...")}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message}</div> </React.Fragment> }
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClassName}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Spinner.propTypes = {
|
||||
w: PropTypes.number,
|
||||
h: PropTypes.number,
|
||||
imgClassName: PropTypes.string,
|
||||
message: PropTypes.node,
|
||||
};
|
||||
|
||||
|
|
|
@ -34,10 +34,19 @@ interface IProps<T extends string> {
|
|||
definitions: IDefinition<T>[];
|
||||
value?: T; // if not provided no options will be selected
|
||||
outlined?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange(newValue: T): void;
|
||||
}
|
||||
|
||||
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
|
||||
function StyledRadioGroup<T extends string>({
|
||||
name,
|
||||
definitions,
|
||||
value,
|
||||
className,
|
||||
outlined,
|
||||
disabled,
|
||||
onChange,
|
||||
}: IProps<T>) {
|
||||
const _onChange = e => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
@ -50,12 +59,12 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
|
|||
checked={d.checked !== undefined ? d.checked : d.value === value}
|
||||
name={name}
|
||||
value={d.value}
|
||||
disabled={d.disabled}
|
||||
disabled={disabled || d.disabled}
|
||||
outlined={outlined}
|
||||
>
|
||||
{d.label}
|
||||
{ d.label }
|
||||
</StyledRadioButton>
|
||||
{d.description}
|
||||
{ d.description ? <span>{ d.description }</span> : null }
|
||||
</React.Fragment>)}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import React, {Component, CSSProperties} from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
||||
|
@ -69,7 +70,10 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this.renderTooltip, true);
|
||||
window.addEventListener('scroll', this.renderTooltip, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
|
||||
|
@ -84,7 +88,9 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
public componentWillUnmount() {
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this.renderTooltip, true);
|
||||
window.removeEventListener('scroll', this.renderTooltip, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
private updatePosition(style: CSSProperties) {
|
||||
|
@ -97,15 +103,15 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
// we need so that we're still centered.
|
||||
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
||||
}
|
||||
|
||||
const width = UIStore.instance.windowWidth;
|
||||
const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset;
|
||||
const top = baseTop + offset;
|
||||
const right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
|
||||
const right = width - parentBox.right - window.pageXOffset - 16;
|
||||
const left = parentBox.right + window.pageXOffset + 6;
|
||||
const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2);
|
||||
switch (this.props.alignment) {
|
||||
case Alignment.Natural:
|
||||
if (parentBox.right > window.innerWidth / 2) {
|
||||
if (parentBox.right > width / 2) {
|
||||
style.right = right;
|
||||
style.top = top;
|
||||
break;
|
||||
|
|
|
@ -19,19 +19,30 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.elements.TooltipButton")
|
||||
export default class TooltipButton extends React.Component {
|
||||
state = {
|
||||
hover: false,
|
||||
};
|
||||
interface IProps {
|
||||
helpText: string;
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.TooltipButton")
|
||||
export default class TooltipButton extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onMouseOver = () => {
|
||||
this.setState({
|
||||
hover: true,
|
||||
});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
private onMouseLeave = () => {
|
||||
this.setState({
|
||||
hover: false,
|
||||
});
|
|
@ -16,31 +16,29 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.elements.TruncatedList")
|
||||
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
|
||||
className: PropTypes.string,
|
||||
// A function that returns the children to be rendered into the element.
|
||||
// function getChildren(start: number, end: number): Array<React.Node>
|
||||
// The start element is included, the end is not (as in `slice`).
|
||||
// If omitted, the React child elements will be used. This parameter can be used
|
||||
// to avoid creating unnecessary React elements.
|
||||
getChildren: PropTypes.func,
|
||||
// A function that should return the total number of child element available.
|
||||
// Required if getChildren is supplied.
|
||||
getChildCount: PropTypes.func,
|
||||
// A function which will be invoked when an overflow element is required.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement: PropTypes.func,
|
||||
};
|
||||
interface IProps {
|
||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||
truncateAt?: number;
|
||||
// The className to apply to the wrapping div
|
||||
className?: string;
|
||||
// A function that returns the children to be rendered into the element.
|
||||
// The start element is included, the end is not (as in `slice`).
|
||||
// If omitted, the React child elements will be used. This parameter can be used
|
||||
// to avoid creating unnecessary React elements.
|
||||
getChildren?: (start: number, end: number) => Array<React.ReactNode>;
|
||||
// A function that should return the total number of child element available.
|
||||
// Required if getChildren is supplied.
|
||||
getChildCount?: () => number;
|
||||
// A function which will be invoked when an overflow element is required.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.TruncatedList")
|
||||
export default class TruncatedList extends React.Component<IProps> {
|
||||
static defaultProps ={
|
||||
truncateAt: 2,
|
||||
createOverflowElement(overflowCount, totalCount) {
|
||||
|
@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
|
|||
},
|
||||
};
|
||||
|
||||
_getChildren(start, end) {
|
||||
private getChildren(start: number, end: number): Array<React.ReactNode> {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildren(start, end);
|
||||
} else {
|
||||
|
@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getChildCount() {
|
||||
private getChildCount(): number {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildCount();
|
||||
} else {
|
||||
|
@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
let overflowNode = null;
|
||||
|
||||
const totalChildren = this._getChildCount();
|
||||
const totalChildren = this.getChildCount();
|
||||
let upperBound = totalChildren;
|
||||
if (this.props.truncateAt >= 0) {
|
||||
const overflowCount = totalChildren - this.props.truncateAt;
|
||||
|
@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
|
|||
upperBound = this.props.truncateAt;
|
||||
}
|
||||
}
|
||||
const childNodes = this._getChildren(0, upperBound);
|
||||
const childNodes = this.getChildren(0, upperBound);
|
||||
|
||||
return (
|
||||
<div className={this.props.className}>
|
|
@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component {
|
|||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false}
|
||||
avatarHeight={40} draggable={false}
|
||||
/>
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||
<ToggleSwitch checked={this.state.isGroupPublicised}
|
||||
disabled={!this.state.ready || this.state.busy}
|
||||
onChange={this._onPublicityToggle} />
|
||||
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
function nop() {}
|
||||
import { _t } from "../../../languageHandler";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
|
||||
@replaceableComponent("views.groups.GroupTile")
|
||||
class GroupTile extends React.Component {
|
||||
|
@ -34,7 +34,6 @@ class GroupTile extends React.Component {
|
|||
showDescription: PropTypes.bool,
|
||||
// Height of the group avatar in pixels
|
||||
avatarHeight: PropTypes.number,
|
||||
draggable: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -42,7 +41,6 @@ class GroupTile extends React.Component {
|
|||
static defaultProps = {
|
||||
showDescription: true,
|
||||
avatarHeight: 50,
|
||||
draggable: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -57,7 +55,7 @@ class GroupTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onMouseDown = e => {
|
||||
onClick = e => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
|
@ -65,6 +63,18 @@ class GroupTile extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onPinClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
|
||||
};
|
||||
|
||||
onUnpinClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
@ -78,7 +88,7 @@ class GroupTile extends React.Component {
|
|||
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
|
||||
: null;
|
||||
|
||||
let avatarElement = (
|
||||
const avatarElement = (
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
|
@ -88,46 +98,21 @@ class GroupTile extends React.Component {
|
|||
height={avatarHeight} />
|
||||
</div>
|
||||
);
|
||||
if (this.props.draggable) {
|
||||
const avatarClone = avatarElement;
|
||||
avatarElement = (
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
<Draggable
|
||||
key={"GroupTile " + this.props.groupId}
|
||||
draggableId={"GroupTile " + this.props.groupId}
|
||||
index={this.props.groupId}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{avatarClone}
|
||||
</div>
|
||||
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
||||
{ provided.placeholder ? avatarClone : <div /> }
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
);
|
||||
}
|
||||
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||
{ avatarElement }
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
|
||||
{ !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
|
||||
? <AccessibleButton kind="link" onClick={this.onPinClick}>
|
||||
{ _t("Pin") }
|
||||
</AccessibleButton>
|
||||
: <AccessibleButton kind="link" onClick={this.onUnpinClick}>
|
||||
{ _t("Unpin") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {forwardRef, ReactNode} from "react";
|
||||
import React, {forwardRef, ReactNode, ReactChildren} from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
className: string;
|
||||
title: string;
|
||||
subtitle?: ReactNode;
|
||||
children?: ReactChildren;
|
||||
}
|
||||
|
||||
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {
|
||||
|
|
|
@ -15,41 +15,40 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {getNameForEventRoom, userLabelForEventRoom}
|
||||
import { getNameForEventRoom, userLabelForEventRoom }
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationRequest")
|
||||
export default class MKeyVerificationRequest extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
export default class MKeyVerificationRequest extends React.Component<IProps> {
|
||||
public componentDidMount() {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.on("change", this._onRequestChanged);
|
||||
request.on("change", this.onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount() {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.off("change", this._onRequestChanged);
|
||||
request.off("change", this.onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
_openRequest = () => {
|
||||
const {verificationRequest} = this.props.mxEvent;
|
||||
private openRequest = () => {
|
||||
const { verificationRequest } = this.props.mxEvent;
|
||||
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
|
@ -58,15 +57,15 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onRequestChanged = () => {
|
||||
private onRequestChanged = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_onAcceptClicked = async () => {
|
||||
private onAcceptClicked = async () => {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
this._openRequest();
|
||||
this.openRequest();
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
|
@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onRejectClicked = async () => {
|
||||
private onRejectClicked = async () => {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
|
@ -85,7 +84,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_acceptedLabel(userId) {
|
||||
private acceptedLabel(userId: string) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const myUserId = client.getUserId();
|
||||
if (userId === myUserId) {
|
||||
|
@ -95,7 +94,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_cancelledLabel(userId) {
|
||||
private cancelledLabel(userId: string) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const myUserId = client.getUserId();
|
||||
const {cancellationCode} = this.props.mxEvent.verificationRequest;
|
||||
|
@ -115,9 +114,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
|
||||
const {mxEvent} = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
|
@ -134,11 +132,11 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
let stateLabel;
|
||||
const accepted = request.ready || request.started || request.done;
|
||||
if (accepted) {
|
||||
stateLabel = (<AccessibleButton onClick={this._openRequest}>
|
||||
{this._acceptedLabel(request.receivingUserId)}
|
||||
stateLabel = (<AccessibleButton onClick={this.openRequest}>
|
||||
{this.acceptedLabel(request.receivingUserId)}
|
||||
</AccessibleButton>);
|
||||
} else if (request.cancelled) {
|
||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||
stateLabel = this.cancelledLabel(request.cancellingUserId);
|
||||
} else if (request.accepting) {
|
||||
stateLabel = _t("Accepting …");
|
||||
} else if (request.declining) {
|
||||
|
@ -153,8 +151,12 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||
if (request.canAccept) {
|
||||
stateNode = (<div className="mx_cryptoEvent_buttons">
|
||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
|
||||
<AccessibleButton kind="danger" onClick={this.onRejectClicked}>
|
||||
{_t("Decline")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onAcceptClicked}>
|
||||
{_t("Accept")}
|
||||
</AccessibleButton>
|
||||
</div>);
|
||||
}
|
||||
} else { // request sent by us
|
||||
|
@ -174,8 +176,3 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MKeyVerificationRequest.propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
|
@ -24,6 +24,7 @@ import {_t} from "../../../languageHandler";
|
|||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
import {decryptFile} from "../../../utils/DecryptFile";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -45,7 +46,7 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
|
|||
|
||||
public async componentDidMount() {
|
||||
let buffer: ArrayBuffer;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const content: IMediaEventContent = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
try {
|
||||
|
@ -71,10 +72,14 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
|
|||
|
||||
// We should have a buffer to work with now: let's set it up
|
||||
const playback = new Playback(buffer, waveform);
|
||||
this.setState({playback});
|
||||
this.setState({ playback });
|
||||
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.state.playback?.destroy();
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.error) {
|
||||
// TODO: @@TR: Verify error state
|
||||
|
|
|
@ -28,7 +28,9 @@ interface IProps {
|
|||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||
export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
|
||||
public render() {
|
||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
|
||||
// MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
|
||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
|
||||
|| !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
|
||||
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
||||
if (isVoiceMessage && voiceMessagesEnabled) {
|
||||
return <MVoiceMessageBody {...this.props} />;
|
||||
|
|
|
@ -31,6 +31,7 @@ import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessi
|
|||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {canCancel} from "../context_menus/MessageContextMenu";
|
||||
import Resend from "../../../Resend";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -47,15 +48,14 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
|
|||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<MessageContextMenu
|
||||
mxEvent={mxEvent}
|
||||
permalinkCreator={permalinkCreator}
|
||||
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
|
||||
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
|
||||
onFinished={closeMenu}
|
||||
/>
|
||||
</ContextMenu>;
|
||||
contextMenu = <MessageContextMenu
|
||||
{...aboveLeftOf(buttonRect)}
|
||||
mxEvent={mxEvent}
|
||||
permalinkCreator={permalinkCreator}
|
||||
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
|
||||
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
|
||||
onFinished={closeMenu}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
|
@ -122,6 +122,10 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||
this.props.mxEvent.on("Event.status", this.onSent);
|
||||
}
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.decryptEventIfNeeded(this.props.mxEvent);
|
||||
|
||||
if (this.props.mxEvent.isBeingDecrypted()) {
|
||||
this.props.mxEvent.once("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
|
|
@ -16,20 +16,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
ts: number;
|
||||
showTwelveHour?: boolean;
|
||||
showFullDate?: boolean;
|
||||
showSeconds?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MessageTimestamp")
|
||||
export default class MessageTimestamp extends React.Component {
|
||||
static propTypes = {
|
||||
ts: PropTypes.number.isRequired,
|
||||
showTwelveHour: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
showSeconds: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
export default class MessageTimestamp extends React.Component<IProps> {
|
||||
public render() {
|
||||
const date = new Date(this.props.ts);
|
||||
let timestamp;
|
||||
if (this.props.showFullDate) {
|
||||
|
@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
|
||||
<span
|
||||
className="mx_MessageTimestamp"
|
||||
title={formatFullDate(date, this.props.showTwelveHour)}
|
||||
aria-hidden={true}
|
||||
>
|
||||
{timestamp}
|
||||
</span>
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 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,35 +14,72 @@ 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 from "react";
|
||||
import classNames from "classnames";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu";
|
||||
import ReactionPicker from "../emojipicker/ReactionPicker";
|
||||
import ReactionsRowButton from "./ReactionsRowButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// The maximum number of reactions to initially show on a message.
|
||||
const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRow")
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: PropTypes.object,
|
||||
const ReactButton = ({ mxEvent, reactions }: IProps) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
return <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className={classNames("mx_ReactionsRow_addReactionButton", {
|
||||
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
|
||||
})}
|
||||
title={_t("Add reaction")}
|
||||
onClick={openMenu}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
openMenu();
|
||||
}}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions?: Relations;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
myReactions: MatrixEvent[];
|
||||
showAll: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRow")
|
||||
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
myReactions: this.getMyReactions(),
|
||||
|
@ -50,7 +87,33 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidMount() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
if (mxEvent.isBeingDecrypted() || mxEvent.shouldAttemptDecryption()) {
|
||||
mxEvent.once("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
||||
if (reactions) {
|
||||
reactions.on("Relations.add", this.onReactionsChange);
|
||||
reactions.on("Relations.remove", this.onReactionsChange);
|
||||
reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
mxEvent.off("Event.decrypted", this.onDecrypted);
|
||||
|
||||
if (reactions) {
|
||||
reactions.off("Relations.add", this.onReactionsChange);
|
||||
reactions.off("Relations.remove", this.onReactionsChange);
|
||||
reactions.off("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
|
@ -59,24 +122,12 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
private onDecrypted = () => {
|
||||
// Decryption changes whether the event is actionable
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
private onReactionsChange = () => {
|
||||
// TODO: Call `onHeightChanged` as needed
|
||||
this.setState({
|
||||
myReactions: this.getMyReactions(),
|
||||
|
@ -87,12 +138,12 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
this.forceUpdate();
|
||||
}
|
||||
|
||||
getMyReactions() {
|
||||
private getMyReactions() {
|
||||
const reactions = this.props.reactions;
|
||||
if (!reactions) {
|
||||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const userId = this.context.getUserId();
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
|
@ -100,7 +151,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onShowAllClick = () => {
|
||||
private onShowAllClick = () => {
|
||||
this.setState({
|
||||
showAll: true,
|
||||
});
|
||||
|
@ -114,7 +165,6 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
const count = events.size;
|
||||
if (!count) {
|
||||
|
@ -136,6 +186,8 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
/>;
|
||||
}).filter(item => !!item);
|
||||
|
||||
if (!items.length) return null;
|
||||
|
||||
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
|
||||
// The "+ 1" ensure that the "show all" reveals something that takes up
|
||||
// more space than the button itself.
|
||||
|
@ -151,13 +203,22 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
</a>;
|
||||
}
|
||||
|
||||
const cli = this.context;
|
||||
|
||||
let addReactionButton;
|
||||
const room = cli.getRoom(mxEvent.getRoomId());
|
||||
if (room.getMyMembership() === "join" && room.currentState.maySendEvent(EventType.Reaction, cli.getUserId())) {
|
||||
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
|
||||
}
|
||||
|
||||
return <div
|
||||
className="mx_ReactionsRow"
|
||||
role="toolbar"
|
||||
aria-label={_t("Reactions")}
|
||||
>
|
||||
{items}
|
||||
{showAllButton}
|
||||
{ items }
|
||||
{ showAllButton }
|
||||
{ addReactionButton }
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 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,49 +14,54 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The reaction content / key / emoji
|
||||
content: string;
|
||||
// The count of votes for this key
|
||||
count: number;
|
||||
// A Set of Matrix reaction events for this key
|
||||
reactionEvents: Set<MatrixEvent>;
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent?: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
tooltipRendered: boolean;
|
||||
tooltipVisible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButton")
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
// The count of votes for this key
|
||||
count: PropTypes.number.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent: PropTypes.object,
|
||||
}
|
||||
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
state = {
|
||||
tooltipRendered: false,
|
||||
tooltipVisible: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
tooltipVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
onClick = () => {
|
||||
const { mxEvent, myReactionEvent, content } = this.props;
|
||||
if (myReactionEvent) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.context.redactEvent(
|
||||
mxEvent.getRoomId(),
|
||||
myReactionEvent.getId(),
|
||||
);
|
||||
} else {
|
||||
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": mxEvent.getId(),
|
||||
|
@ -83,8 +88,6 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const ReactionsRowButtonTooltip =
|
||||
sdk.getComponent('messages.ReactionsRowButtonTooltip');
|
||||
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -102,7 +105,7 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
/>;
|
||||
}
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||
let label;
|
||||
if (room) {
|
||||
const senders = [];
|
||||
|
@ -130,7 +133,6 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
const isPeeking = room.getMyMembership() !== "join";
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <AccessibleButton
|
||||
className={classes}
|
||||
aria-label={label}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 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,33 +14,34 @@ 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 from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The reaction content / key / emoji
|
||||
content: string;
|
||||
// A Set of Matrix reaction events for this key
|
||||
reactionEvents: Set<MatrixEvent>;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
}
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const { content, reactionEvents, mxEvent, visible } = this.props;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||
let tooltipLabel;
|
||||
if (room) {
|
||||
const senders = [];
|
|
@ -15,37 +15,47 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import Flair from '../elements/Flair';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onClick(): void;
|
||||
enableFlair: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
userGroups;
|
||||
relatedGroups;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.SenderProfile")
|
||||
export default class SenderProfile extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default class SenderProfile extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
private unmounted: boolean;
|
||||
|
||||
state = {
|
||||
userGroups: null,
|
||||
relatedGroups: [],
|
||||
};
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
const senderId = this.props.mxEvent.getSender();
|
||||
|
||||
this.state = {
|
||||
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
|
||||
relatedGroups: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this._updateRelatedGroups();
|
||||
|
||||
FlairStore.getPublicisedGroupsCached(
|
||||
this.context, this.props.mxEvent.getSender(),
|
||||
).then((userGroups) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({userGroups});
|
||||
});
|
||||
if (this.state.userGroups.length === 0) {
|
||||
this.getPublicisedGroups();
|
||||
}
|
||||
|
||||
|
||||
this.context.on('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
@ -55,6 +65,15 @@ export default class SenderProfile extends React.Component {
|
|||
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
async getPublicisedGroups() {
|
||||
if (!this.unmounted) {
|
||||
const userGroups = await FlairStore.getPublicisedGroupsCached(
|
||||
this.context, this.props.mxEvent.getSender(),
|
||||
);
|
||||
this.setState({userGroups});
|
||||
}
|
||||
}
|
||||
|
||||
onRoomStateEvents = event => {
|
||||
if (event.getType() === 'm.room.related_groups' &&
|
||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||
|
@ -89,14 +108,26 @@ export default class SenderProfile extends React.Component {
|
|||
render() {
|
||||
const {mxEvent} = this.props;
|
||||
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
const disambiguate = mxEvent.sender?.disambiguate;
|
||||
const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
|
||||
const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return <span />; // emote message must include the name so don't duplicate it
|
||||
return null; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
let flair = <div />;
|
||||
let mxidElement;
|
||||
if (disambiguate) {
|
||||
mxidElement = (
|
||||
<span className="mx_SenderProfile_mxid">
|
||||
{ mxid }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let flair;
|
||||
if (this.props.enableFlair) {
|
||||
const displayedGroups = this._getDisplayedGroups(
|
||||
this.state.userGroups, this.state.relatedGroups,
|
||||
|
@ -108,21 +139,13 @@ export default class SenderProfile extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
const nameElem = name || '';
|
||||
|
||||
// Name + flair
|
||||
const nameFlair = <span>
|
||||
<span className={`mx_SenderProfile_name ${colorClass}`}>
|
||||
{ nameElem }
|
||||
</span>
|
||||
{ flair }
|
||||
</span>;
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
||||
<div className="mx_SenderProfile_hover">
|
||||
{ nameFlair }
|
||||
</div>
|
||||
<span className={`mx_SenderProfile_displayName ${colorClass}`}>
|
||||
{ displayName }
|
||||
</span>
|
||||
{ mxidElement }
|
||||
{ flair }
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -16,12 +16,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import highlight from 'highlight.js';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import { formatDate } from '../../../DateUtils';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -29,13 +29,16 @@ import { _t } from '../../../languageHandler';
|
|||
import * as ContextMenu from '../../structures/ContextMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import { isPermalinkHost } from "../../../utils/permalinks/Permalinks";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
@replaceableComponent("views.messages.TextualBody")
|
||||
export default class TextualBody extends React.Component {
|
||||
|
@ -143,7 +146,7 @@ export default class TextualBody extends React.Component {
|
|||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
@ -277,15 +280,15 @@ export default class TextualBody extends React.Component {
|
|||
// pass only the first child which is the event tile otherwise this recurses on edited events
|
||||
let links = this.findLinks([this._content.current]);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
const seen = new Set();
|
||||
links = links.filter((link) => {
|
||||
if (seen.has(link)) return false;
|
||||
seen.add(link);
|
||||
return true;
|
||||
});
|
||||
// de-duplicate the links after stripping hashes as they don't affect the preview
|
||||
// using a set here maintains the order
|
||||
links = Array.from(new Set(links.map(link => {
|
||||
const url = new URL(link);
|
||||
url.hash = "";
|
||||
return url.toString();
|
||||
})));
|
||||
|
||||
this.setState({ links: links });
|
||||
this.setState({ links });
|
||||
|
||||
// lazy-load the hidden state of the preview widget from localstorage
|
||||
if (global.localStorage) {
|
||||
|
@ -389,9 +392,9 @@ export default class TextualBody extends React.Component {
|
|||
|
||||
onEmoteSenderClick = event => {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
dis.dispatch({
|
||||
action: 'insert_mention',
|
||||
user_id: mxEvent.getSender(),
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: mxEvent.getSender(),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class TextualEvent extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
|
||||
if (text == null || text.length === 0) return null;
|
||||
return (
|
||||
<div className="mx_TextualEvent">{ text }</div>
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
@replaceableComponent("views.messages.ViewSourceEvent")
|
||||
export default class ViewSourceEvent extends React.PureComponent {
|
||||
|
@ -36,6 +37,10 @@ export default class ViewSourceEvent extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
const {mxEvent} = this.props;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.decryptEventIfNeeded(mxEvent);
|
||||
|
||||
if (mxEvent.isBeingDecrypted()) {
|
||||
mxEvent.once("Event.decrypted", () => this.forceUpdate());
|
||||
}
|
||||
|
|
|
@ -17,8 +17,9 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
export const PendingActionSpinner = ({text}) => {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
@ -31,7 +32,7 @@ export const PendingActionSpinner = ({text}) => {
|
|||
interface IProps {
|
||||
waitingForOtherParty: boolean;
|
||||
waitingForNetwork: boolean;
|
||||
member: RoomMember;
|
||||
member: RoomMember | User;
|
||||
onStartVerification: () => Promise<void>;
|
||||
isRoomEncrypted: boolean;
|
||||
inDialog: boolean;
|
||||
|
@ -55,7 +56,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
text = _t("Accept on your other login…");
|
||||
} else {
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -14,28 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { PHASE_REQUESTED, PHASE_UNSENT } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import EncryptionInfo from "./EncryptionInfo";
|
||||
import VerificationPanel from "./VerificationPanel";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {ensureDMExists} from "../../../createRoom";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import Modal from "../../../Modal";
|
||||
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
|
||||
// cancellation codes which constitute a key mismatch
|
||||
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
||||
|
||||
interface IProps {
|
||||
member: RoomMember;
|
||||
member: RoomMember | User;
|
||||
onClose: () => void;
|
||||
verificationRequest: VerificationRequest;
|
||||
verificationRequestPromise: Promise<VerificationRequest>;
|
||||
|
|
|
@ -21,12 +21,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const GROUP_PHASES = [
|
||||
RightPanelPhases.GroupMemberInfo,
|
||||
|
@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
|||
};
|
||||
|
||||
renderButtons() {
|
||||
return [
|
||||
<HeaderButton key="groupMembersButton" name="groupMembersButton"
|
||||
return <>
|
||||
<HeaderButton
|
||||
name="groupMembersButton"
|
||||
title={_t('Members')}
|
||||
isHighlighted={this.isPhase(GROUP_PHASES)}
|
||||
onClick={this.onMembersClicked}
|
||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="roomsButton" name="roomsButton"
|
||||
/>
|
||||
<HeaderButton
|
||||
name="roomsButton"
|
||||
title={_t('Rooms')}
|
||||
isHighlighted={this.isPhase(ROOM_PHASES)}
|
||||
onClick={this.onRoomsClicked}
|
||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,15 +22,13 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
import Analytics from '../../../Analytics';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// Whether this button is highlighted
|
||||
isHighlighted: boolean;
|
||||
// click handler
|
||||
onClick: () => void;
|
||||
// The badge to display above the icon
|
||||
badge?: React.ReactNode;
|
||||
// The parameters to track the click event
|
||||
analytics: Parameters<typeof Analytics.trackEvent>;
|
||||
|
||||
|
@ -40,31 +38,29 @@ interface IProps {
|
|||
title: string;
|
||||
}
|
||||
|
||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified
|
||||
// representation
|
||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified representation
|
||||
@replaceableComponent("views.right_panel.HeaderButton")
|
||||
export default class HeaderButton extends React.Component<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
private onClick() {
|
||||
private onClick = () => {
|
||||
Analytics.trackEvent(...this.props.analytics);
|
||||
this.props.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {isHighlighted, onClick, analytics, name, title, ...props} = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_RightPanel_headerButton: true,
|
||||
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
|
||||
[`mx_RightPanel_${this.props.name}`]: true,
|
||||
mx_RightPanel_headerButton_highlight: isHighlighted,
|
||||
[`mx_RightPanel_${name}`]: true,
|
||||
});
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
aria-selected={this.props.isHighlighted}
|
||||
{...props}
|
||||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={this.props.title}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
/>;
|
||||
|
|
|
@ -21,14 +21,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from '../../../dispatcher/actions';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import {
|
||||
SetRightPanelPhasePayload,
|
||||
SetRightPanelPhaseRefireParams,
|
||||
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import type { EventSubscription } from "fbemitter";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export enum HeaderKind {
|
||||
Room = "room",
|
||||
|
@ -43,11 +43,11 @@ interface IState {
|
|||
interface IProps {}
|
||||
|
||||
@replaceableComponent("views.right_panel.HeaderButtons")
|
||||
export default abstract class HeaderButtons extends React.Component<IProps, IState> {
|
||||
export default abstract class HeaderButtons<P = {}> extends React.Component<IProps & P, IState> {
|
||||
private storeToken: EventSubscription;
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps, kind: HeaderKind) {
|
||||
constructor(props: IProps & P, kind: HeaderKind) {
|
||||
super(props);
|
||||
|
||||
const rps = RightPanelStore.getSharedInstance();
|
||||
|
@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
|
|||
}
|
||||
|
||||
// XXX: Make renderButtons a prop
|
||||
public abstract renderButtons(): JSX.Element[];
|
||||
public abstract renderButtons(): JSX.Element;
|
||||
|
||||
public render() {
|
||||
return <div className="mx_HeaderButtons">
|
||||
|
|
170
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
170
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
Copyright 2021 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, {useCallback, useContext, useEffect, useState} from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import BaseCard from "./BaseCard";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import PinningUtils from "../../../utils/PinningUtils";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import PinnedEventTile from "../rooms/PinnedEventTile";
|
||||
import { useRoomState } from "../../../hooks/useRoomState";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export const usePinnedEvents = (room: Room): string[] => {
|
||||
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
|
||||
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
|
||||
setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(room?.currentState, "RoomState.events", update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
setPinnedEvents([]);
|
||||
};
|
||||
}, [update]);
|
||||
return pinnedEvents;
|
||||
};
|
||||
|
||||
export const ReadPinsEventId = "im.vector.room.read_pins";
|
||||
|
||||
export const useReadPinnedEvents = (room: Room): Set<string> => {
|
||||
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
|
||||
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== ReadPinsEventId) return;
|
||||
const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids;
|
||||
setReadPinnedEvents(new Set(readPins || []));
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(room, "Room.accountData", update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
setReadPinnedEvents(new Set());
|
||||
};
|
||||
}, [update]);
|
||||
return readPinnedEvents;
|
||||
};
|
||||
|
||||
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
||||
const pinnedEventIds = usePinnedEvents(room);
|
||||
const readPinnedEvents = useReadPinnedEvents(room);
|
||||
|
||||
useEffect(() => {
|
||||
const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id));
|
||||
if (newlyRead.length > 0) {
|
||||
// clear out any read pinned events which no longer are pinned
|
||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||
event_ids: pinnedEventIds,
|
||||
});
|
||||
}
|
||||
}, [cli, room.roomId, pinnedEventIds, readPinnedEvents]);
|
||||
|
||||
const pinnedEvents = useAsyncMemo(() => {
|
||||
const promises = pinnedEventIds.map(async eventId => {
|
||||
const timelineSet = room.getUnfilteredTimelineSet();
|
||||
const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId);
|
||||
if (localEvent) return localEvent;
|
||||
|
||||
try {
|
||||
const evJson = await cli.fetchRoomEvent(room.roomId, eventId);
|
||||
const event = new MatrixEvent(evJson);
|
||||
if (event.isEncrypted()) {
|
||||
await cli.decryptEventIfNeeded(event); // TODO await?
|
||||
}
|
||||
if (event && PinningUtils.isPinnable(event)) {
|
||||
return event;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
|
||||
console.error(err);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}, [cli, room, pinnedEventIds], null);
|
||||
|
||||
let content;
|
||||
if (!pinnedEvents) {
|
||||
content = <Spinner />;
|
||||
} else if (pinnedEvents.length > 0) {
|
||||
let onUnpinClicked;
|
||||
if (canUnpin) {
|
||||
onUnpinClicked = async (event: MatrixEvent) => {
|
||||
const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
|
||||
if (pinnedEvents?.getContent()?.pinned) {
|
||||
const pinned = pinnedEvents.getContent().pinned;
|
||||
const index = pinned.indexOf(event.getId());
|
||||
if (index !== -1) {
|
||||
pinned.splice(index, 1);
|
||||
await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// show them in reverse, with latest pinned at the top
|
||||
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
|
||||
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
|
||||
));
|
||||
} else {
|
||||
content = <div className="mx_PinnedMessagesCard_empty">
|
||||
<div>
|
||||
{ /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
|
||||
<div className="mx_PinnedMessagesCard_MessageActionBar">
|
||||
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
|
||||
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
|
||||
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
|
||||
</div>
|
||||
|
||||
<h2>{ _t("Nothing pinned, yet") }</h2>
|
||||
{ _t("If you have permissions, open the menu on any message and select " +
|
||||
"<b>Pin</b> to stick them here.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <BaseCard
|
||||
header={<h2>{ _t("Pinned messages") }</h2>}
|
||||
className="mx_PinnedMessagesCard"
|
||||
onClose={onClose}
|
||||
>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
};
|
||||
|
||||
export default PinnedMessagesCard;
|
|
@ -18,15 +18,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||
|
||||
const ROOM_INFO_PHASES = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
|
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
|
|||
RightPanelPhases.Room3pidMemberInfo,
|
||||
];
|
||||
|
||||
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
|
||||
const pinningEnabled = useSettingValue("feature_pinning");
|
||||
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
|
||||
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
|
||||
if (!pinningEnabled) return null;
|
||||
|
||||
let unreadIndicator;
|
||||
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
|
||||
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
|
||||
}
|
||||
|
||||
return <HeaderButton
|
||||
name="pinnedMessagesButton"
|
||||
title={_t("Pinned messages")}
|
||||
isHighlighted={isHighlighted}
|
||||
onClick={onClick}
|
||||
analytics={["Right Panel", "Pinned Messages Button", "click"]}
|
||||
>
|
||||
{ unreadIndicator }
|
||||
</HeaderButton>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
room?: Room;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
||||
export default class RoomHeaderButtons extends HeaderButtons {
|
||||
constructor(props) {
|
||||
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props, HeaderKind.Room);
|
||||
}
|
||||
|
||||
|
@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||
};
|
||||
|
||||
private onPinnedMessagesClicked = () => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.PinnedMessages);
|
||||
};
|
||||
|
||||
public renderButtons() {
|
||||
return [
|
||||
return <>
|
||||
<PinnedMessagesHeaderButton
|
||||
room={this.props.room}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||
onClick={this.onPinnedMessagesClicked}
|
||||
/>
|
||||
<HeaderButton
|
||||
key="notifsButton"
|
||||
name="notifsButton"
|
||||
title={_t('Notifications')}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||
onClick={this.onNotificationsClicked}
|
||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||
/>,
|
||||
/>
|
||||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room Info')}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
analytics={['Right Panel', 'Room Summary Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,8 @@ import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../struc
|
|||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -115,8 +117,8 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
const rect = handle.current.getBoundingClientRect();
|
||||
contextMenu = <WidgetContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={window.innerWidth - rect.right}
|
||||
bottom={window.innerHeight - rect.top}
|
||||
right={UIStore.instance.windowWidth - rect.right}
|
||||
bottom={UIStore.instance.windowHeight - rect.top}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
/>;
|
||||
|
@ -249,7 +251,13 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<h2 title={room.name}>{ room.name }</h2>
|
||||
<RoomName room={room}>
|
||||
{ name => (
|
||||
<h2 title={name}>
|
||||
{ name }
|
||||
</h2>
|
||||
)}
|
||||
</RoomName>
|
||||
<div className="mx_RoomSummaryCard_alias" title={alias}>
|
||||
{ alias }
|
||||
</div>
|
||||
|
|
|
@ -17,18 +17,19 @@ 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, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
||||
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
|
||||
import {User} from 'matrix-js-sdk/src/models/user';
|
||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { User } from 'matrix-js-sdk/src/models/user';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -37,20 +38,20 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {textualPowerLevel} from '../../../Roles';
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { textualPowerLevel } from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
|
||||
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
||||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
import {E2EStatus} from "../../../utils/ShieldUtils";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import ImageView from "../elements/ImageView";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import PowerSelector from "../elements/PowerSelector";
|
||||
|
@ -65,9 +66,11 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
interface IDevice {
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
ambiguous?: boolean;
|
||||
getDisplayName(): string;
|
||||
|
@ -144,7 +147,7 @@ async function openDMForUser(matrixClient: MatrixClient, userId: string) {
|
|||
|
||||
type SetUpdating = (updating: boolean) => void;
|
||||
|
||||
function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) {
|
||||
function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean, setUpdating: SetUpdating) {
|
||||
return useAsyncMemo(async () => {
|
||||
if (!canVerify) {
|
||||
return undefined;
|
||||
|
@ -366,9 +369,9 @@ const UserOptionsSection: React.FC<{
|
|||
};
|
||||
|
||||
const onInsertPillButton = function() {
|
||||
dis.dispatch({
|
||||
action: 'insert_mention',
|
||||
user_id: member.userId,
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: member.userId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -500,22 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
|
|||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
const getPowerLevels = room => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||
|
||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||
|
||||
const update = useCallback((ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||
|
||||
const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (event) {
|
||||
setPowerLevels(event.getContent());
|
||||
} else {
|
||||
setPowerLevels({});
|
||||
}
|
||||
return () => {
|
||||
setPowerLevels({});
|
||||
};
|
||||
setPowerLevels(getPowerLevels(room));
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(cli, "RoomState.events", update);
|
||||
|
@ -972,7 +968,7 @@ interface IRoomPermissions {
|
|||
canInvite: boolean;
|
||||
}
|
||||
|
||||
function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions {
|
||||
function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions {
|
||||
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
modifyLevelMax: -1,
|
||||
|
@ -1029,7 +1025,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPer
|
|||
}
|
||||
|
||||
const PowerLevelSection: React.FC<{
|
||||
user: User;
|
||||
user: RoomMember;
|
||||
room: Room;
|
||||
roomPermissions: IRoomPermissions;
|
||||
powerLevels: IPowerLevelsContent;
|
||||
|
@ -1038,7 +1034,7 @@ const PowerLevelSection: React.FC<{
|
|||
return (<PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />);
|
||||
} else {
|
||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||
const powerLevel = parseInt(user.powerLevel, 10);
|
||||
const powerLevel = user.powerLevel;
|
||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
|
@ -1049,13 +1045,13 @@ const PowerLevelSection: React.FC<{
|
|||
};
|
||||
|
||||
const PowerLevelEditor: React.FC<{
|
||||
user: User;
|
||||
user: RoomMember;
|
||||
room: Room;
|
||||
roomPermissions: IRoomPermissions;
|
||||
}> = ({user, room, roomPermissions}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
||||
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
||||
const powerLevel = parseInt(powerLevelStr, 10);
|
||||
setSelectedPowerLevel(powerLevel);
|
||||
|
@ -1232,7 +1228,7 @@ const BasicUserInfo: React.FC<{
|
|||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, member);
|
||||
const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||
|
@ -1276,12 +1272,26 @@ const BasicUserInfo: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
let memberDetails;
|
||||
let adminToolsContainer;
|
||||
if (room && member.roomId) {
|
||||
if (room && (member as RoomMember).roomId) {
|
||||
// hide the Roles section for DMs as it doesn't make sense there
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
|
||||
memberDetails = <div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Role") }</h3>
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member as RoomMember}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
adminToolsContainer = (
|
||||
<RoomAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={member}
|
||||
member={member as RoomMember}
|
||||
room={room}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
|
@ -1307,21 +1317,7 @@ const BasicUserInfo: React.FC<{
|
|||
}
|
||||
|
||||
if (pendingUpdateCount > 0) {
|
||||
spinner = <Spinner imgClassName="mx_ContextualMenu_spinner" />;
|
||||
}
|
||||
|
||||
let memberDetails;
|
||||
// hide the Roles section for DMs as it doesn't make sense there
|
||||
if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) {
|
||||
memberDetails = <div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Role") }</h3>
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
</div>;
|
||||
spinner = <Spinner />;
|
||||
}
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
|
@ -1350,8 +1346,7 @@ const BasicUserInfo: React.FC<{
|
|||
const setUpdating = (updating) => {
|
||||
setPendingUpdateCount(count => count + (updating ? 1 : -1));
|
||||
};
|
||||
const hasCrossSigningKeys =
|
||||
useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
|
||||
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
|
||||
|
||||
const showDeviceListSpinner = devices === undefined;
|
||||
if (canVerify) {
|
||||
|
@ -1360,9 +1355,9 @@ const BasicUserInfo: React.FC<{
|
|||
verifyButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {
|
||||
if (hasCrossSigningKeys) {
|
||||
verifyUser(member);
|
||||
verifyUser(member as User);
|
||||
} else {
|
||||
legacyVerifyUser(member);
|
||||
legacyVerifyUser(member as User);
|
||||
}
|
||||
}}>
|
||||
{_t("Verify")}
|
||||
|
@ -1382,7 +1377,7 @@ const BasicUserInfo: React.FC<{
|
|||
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: USER_SECURITY_TAB,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}}>
|
||||
{ _t("Edit devices") }
|
||||
|
@ -1410,7 +1405,7 @@ const BasicUserInfo: React.FC<{
|
|||
<UserOptionsSection
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member}
|
||||
member={member as RoomMember}
|
||||
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
|
||||
/>
|
||||
|
||||
|
@ -1429,13 +1424,15 @@ const UserInfoHeader: React.FC<{
|
|||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
||||
? (member as RoomMember).getMxcAvatarUrl()
|
||||
: (member as User).avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: member.name,
|
||||
name: (member as RoomMember).name || (member as User).displayName,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
|
@ -1447,13 +1444,13 @@ const UserInfoHeader: React.FC<{
|
|||
<div>
|
||||
<MemberAvatar
|
||||
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
||||
member={member}
|
||||
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
member={member as RoomMember}
|
||||
width={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||
height={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={onMemberAvatarClick}
|
||||
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
||||
urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1470,7 +1467,11 @@ const UserInfoHeader: React.FC<{
|
|||
presenceCurrentlyActive = member.user.currentlyActive;
|
||||
|
||||
if (SettingsStore.getValue("feature_custom_status")) {
|
||||
statusMessage = member.user._unstable_statusMessage;
|
||||
if ((member as RoomMember).user) {
|
||||
statusMessage = member.user.unstable_statusMessage;
|
||||
} else {
|
||||
statusMessage = (member as unknown as User).unstable_statusMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1501,7 +1502,7 @@ const UserInfoHeader: React.FC<{
|
|||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
|
||||
const displayName = member.rawDisplayName || member.displayname;
|
||||
const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname;
|
||||
return <React.Fragment>
|
||||
{ avatarElement }
|
||||
|
||||
|
@ -1529,21 +1530,16 @@ interface IProps {
|
|||
user: Member;
|
||||
groupId?: string;
|
||||
room?: Room;
|
||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
|
||||
phase: RightPanelPhases.RoomMemberInfo
|
||||
| RightPanelPhases.GroupMemberInfo
|
||||
| RightPanelPhases.SpaceMemberInfo
|
||||
| RightPanelPhases.EncryptionPanel;
|
||||
onClose(): void;
|
||||
verificationRequest?: VerificationRequest;
|
||||
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||
}
|
||||
|
||||
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
|
||||
user: Member;
|
||||
groupId: void;
|
||||
room: Room;
|
||||
phase: RightPanelPhases.EncryptionPanel;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
type Props = IProps | IPropsWithEncryptionPanel;
|
||||
|
||||
const UserInfo: React.FC<Props> = ({
|
||||
const UserInfo: React.FC<IProps> = ({
|
||||
user,
|
||||
groupId,
|
||||
room,
|
||||
|
@ -1594,7 +1590,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
member={member}
|
||||
member={member as User}
|
||||
groupId={groupId as string}
|
||||
devices={devices}
|
||||
isRoomEncrypted={isRoomEncrypted} />
|
||||
|
@ -1605,7 +1601,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
content = (
|
||||
<EncryptionPanel
|
||||
{...props as React.ComponentProps<typeof EncryptionPanel>}
|
||||
member={member}
|
||||
member={member as User | RoomMember}
|
||||
onClose={onEncryptionPanelClose}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
|
|
|
@ -22,6 +22,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
|||
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
|
||||
|
@ -51,7 +52,7 @@ enum VerificationPhase {
|
|||
interface IProps {
|
||||
layout: string;
|
||||
request: VerificationRequest;
|
||||
member: RoomMember;
|
||||
member: RoomMember | User;
|
||||
phase: VerificationPhase;
|
||||
onClose: () => void;
|
||||
isRoomEncrypted: boolean;
|
||||
|
@ -134,7 +135,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
qrBlock = <div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify by scanning")}</h3>
|
||||
<p>{_t("Ask %(displayName)s to scan your code:", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
})}</p>
|
||||
|
||||
<div className="mx_VerificationPanel_qrCode">
|
||||
|
@ -194,37 +195,33 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
private renderQRReciprocatePhase() {
|
||||
const {member, request} = this.props;
|
||||
let Button;
|
||||
// a bit of a hack, but the FormButton should only be used in the right panel
|
||||
// they should probably just be the same component with a css class applied to it?
|
||||
if (this.props.inDialog) {
|
||||
Button = sdk.getComponent("elements.AccessibleButton");
|
||||
} else {
|
||||
Button = sdk.getComponent("elements.FormButton");
|
||||
}
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const description = request.isSelfVerification ?
|
||||
_t("Almost there! Is your other session showing the same shield?") :
|
||||
_t("Almost there! Is %(displayName)s showing the same shield?", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
let body: JSX.Element;
|
||||
if (this.state.reciprocateQREvent) {
|
||||
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
||||
//
|
||||
// we're passing both a label and a child string to Button as
|
||||
// FormButton and AccessibleButton expect this differently
|
||||
body = <React.Fragment>
|
||||
<p>{description}</p>
|
||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||
<Button
|
||||
label={_t("No")} kind="danger"
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateNoClick}>{_t("No")}</Button>
|
||||
<Button
|
||||
label={_t("Yes")} kind="primary"
|
||||
onClick={this.onReciprocateNoClick}
|
||||
>
|
||||
{ _t("No") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateYesClick}>{_t("Yes")}</Button>
|
||||
onClick={this.onReciprocateYesClick}
|
||||
>
|
||||
{ _t("Yes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
|
@ -264,7 +261,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
}
|
||||
} else {
|
||||
description = _t("You've successfully verified %(displayName)s!", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -302,7 +299,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
text = _t("You cancelled verification on your other session.");
|
||||
} else {
|
||||
text = _t("%(displayName)s cancelled verification.", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
text = `${text} ${startAgainInstruction}`;
|
||||
|
@ -325,7 +322,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
public render() {
|
||||
const {member, phase, request} = this.props;
|
||||
|
||||
const displayName = member.displayName || member.name || member.userId;
|
||||
const displayName = (member as User).displayName || (member as RoomMember).name || member.userId;
|
||||
|
||||
switch (phase) {
|
||||
case PHASE_READY:
|
||||
|
|
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