Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/spaces-admin-check
This commit is contained in:
commit
c39519a753
248 changed files with 5294 additions and 18209 deletions
|
@ -19,6 +19,8 @@ import { _t } from '../../../languageHandler';
|
|||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
||||
interface ICaptchaFormProps {
|
||||
|
@ -60,7 +62,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
|
|||
// already loaded
|
||||
this.onCaptchaLoaded();
|
||||
} else {
|
||||
console.log("Loading recaptcha script...");
|
||||
logger.log("Loading recaptcha script...");
|
||||
window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); };
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute(
|
||||
|
@ -109,7 +111,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
|
|||
}
|
||||
|
||||
private onCaptchaLoaded() {
|
||||
console.log("Loaded recaptcha script.");
|
||||
logger.log("Loaded recaptcha script.");
|
||||
try {
|
||||
this.renderRecaptcha(DIV_ID);
|
||||
// clear error if re-rendered
|
||||
|
|
|
@ -29,6 +29,8 @@ import { LocalisedPolicy, Policies } from '../../../Terms';
|
|||
import Field from '../elements/Field';
|
||||
import CaptchaForm from "./CaptchaForm";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
* for an auth stage. (The intention is that they could also be used for other
|
||||
|
@ -555,7 +557,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
|||
}
|
||||
} catch (e) {
|
||||
this.props.fail(e);
|
||||
console.log("Failed to submit msisdn token");
|
||||
logger.log("Failed to submit msisdn token");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
|
|||
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||
import ShareDialog from '../dialogs/ShareDialog';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
|
||||
|
||||
export function canCancel(eventStatus: EventStatus): boolean {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
|
@ -52,7 +53,8 @@ export interface IOperableEventTile {
|
|||
getEventTileOps(): IEventTileOps;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends IPosition {
|
||||
chevronFace: ChevronFace;
|
||||
/* the MatrixEvent associated with the context menu */
|
||||
mxEvent: MatrixEvent;
|
||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
||||
|
|
|
@ -18,15 +18,54 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Key } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
// Whether the dialog should have a 'close' button that will
|
||||
// cause the dialog to be cancelled. This should only be set
|
||||
// to false if there is nothing the app can sensibly do if the
|
||||
// dialog is cancelled, eg. "We can't restore your session and
|
||||
// the app cannot work". Default: true.
|
||||
hasCancel?: boolean;
|
||||
|
||||
// called when a key is pressed
|
||||
onKeyDown?: (e: KeyboardEvent | React.KeyboardEvent) => void;
|
||||
|
||||
// CSS class to apply to dialog div
|
||||
className?: string;
|
||||
|
||||
// if true, dialog container is 60% of the viewport width. Otherwise,
|
||||
// the container will have no fixed size, allowing its contents to
|
||||
// determine its size. Default: true.
|
||||
fixedWidth?: boolean;
|
||||
|
||||
// Title for the dialog.
|
||||
title?: JSX.Element | string;
|
||||
|
||||
// Path to an icon to put in the header
|
||||
headerImage?: string;
|
||||
|
||||
// children should be the content of the dialog
|
||||
children?: React.ReactNode;
|
||||
|
||||
// Id of content element
|
||||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId?: string;
|
||||
|
||||
// optional additional class for the title element (basically anything that can be passed to classnames)
|
||||
titleClass?: string | string[];
|
||||
|
||||
headerButton?: JSX.Element;
|
||||
}
|
||||
|
||||
/*
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -35,54 +74,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
* dialog on escape.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.BaseDialog")
|
||||
export default class BaseDialog extends React.Component {
|
||||
static propTypes = {
|
||||
// onFinished callback to call when Escape is pressed
|
||||
// Take a boolean which is true if the dialog was dismissed
|
||||
// with a positive / confirm action or false if it was
|
||||
// cancelled (BaseDialog itself only calls this with false).
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
export default class BaseDialog extends React.Component<IProps> {
|
||||
private matrixClient: MatrixClient;
|
||||
|
||||
// Whether the dialog should have a 'close' button that will
|
||||
// cause the dialog to be cancelled. This should only be set
|
||||
// to false if there is nothing the app can sensibly do if the
|
||||
// dialog is cancelled, eg. "We can't restore your session and
|
||||
// the app cannot work". Default: true.
|
||||
hasCancel: PropTypes.bool,
|
||||
|
||||
// called when a key is pressed
|
||||
onKeyDown: PropTypes.func,
|
||||
|
||||
// CSS class to apply to dialog div
|
||||
className: PropTypes.string,
|
||||
|
||||
// if true, dialog container is 60% of the viewport width. Otherwise,
|
||||
// the container will have no fixed size, allowing its contents to
|
||||
// determine its size. Default: true.
|
||||
fixedWidth: PropTypes.bool,
|
||||
|
||||
// Title for the dialog.
|
||||
title: PropTypes.node.isRequired,
|
||||
|
||||
// Path to an icon to put in the header
|
||||
headerImage: PropTypes.string,
|
||||
|
||||
// children should be the content of the dialog
|
||||
children: PropTypes.node,
|
||||
|
||||
// Id of content element
|
||||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId: PropTypes.string,
|
||||
|
||||
// optional additional class for the title element (basically anything that can be passed to classnames)
|
||||
titleClass: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
public static defaultProps = {
|
||||
hasCancel: true,
|
||||
fixedWidth: true,
|
||||
};
|
||||
|
@ -90,10 +85,10 @@ export default class BaseDialog extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
this.matrixClient = MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
_onKeyDown = (e) => {
|
||||
private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
|
@ -104,15 +99,15 @@ export default class BaseDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onCancelClick = (e) => {
|
||||
private onCancelClick = (e: ButtonEvent): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
cancelButton = (
|
||||
<AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("Close dialog")} />
|
||||
<AccessibleButton onClick={this.onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("Close dialog")} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -122,11 +117,11 @@ export default class BaseDialog extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<MatrixClientContext.Provider value={this.matrixClient}>
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={{
|
||||
onKeyDown: this._onKeyDown,
|
||||
onKeyDown: this.onKeyDown,
|
||||
role: "dialog",
|
||||
["aria-labelledby"]: "mx_BaseDialog_title",
|
||||
// This should point to a node describing the dialog.
|
|
@ -125,14 +125,14 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
setBusy(true);
|
||||
|
||||
// require & validate the space name field
|
||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||
setBusy(false);
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||
setBusy(false);
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
|
|
|
@ -64,14 +64,14 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
|
|||
|
||||
setBusy(true);
|
||||
// require & validate the space name field
|
||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
setBusy(false);
|
||||
|
|
|
@ -23,10 +23,9 @@ import Modal from '../../../Modal';
|
|||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
|
|
@ -44,6 +44,8 @@ import { SettingLevel } from '../../../settings/SettingLevel';
|
|||
import BaseDialog from "./BaseDialog";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
interface IGenericEditorProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
@ -984,7 +986,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
|||
const parsedExplicit = JSON.parse(this.state.explicitValues);
|
||||
const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues);
|
||||
for (const level of Object.keys(parsedExplicit)) {
|
||||
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
||||
logger.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
||||
try {
|
||||
const val = parsedExplicit[level];
|
||||
await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
|
||||
|
@ -994,7 +996,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
|||
}
|
||||
const roomId = this.props.room.roomId;
|
||||
for (const level of Object.keys(parsedExplicit)) {
|
||||
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
||||
logger.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
||||
try {
|
||||
const val = parsedExplicitRoom[level];
|
||||
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);
|
||||
|
|
|
@ -19,30 +19,33 @@ import QuestionDialog from './QuestionDialog';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import CountlyAnalytics, { Rating } from "../../../CountlyAnalytics";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Modal from "../../../Modal";
|
||||
import BugReportDialog from "./BugReportDialog";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
|
||||
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
|
||||
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose";
|
||||
|
||||
export default (props) => {
|
||||
const [rating, setRating] = useState("");
|
||||
const [comment, setComment] = useState("");
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
const onDebugLogsLinkClick = () => {
|
||||
const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
||||
const [rating, setRating] = useState<Rating>();
|
||||
const [comment, setComment] = useState<string>("");
|
||||
|
||||
const onDebugLogsLinkClick = (): void => {
|
||||
props.onFinished();
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||
};
|
||||
|
||||
const hasFeedback = CountlyAnalytics.instance.canEnable();
|
||||
const onFinished = (sendFeedback) => {
|
||||
const onFinished = (sendFeedback: boolean): void => {
|
||||
if (hasFeedback && sendFeedback) {
|
||||
CountlyAnalytics.instance.reportFeedback(parseInt(rating, 10), comment);
|
||||
CountlyAnalytics.instance.reportFeedback(rating, comment);
|
||||
Modal.createTrackedDialog('Feedback sent', '', InfoDialog, {
|
||||
title: _t('Feedback sent'),
|
||||
description: _t('Thank you!'),
|
||||
|
@ -65,8 +68,8 @@ export default (props) => {
|
|||
|
||||
<StyledRadioGroup
|
||||
name="feedbackRating"
|
||||
value={rating}
|
||||
onChange={setRating}
|
||||
value={String(rating)}
|
||||
onChange={(r) => setRating(parseInt(r, 10) as Rating)}
|
||||
definitions={[
|
||||
{ value: "1", label: "😠" },
|
||||
{ value: "2", label: "😞" },
|
||||
|
@ -138,7 +141,9 @@ export default (props) => {
|
|||
{ countlyFeedbackSection }
|
||||
</React.Fragment>}
|
||||
button={hasFeedback ? _t("Send feedback") : _t("Go back")}
|
||||
buttonDisabled={hasFeedback && rating === ""}
|
||||
buttonDisabled={hasFeedback && !rating}
|
||||
onFinished={onFinished}
|
||||
/>);
|
||||
};
|
||||
|
||||
export default FeedbackDialog;
|
|
@ -15,12 +15,22 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import VerificationComplete from "../verification/VerificationComplete";
|
||||
import VerificationCancelled from "../verification/VerificationCancelled";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import VerificationShowSas from "../verification/VerificationShowSas";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import { IGeneratedSas, ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
import { VerificationBase } from "matrix-js-sdk/src/crypto/verification/Base";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
const PHASE_START = 0;
|
||||
const PHASE_SHOW_SAS = 1;
|
||||
|
@ -28,41 +38,56 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
|
|||
const PHASE_VERIFIED = 3;
|
||||
const PHASE_CANCELLED = 4;
|
||||
|
||||
@replaceableComponent("views.dialogs.IncomingSasDialog")
|
||||
export default class IncomingSasDialog extends React.Component {
|
||||
static propTypes = {
|
||||
verifier: PropTypes.object.isRequired,
|
||||
};
|
||||
interface IProps extends IDialogProps {
|
||||
verifier: VerificationBase; // TODO types
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IState {
|
||||
phase: number;
|
||||
sasVerified: boolean;
|
||||
opponentProfile: {
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
displayname?: string;
|
||||
};
|
||||
opponentProfileError: Error;
|
||||
sas: IGeneratedSas;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.IncomingSasDialog")
|
||||
export default class IncomingSasDialog extends React.Component<IProps, IState> {
|
||||
private showSasEvent: ISasEvent;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
let phase = PHASE_START;
|
||||
if (this.props.verifier.cancelled) {
|
||||
console.log("Verifier was cancelled in the background.");
|
||||
if (this.props.verifier.hasBeenCancelled) {
|
||||
logger.log("Verifier was cancelled in the background.");
|
||||
phase = PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
this._showSasEvent = null;
|
||||
this.showSasEvent = null;
|
||||
this.state = {
|
||||
phase: phase,
|
||||
sasVerified: false,
|
||||
opponentProfile: null,
|
||||
opponentProfileError: null,
|
||||
sas: null,
|
||||
};
|
||||
this.props.verifier.on('show_sas', this._onVerifierShowSas);
|
||||
this.props.verifier.on('cancel', this._onVerifierCancel);
|
||||
this._fetchOpponentProfile();
|
||||
this.props.verifier.on('show_sas', this.onVerifierShowSas);
|
||||
this.props.verifier.on('cancel', this.onVerifierCancel);
|
||||
this.fetchOpponentProfile();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) {
|
||||
this.props.verifier.cancel('User cancel');
|
||||
this.props.verifier.cancel(new Error('User cancel'));
|
||||
}
|
||||
this.props.verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
this.props.verifier.removeListener('show_sas', this.onVerifierShowSas);
|
||||
}
|
||||
|
||||
async _fetchOpponentProfile() {
|
||||
private async fetchOpponentProfile(): Promise<void> {
|
||||
try {
|
||||
const prof = await MatrixClientPeg.get().getProfileInfo(
|
||||
this.props.verifier.userId,
|
||||
|
@ -77,53 +102,49 @@ export default class IncomingSasDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onFinished = () => {
|
||||
private onFinished = (): void => {
|
||||
this.props.onFinished(this.state.phase === PHASE_VERIFIED);
|
||||
}
|
||||
};
|
||||
|
||||
_onCancelClick = () => {
|
||||
private onCancelClick = (): void => {
|
||||
this.props.onFinished(this.state.phase === PHASE_VERIFIED);
|
||||
}
|
||||
};
|
||||
|
||||
_onContinueClick = () => {
|
||||
private onContinueClick = (): void => {
|
||||
this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM });
|
||||
this.props.verifier.verify().then(() => {
|
||||
this.setState({ phase: PHASE_VERIFIED });
|
||||
}).catch((e) => {
|
||||
console.log("Verification failed", e);
|
||||
logger.log("Verification failed", e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onVerifierShowSas = (e) => {
|
||||
this._showSasEvent = e;
|
||||
private onVerifierShowSas = (e: ISasEvent): void => {
|
||||
this.showSasEvent = e;
|
||||
this.setState({
|
||||
phase: PHASE_SHOW_SAS,
|
||||
sas: e.sas,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onVerifierCancel = (e) => {
|
||||
private onVerifierCancel = (): void => {
|
||||
this.setState({
|
||||
phase: PHASE_CANCELLED,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onSasMatchesClick = () => {
|
||||
this._showSasEvent.confirm();
|
||||
private onSasMatchesClick = (): void => {
|
||||
this.showSasEvent.confirm();
|
||||
this.setState({
|
||||
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onVerifiedDoneClick = () => {
|
||||
private onVerifiedDoneClick = (): void => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_renderPhaseStart() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
};
|
||||
|
||||
private renderPhaseStart(): JSX.Element {
|
||||
const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
let profile;
|
||||
|
@ -190,27 +211,24 @@ export default class IncomingSasDialog extends React.Component {
|
|||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={this._onContinueClick}
|
||||
onCancel={this._onCancelClick}
|
||||
onPrimaryButtonClick={this.onContinueClick}
|
||||
onCancel={this.onCancelClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderPhaseShowSas() {
|
||||
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
|
||||
private renderPhaseShowSas(): JSX.Element {
|
||||
return <VerificationShowSas
|
||||
sas={this._showSasEvent.sas}
|
||||
onCancel={this._onCancelClick}
|
||||
onDone={this._onSasMatchesClick}
|
||||
sas={this.showSasEvent.sas}
|
||||
onCancel={this.onCancelClick}
|
||||
onDone={this.onSasMatchesClick}
|
||||
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
|
||||
inDialog={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
_renderPhaseWaitForPartnerToConfirm() {
|
||||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||
|
||||
private renderPhaseWaitForPartnerToConfirm(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
|
@ -219,41 +237,38 @@ export default class IncomingSasDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderPhaseVerified() {
|
||||
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
|
||||
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
|
||||
private renderPhaseVerified(): JSX.Element {
|
||||
return <VerificationComplete onDone={this.onVerifiedDoneClick} />;
|
||||
}
|
||||
|
||||
_renderPhaseCancelled() {
|
||||
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
|
||||
return <VerificationCancelled onDone={this._onCancelClick} />;
|
||||
private renderPhaseCancelled(): JSX.Element {
|
||||
return <VerificationCancelled onDone={this.onCancelClick} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let body;
|
||||
switch (this.state.phase) {
|
||||
case PHASE_START:
|
||||
body = this._renderPhaseStart();
|
||||
body = this.renderPhaseStart();
|
||||
break;
|
||||
case PHASE_SHOW_SAS:
|
||||
body = this._renderPhaseShowSas();
|
||||
body = this.renderPhaseShowSas();
|
||||
break;
|
||||
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
|
||||
body = this._renderPhaseWaitForPartnerToConfirm();
|
||||
body = this.renderPhaseWaitForPartnerToConfirm();
|
||||
break;
|
||||
case PHASE_VERIFIED:
|
||||
body = this._renderPhaseVerified();
|
||||
body = this.renderPhaseVerified();
|
||||
break;
|
||||
case PHASE_CANCELLED:
|
||||
body = this._renderPhaseCancelled();
|
||||
body = this.renderPhaseCancelled();
|
||||
break;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("Incoming Verification Request")}
|
||||
onFinished={this._onFinished}
|
||||
onFinished={this.onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
{ body }
|
|
@ -15,32 +15,28 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
@replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
|
||||
export default class IntegrationsDisabledDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onAcknowledgeClick = () => {
|
||||
export default class IntegrationsDisabledDialog extends React.Component<IProps> {
|
||||
private onAcknowledgeClick = (): void => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onOpenSettingsClick = () => {
|
||||
private onOpenSettingsClick = (): void => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_IntegrationsDisabledDialog'
|
||||
|
@ -53,9 +49,9 @@ export default class IntegrationsDisabledDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Settings")}
|
||||
onPrimaryButtonClick={this._onOpenSettingsClick}
|
||||
onPrimaryButtonClick={this.onOpenSettingsClick}
|
||||
cancelButton={_t("OK")}
|
||||
onCancel={this._onAcknowledgeClick}
|
||||
onCancel={this.onAcknowledgeClick}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -15,23 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as sdk from "../../../index";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
|
||||
export default class IntegrationsImpossibleDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onAcknowledgeClick = () => {
|
||||
export default class IntegrationsImpossibleDialog extends React.Component<IProps> {
|
||||
private onAcknowledgeClick = (): void => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
@ -54,7 +52,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
onPrimaryButtonClick={this._onAcknowledgeClick}
|
||||
onPrimaryButtonClick={this.onAcknowledgeClick}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</BaseDialog>
|
|
@ -17,69 +17,88 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
|
||||
import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
|
||||
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { IAuthData } from "matrix-js-sdk/src/interactive-auth";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IDialogAesthetics {
|
||||
[x: string]: {
|
||||
[x: number]: {
|
||||
title: string;
|
||||
body: string;
|
||||
continueText: string;
|
||||
continueKind: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: MatrixClient;
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData?: IAuthData;
|
||||
|
||||
// callback
|
||||
makeRequest: (auth: IAuthData) => Promise<IAuthData>;
|
||||
|
||||
// Optional title and body to show when not showing a particular stage
|
||||
title?: string;
|
||||
body?: string;
|
||||
|
||||
// Optional title and body pairs for particular stages and phases within
|
||||
// those stages. Object structure/example is:
|
||||
// {
|
||||
// "org.example.stage_type": {
|
||||
// 1: {
|
||||
// "body": "This is a body for phase 1" of org.example.stage_type,
|
||||
// "title": "Title for phase 1 of org.example.stage_type"
|
||||
// },
|
||||
// 2: {
|
||||
// "body": "This is a body for phase 2 of org.example.stage_type",
|
||||
// "title": "Title for phase 2 of org.example.stage_type"
|
||||
// "continueText": "Confirm identity with Example Auth",
|
||||
// "continueKind": "danger"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Default is defined in _getDefaultDialogAesthetics()
|
||||
aestheticsForStagePhases?: IDialogAesthetics;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
authError: Error;
|
||||
|
||||
// See _onUpdateStagePhase()
|
||||
uiaStage: number | string;
|
||||
uiaStagePhase: number | string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.InteractiveAuthDialog")
|
||||
export default class InteractiveAuthDialog extends React.Component {
|
||||
static propTypes = {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
export default class InteractiveAuthDialog extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: PropTypes.shape({
|
||||
flows: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
session: PropTypes.string,
|
||||
}),
|
||||
this.state = {
|
||||
authError: null,
|
||||
|
||||
// callback
|
||||
makeRequest: PropTypes.func.isRequired,
|
||||
// See _onUpdateStagePhase()
|
||||
uiaStage: null,
|
||||
uiaStagePhase: null,
|
||||
};
|
||||
}
|
||||
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Optional title and body to show when not showing a particular stage
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
|
||||
// Optional title and body pairs for particular stages and phases within
|
||||
// those stages. Object structure/example is:
|
||||
// {
|
||||
// "org.example.stage_type": {
|
||||
// 1: {
|
||||
// "body": "This is a body for phase 1" of org.example.stage_type,
|
||||
// "title": "Title for phase 1 of org.example.stage_type"
|
||||
// },
|
||||
// 2: {
|
||||
// "body": "This is a body for phase 2 of org.example.stage_type",
|
||||
// "title": "Title for phase 2 of org.example.stage_type"
|
||||
// "continueText": "Confirm identity with Example Auth",
|
||||
// "continueKind": "danger"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Default is defined in _getDefaultDialogAesthetics()
|
||||
aestheticsForStagePhases: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
authError: null,
|
||||
|
||||
// See _onUpdateStagePhase()
|
||||
uiaStage: null,
|
||||
uiaStagePhase: null,
|
||||
};
|
||||
|
||||
_getDefaultDialogAesthetics() {
|
||||
private getDefaultDialogAesthetics(): IDialogAesthetics {
|
||||
const ssoAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
|
@ -101,7 +120,7 @@ export default class InteractiveAuthDialog extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
_onAuthFinished = (success, result) => {
|
||||
private onAuthFinished = (success: boolean, result: Error): void => {
|
||||
if (success) {
|
||||
this.props.onFinished(true, result);
|
||||
} else {
|
||||
|
@ -115,19 +134,16 @@ export default class InteractiveAuthDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onUpdateStagePhase = (newStage, newPhase) => {
|
||||
private onUpdateStagePhase = (newStage: string | number, newPhase: string | number): void => {
|
||||
// We copy the stage and stage phase params into state for title selection in render()
|
||||
this.setState({ uiaStage: newStage, uiaStagePhase: newPhase });
|
||||
};
|
||||
|
||||
_onDismissClick = () => {
|
||||
private onDismissClick = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
public render(): JSX.Element {
|
||||
// Let's pick a title, body, and other params text that we'll show to the user. The order
|
||||
// is most specific first, so stagePhase > our props > defaults.
|
||||
|
||||
|
@ -135,7 +151,7 @@ export default class InteractiveAuthDialog extends React.Component {
|
|||
let body = this.state.authError ? null : this.props.body;
|
||||
let continueText = null;
|
||||
let continueKind = null;
|
||||
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics();
|
||||
const dialogAesthetics = this.props.aestheticsForStagePhases || this.getDefaultDialogAesthetics();
|
||||
if (!this.state.authError && dialogAesthetics) {
|
||||
if (dialogAesthetics[this.state.uiaStage]) {
|
||||
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
|
||||
|
@ -152,9 +168,9 @@ export default class InteractiveAuthDialog extends React.Component {
|
|||
<div id='mx_Dialog_content'>
|
||||
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||
<br />
|
||||
<AccessibleButton onClick={this._onDismissClick}
|
||||
<AccessibleButton onClick={this.onDismissClick}
|
||||
className="mx_GeneralButton"
|
||||
autoFocus="true"
|
||||
autoFocus={true}
|
||||
>
|
||||
{ _t("Dismiss") }
|
||||
</AccessibleButton>
|
||||
|
@ -165,12 +181,11 @@ export default class InteractiveAuthDialog extends React.Component {
|
|||
<div id='mx_Dialog_content'>
|
||||
{ body }
|
||||
<InteractiveAuth
|
||||
ref={this._collectInteractiveAuth}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authData={this.props.authData}
|
||||
makeRequest={this.props.makeRequest}
|
||||
onAuthFinished={this._onAuthFinished}
|
||||
onStagePhaseChange={this._onUpdateStagePhase}
|
||||
onAuthFinished={this.onAuthFinished}
|
||||
onStagePhaseChange={this.onUpdateStagePhase}
|
||||
continueText={continueText}
|
||||
continueKind={continueKind}
|
||||
/>
|
|
@ -73,6 +73,8 @@ import BaseDialog from "./BaseDialog";
|
|||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
|
@ -775,7 +777,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
invitedUsers.push(addr);
|
||||
}
|
||||
}
|
||||
console.log("Sharing history with", invitedUsers);
|
||||
logger.log("Sharing history with", invitedUsers);
|
||||
cli.sendSharedHistoryKeys(
|
||||
this.props.roomId, invitedUsers,
|
||||
);
|
||||
|
|
|
@ -15,20 +15,29 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
export default function KeySignatureUploadFailedDialog({
|
||||
interface IProps extends IDialogProps {
|
||||
failures: Record<string, Record<string, {
|
||||
errcode: string;
|
||||
error: string;
|
||||
}>>;
|
||||
source: string;
|
||||
continuation: () => void;
|
||||
}
|
||||
|
||||
const KeySignatureUploadFailedDialog: React.FC<IProps> = ({
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
}) => {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const [retry, setRetry] = useState(RETRIES);
|
||||
const [cancelled, setCancelled] = useState(false);
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
|
@ -107,4 +116,6 @@ export default function KeySignatureUploadFailedDialog({
|
|||
{ body }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default KeySignatureUploadFailedDialog;
|
|
@ -19,8 +19,13 @@ import React from 'react';
|
|||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
export default (props) => {
|
||||
interface IProps extends IDialogProps {
|
||||
host: string;
|
||||
}
|
||||
|
||||
const LazyLoadingDisabledDialog: React.FC<IProps> = (props) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const description1 = _t(
|
||||
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " +
|
||||
|
@ -49,3 +54,5 @@ export default (props) => {
|
|||
onFinished={props.onFinished}
|
||||
/>);
|
||||
};
|
||||
|
||||
export default LazyLoadingDisabledDialog;
|
|
@ -19,8 +19,11 @@ import React from 'react';
|
|||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
export default (props) => {
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
const LazyLoadingResyncDialog: React.FC<IProps> = (props) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const description =
|
||||
_t(
|
||||
|
@ -38,3 +41,5 @@ export default (props) => {
|
|||
onFinished={props.onFinished}
|
||||
/>);
|
||||
};
|
||||
|
||||
export default LazyLoadingResyncDialog;
|
|
@ -97,13 +97,13 @@ const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave
|
|||
definitions={[
|
||||
{
|
||||
value: RoomsToLeave.None,
|
||||
label: _t("Don't leave any"),
|
||||
label: _t("Don't leave any rooms"),
|
||||
}, {
|
||||
value: RoomsToLeave.All,
|
||||
label: _t("Leave all rooms and spaces"),
|
||||
label: _t("Leave all rooms"),
|
||||
}, {
|
||||
value: RoomsToLeave.Specific,
|
||||
label: _t("Leave specific rooms and spaces"),
|
||||
label: _t("Leave some rooms"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
@ -171,11 +171,13 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
|||
>
|
||||
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
|
||||
<p>
|
||||
{ _t("Are you sure you want to leave <spaceName/>?", {}, {
|
||||
{ _t("You are about to leave <spaceName/>.", {}, {
|
||||
spaceName: () => <b>{ space.name }</b>,
|
||||
}) }
|
||||
|
||||
{ rejoinWarning }
|
||||
{ rejoinWarning && (<> </>) }
|
||||
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
|
||||
</p>
|
||||
|
||||
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
||||
|
|
|
@ -25,6 +25,8 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
@ -68,7 +70,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
backupInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
logger.log("Unable to fetch key backup status", e);
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
|
|
|
@ -19,37 +19,31 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
userId: string;
|
||||
device: DeviceInfo;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
|
||||
export default class ManualDeviceKeyVerificationDialog extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onLegacyFinished = (confirm) => {
|
||||
export default class ManualDeviceKeyVerificationDialog extends React.Component<IProps> {
|
||||
private onLegacyFinished = (confirm: boolean): void => {
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.deviceId, true,
|
||||
);
|
||||
}
|
||||
this.props.onFinished(confirm);
|
||||
}
|
||||
|
||||
render() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
let text;
|
||||
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
|
||||
text = _t("Confirm by comparing the following with the User Settings in your other session:");
|
||||
|
@ -81,7 +75,7 @@ export default class ManualDeviceKeyVerificationDialog extends React.Component {
|
|||
title={_t("Verify session")}
|
||||
description={body}
|
||||
button={_t("Verify session")}
|
||||
onFinished={this._onLegacyFinished}
|
||||
onFinished={this.onLegacyFinished}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -15,21 +15,39 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
import { wantsDateSeparator } from '../../../DateUtils';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import ScrollPanel from "../../structures/ScrollPanel";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import EditHistoryMessage from "../messages/EditHistoryMessage";
|
||||
import DateSeparator from "../messages/DateSeparator";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
originalEvent: MatrixEvent;
|
||||
error: {
|
||||
errcode: string;
|
||||
};
|
||||
events: MatrixEvent[];
|
||||
nextBatch: string;
|
||||
isLoading: boolean;
|
||||
isTwelveHour: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
|
||||
export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
export default class MessageEditHistoryDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
originalEvent: null,
|
||||
|
@ -41,7 +59,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
loadMoreEdits = async (backwards) => {
|
||||
private loadMoreEdits = async (backwards?: boolean): Promise<boolean> => {
|
||||
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
|
||||
// bail out on backwards as we only paginate in one direction
|
||||
return false;
|
||||
|
@ -50,13 +68,13 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
const roomId = this.props.mxEvent.getRoomId();
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const { resolve, reject, promise } = defer<boolean>();
|
||||
let result;
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;});
|
||||
|
||||
try {
|
||||
result = await client.relations(
|
||||
roomId, eventId, "m.replace", "m.room.message", opts);
|
||||
roomId, eventId, RelationType.Replace, EventType.RoomMessage, opts);
|
||||
} catch (error) {
|
||||
// log if the server returned an error
|
||||
if (error.errcode) {
|
||||
|
@ -67,7 +85,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
const newEvents = result.events;
|
||||
this._locallyRedactEventsIfNeeded(newEvents);
|
||||
this.locallyRedactEventsIfNeeded(newEvents);
|
||||
this.setState({
|
||||
originalEvent: this.state.originalEvent || result.originalEvent,
|
||||
events: this.state.events.concat(newEvents),
|
||||
|
@ -78,9 +96,9 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
resolve(hasMoreResults);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
};
|
||||
|
||||
_locallyRedactEventsIfNeeded(newEvents) {
|
||||
private locallyRedactEventsIfNeeded(newEvents: MatrixEvent[]): void {
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
|
@ -95,13 +113,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.loadMoreEdits();
|
||||
}
|
||||
|
||||
_renderEdits() {
|
||||
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
private renderEdits(): JSX.Element[] {
|
||||
const nodes = [];
|
||||
let lastEvent;
|
||||
let allEvents = this.state.events;
|
||||
|
@ -128,7 +144,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
return nodes;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
const { error } = this.state;
|
||||
|
@ -149,20 +165,17 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
</p>);
|
||||
}
|
||||
} else if (this.state.isLoading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = (<ScrollPanel
|
||||
className="mx_MessageEditHistoryDialog_scrollPanel"
|
||||
onFillRequest={this.loadMoreEdits}
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits">{ this._renderEdits() }</ul>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits">{ this.renderEdits() }</ul>
|
||||
</ScrollPanel>);
|
||||
}
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_MessageEditHistoryDialog'
|
|
@ -16,29 +16,30 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from "classnames";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
export default class QuestionDialog extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
extraButtons: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
buttonDisabled: PropTypes.bool,
|
||||
danger: PropTypes.bool,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
|
||||
fixedWidth: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
interface IProps extends IDialogProps {
|
||||
title?: string;
|
||||
description?: React.ReactNode;
|
||||
extraButtons?: React.ReactNode;
|
||||
button?: string;
|
||||
buttonDisabled?: boolean;
|
||||
danger?: boolean;
|
||||
focus?: boolean;
|
||||
headerImage?: string;
|
||||
quitOnly?: boolean; // quitOnly doesn't show the cancel button just the quit [x].
|
||||
fixedWidth?: boolean;
|
||||
className?: string;
|
||||
hasCancelButton?: boolean;
|
||||
cancelButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
export default class QuestionDialog extends React.Component<IProps> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
title: "",
|
||||
description: "",
|
||||
extraButtons: null,
|
||||
|
@ -48,17 +49,19 @@ export default class QuestionDialog extends React.Component {
|
|||
quitOnly: false,
|
||||
};
|
||||
|
||||
onOk = () => {
|
||||
private onOk = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
// Converting these to imports breaks wrench tests
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let primaryButtonClass = "";
|
||||
if (this.props.danger) {
|
||||
primaryButtonClass = "danger";
|
|
@ -17,27 +17,27 @@ 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';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import BugReportDialog from "./BugReportDialog";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
error: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
|
||||
export default class SessionRestoreErrorDialog extends React.Component {
|
||||
static propTypes = {
|
||||
error: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_sendBugReport = () => {
|
||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||
export default class SessionRestoreErrorDialog extends React.Component<IProps> {
|
||||
private sendBugReport = (): void => {
|
||||
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
|
||||
};
|
||||
|
||||
_onClearStorageClick = () => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
private onClearStorageClick = (): void => {
|
||||
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
|
||||
title: _t("Sign out"),
|
||||
description:
|
||||
|
@ -48,19 +48,17 @@ export default class SessionRestoreErrorDialog extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onRefreshClick = () => {
|
||||
private onRefreshClick = (): void => {
|
||||
// Is this likely to help? Probably not, but giving only one button
|
||||
// that clears your storage seems awful.
|
||||
window.location.reload(true);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
const clearStorageButton = (
|
||||
<button onClick={this._onClearStorageClick} className="danger">
|
||||
<button onClick={this.onClearStorageClick} className="danger">
|
||||
{ _t("Clear Storage and Sign Out") }
|
||||
</button>
|
||||
);
|
||||
|
@ -68,7 +66,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
|
|||
let dialogButtons;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
dialogButtons = <DialogButtons primaryButton={_t("Send Logs")}
|
||||
onPrimaryButtonClick={this._sendBugReport}
|
||||
onPrimaryButtonClick={this.sendBugReport}
|
||||
focus={true}
|
||||
hasCancel={false}
|
||||
>
|
||||
|
@ -76,7 +74,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
|
|||
</DialogButtons>;
|
||||
} else {
|
||||
dialogButtons = <DialogButtons primaryButton={_t("Refresh")}
|
||||
onPrimaryButtonClick={this._onRefreshClick}
|
||||
onPrimaryButtonClick={this.onRefreshClick}
|
||||
focus={true}
|
||||
hasCancel={false}
|
||||
>
|
|
@ -16,13 +16,26 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import * as Email from '../../../email';
|
||||
import AddThreepid from '../../../AddThreepid';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import EditableText from "../elements/EditableText";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
emailAddress: string;
|
||||
emailBusy: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
* Prompt the user to set an email address.
|
||||
|
@ -30,26 +43,25 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
* On success, `onFinished(true)` is called.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.SetEmailDialog")
|
||||
export default class SetEmailDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class SetEmailDialog extends React.Component<IProps, IState> {
|
||||
private addThreepid: AddThreepid;
|
||||
|
||||
state = {
|
||||
emailAddress: '',
|
||||
emailBusy: false,
|
||||
};
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
onEmailAddressChanged = value => {
|
||||
this.state = {
|
||||
emailAddress: '',
|
||||
emailBusy: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onEmailAddressChanged = (value: string): void => {
|
||||
this.setState({
|
||||
emailAddress: value,
|
||||
});
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
private onSubmit = (): void => {
|
||||
const emailAddress = this.state.emailAddress;
|
||||
if (!Email.looksValid(emailAddress)) {
|
||||
Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, {
|
||||
|
@ -58,8 +70,8 @@ export default class SetEmailDialog extends React.Component {
|
|||
});
|
||||
return;
|
||||
}
|
||||
this._addThreepid = new AddThreepid();
|
||||
this._addThreepid.addEmailAddress(emailAddress).then(() => {
|
||||
this.addThreepid = new AddThreepid();
|
||||
this.addThreepid.addEmailAddress(emailAddress).then(() => {
|
||||
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||
title: _t("Verification Pending"),
|
||||
description: _t(
|
||||
|
@ -80,11 +92,11 @@ export default class SetEmailDialog extends React.Component {
|
|||
this.setState({ emailBusy: true });
|
||||
};
|
||||
|
||||
onCancelled = () => {
|
||||
private onCancelled = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
onEmailDialogFinished = ok => {
|
||||
private onEmailDialogFinished = (ok: boolean): void => {
|
||||
if (ok) {
|
||||
this.verifyEmailAddress();
|
||||
} else {
|
||||
|
@ -92,13 +104,12 @@ export default class SetEmailDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
verifyEmailAddress() {
|
||||
this._addThreepid.checkEmailLinkClicked().then(() => {
|
||||
private verifyEmailAddress(): void {
|
||||
this.addThreepid.checkEmailLinkClicked().then(() => {
|
||||
this.props.onFinished(true);
|
||||
}, (err) => {
|
||||
this.setState({ emailBusy: false });
|
||||
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const message = _t("Unable to verify email address.") + " " +
|
||||
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
|
||||
Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, {
|
||||
|
@ -108,7 +119,6 @@ export default class SetEmailDialog extends React.Component {
|
|||
onFinished: this.onEmailDialogFinished,
|
||||
});
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify email address: " + err);
|
||||
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
|
||||
title: _t("Unable to verify email address."),
|
||||
|
@ -118,15 +128,10 @@ export default class SetEmailDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
|
||||
public render(): JSX.Element {
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
initialValue={this.state.emailAddress}
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
autoFocus="true"
|
||||
placeholder={_t("Email address")}
|
||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||
blurToCancel={false}
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { CommandCategories, Commands } from "../../../SlashCommands";
|
||||
import * as sdk from "../../../index";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
|
||||
export default ({ onFinished }) => {
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
const SlashCommandHelpDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||
const categories = {};
|
||||
Commands.forEach(cmd => {
|
||||
if (!cmd.isEnabled()) return;
|
||||
|
@ -62,3 +63,5 @@ export default ({ onFinished }) => {
|
|||
hasCloseButton={true}
|
||||
onFinished={onFinished} />;
|
||||
};
|
||||
|
||||
export default SlashCommandHelpDialog;
|
|
@ -15,40 +15,36 @@ 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';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import BugReportDialog from "./BugReportDialog";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps { }
|
||||
|
||||
@replaceableComponent("views.dialogs.StorageEvictedDialog")
|
||||
export default class StorageEvictedDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_sendBugReport = ev => {
|
||||
export default class StorageEvictedDialog extends React.Component<IProps> {
|
||||
private sendBugReport = (ev: React.MouseEvent): void => {
|
||||
ev.preventDefault();
|
||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||
Modal.createTrackedDialog('Storage evicted', 'Send Bug Report Dialog', BugReportDialog, {});
|
||||
};
|
||||
|
||||
_onSignOutClick = () => {
|
||||
private onSignOutClick = (): void => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
public render(): JSX.Element {
|
||||
let logRequest;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
logRequest = _t(
|
||||
"To help us prevent this in future, please <a>send us logs</a>.",
|
||||
{},
|
||||
{
|
||||
a: text => <a href="#" onClick={this._sendBugReport}>{ text }</a>,
|
||||
a: text => <a href="#" onClick={this.sendBugReport}>{ text }</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -73,7 +69,7 @@ export default class StorageEvictedDialog extends React.Component {
|
|||
) } { logRequest }</p>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Sign out")}
|
||||
onPrimaryButtonClick={this._onSignOutClick}
|
||||
onPrimaryButtonClick={this.onSignOutClick}
|
||||
focus={true}
|
||||
hasCancel={false}
|
||||
/>
|
|
@ -15,42 +15,47 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as sdk from '../../../index';
|
||||
import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
|
||||
import classNames from 'classnames';
|
||||
import * as ScalarMessaging from "../../../ScalarMessaging";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
|
||||
import ScalarAuthClient from "../../../ScalarAuthClient";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import IntegrationManager from "../settings/IntegrationManager";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
/**
|
||||
* Optional room where the integration manager should be open to
|
||||
*/
|
||||
room?: Room;
|
||||
|
||||
/**
|
||||
* Optional screen to open on the integration manager
|
||||
*/
|
||||
screen?: string;
|
||||
|
||||
/**
|
||||
* Optional integration ID to open in the integration manager
|
||||
*/
|
||||
integrationId?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
managers: IntegrationManagerInstance[];
|
||||
busy: boolean;
|
||||
currentIndex: number;
|
||||
currentConnected: boolean;
|
||||
currentLoading: boolean;
|
||||
currentScalarClient: ScalarAuthClient;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
|
||||
export default class TabbedIntegrationManagerDialog extends React.Component {
|
||||
static propTypes = {
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
/**
|
||||
* Optional room where the integration manager should be open to
|
||||
*/
|
||||
room: PropTypes.instanceOf(Room),
|
||||
|
||||
/**
|
||||
* Optional screen to open on the integration manager
|
||||
*/
|
||||
screen: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Optional integration ID to open in the integration manager
|
||||
*/
|
||||
integrationId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
export default class TabbedIntegrationManagerDialog extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -63,11 +68,11 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.openManager(0, true);
|
||||
}
|
||||
|
||||
openManager = async (i, force = false) => {
|
||||
private openManager = async (i: number, force = false): Promise<void> => {
|
||||
if (i === this.state.currentIndex && !force) return;
|
||||
|
||||
const manager = this.state.managers[i];
|
||||
|
@ -120,8 +125,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_renderTabs() {
|
||||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
private renderTabs(): JSX.Element[] {
|
||||
return this.state.managers.map((m, i) => {
|
||||
const classes = classNames({
|
||||
'mx_TabbedIntegrationManagerDialog_tab': true,
|
||||
|
@ -140,8 +144,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_renderTab() {
|
||||
const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
|
||||
public renderTab(): JSX.Element {
|
||||
let uiUrl = null;
|
||||
if (this.state.currentScalarClient) {
|
||||
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
|
||||
|
@ -151,7 +154,6 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
return <IntegrationManager
|
||||
configured={true}
|
||||
loading={this.state.currentLoading}
|
||||
connected={this.state.currentConnected}
|
||||
url={uiUrl}
|
||||
|
@ -159,14 +161,14 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className='mx_TabbedIntegrationManagerDialog_container'>
|
||||
<div className='mx_TabbedIntegrationManagerDialog_tabs'>
|
||||
{ this._renderTabs() }
|
||||
{ this.renderTabs() }
|
||||
</div>
|
||||
<div className='mx_TabbedIntegrationManagerDialog_currentManager'>
|
||||
{ this._renderTab() }
|
||||
{ this.renderTab() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -14,33 +14,39 @@ 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 * as sdk from '../../../index';
|
||||
import React, { ChangeEvent, createRef } from 'react';
|
||||
import Field from "../elements/Field";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IFieldState, IValidationResult } from "../elements/Validation";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
title?: string;
|
||||
description?: React.ReactNode;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
button?: string;
|
||||
busyMessage?: string; // pass _td string
|
||||
focus?: boolean;
|
||||
hasCancel?: boolean;
|
||||
validator?: (fieldState: IFieldState) => IValidationResult; // result of withValidation
|
||||
fixedWidth?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
busy: boolean;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.TextInputDialog")
|
||||
export default class TextInputDialog extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
busyMessage: PropTypes.string, // pass _td string
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
hasCancel: PropTypes.bool,
|
||||
validator: PropTypes.func, // result of withValidation
|
||||
fixedWidth: PropTypes.bool,
|
||||
};
|
||||
export default class TextInputDialog extends React.Component<IProps, IState> {
|
||||
private field = createRef<Field>();
|
||||
|
||||
static defaultProps = {
|
||||
public static defaultProps = {
|
||||
title: "",
|
||||
value: "",
|
||||
description: "",
|
||||
|
@ -49,11 +55,9 @@ export default class TextInputDialog extends React.Component {
|
|||
hasCancel: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._field = createRef();
|
||||
|
||||
this.state = {
|
||||
value: this.props.value,
|
||||
busy: false,
|
||||
|
@ -61,23 +65,23 @@ export default class TextInputDialog extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
// this._field.current.value = this.props.value;
|
||||
this._field.current.focus();
|
||||
this.field.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onOk = async ev => {
|
||||
private onOk = async (ev: React.FormEvent): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
if (this.props.validator) {
|
||||
this.setState({ busy: true });
|
||||
await this._field.current.validate({ allowEmpty: false });
|
||||
await this.field.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!this._field.current.state.valid) {
|
||||
this._field.current.focus();
|
||||
this._field.current.validate({ allowEmpty: false, focused: true });
|
||||
if (!this.field.current.state.valid) {
|
||||
this.field.current.focus();
|
||||
this.field.current.validate({ allowEmpty: false, focused: true });
|
||||
this.setState({ busy: false });
|
||||
return;
|
||||
}
|
||||
|
@ -85,17 +89,17 @@ export default class TextInputDialog extends React.Component {
|
|||
this.props.onFinished(true, this.state.value);
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
onChange = ev => {
|
||||
private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
value: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onValidate = async fieldState => {
|
||||
private onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.props.validator(fieldState);
|
||||
this.setState({
|
||||
valid: result.valid,
|
||||
|
@ -103,9 +107,7 @@ export default class TextInputDialog extends React.Component {
|
|||
return result;
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_TextInputDialog"
|
||||
|
@ -121,13 +123,12 @@ export default class TextInputDialog extends React.Component {
|
|||
<div>
|
||||
<Field
|
||||
className="mx_TextInputDialog_input"
|
||||
ref={this._field}
|
||||
ref={this.field}
|
||||
type="text"
|
||||
label={this.props.placeholder}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onValidate={this.props.validator ? this.onValidate : undefined}
|
||||
size="64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
|
@ -19,7 +19,7 @@ import { User } from "matrix-js-sdk/src/models/user";
|
|||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import E2EIcon, { E2EState } from "../rooms/E2EIcon";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
@ -47,7 +47,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
|
|||
onFinished={onFinished}
|
||||
className="mx_UntrustedDeviceDialog"
|
||||
title={<>
|
||||
<E2EIcon status="warning" size={24} hideTooltip={true} />
|
||||
<E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} />
|
||||
{ _t("Not Trusted") }
|
||||
</>}
|
||||
>
|
||||
|
|
|
@ -17,11 +17,18 @@ limitations under the License.
|
|||
import filesize from 'filesize';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
badFiles: File[];
|
||||
totalFiles: number;
|
||||
contentMessages: ContentMessages;
|
||||
}
|
||||
|
||||
/*
|
||||
* Tells the user about files we know cannot be uploaded before we even try uploading
|
||||
|
@ -29,26 +36,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
* the size of the file.
|
||||
*/
|
||||
@replaceableComponent("views.dialogs.UploadFailureDialog")
|
||||
export default class UploadFailureDialog extends React.Component {
|
||||
static propTypes = {
|
||||
badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalFiles: PropTypes.number.isRequired,
|
||||
contentMessages: PropTypes.instanceOf(ContentMessages).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
export default class UploadFailureDialog extends React.Component<IProps> {
|
||||
private onCancelClick = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
};
|
||||
|
||||
_onUploadClick = () => {
|
||||
private onUploadClick = (): void => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
let message;
|
||||
let preview;
|
||||
let buttons;
|
||||
|
@ -65,7 +62,7 @@ export default class UploadFailureDialog extends React.Component {
|
|||
);
|
||||
buttons = <DialogButtons primaryButton={_t('OK')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onCancelClick}
|
||||
onPrimaryButtonClick={this.onCancelClick}
|
||||
focus={true}
|
||||
/>;
|
||||
} else if (this.props.totalFiles === this.props.badFiles.length) {
|
||||
|
@ -80,7 +77,7 @@ export default class UploadFailureDialog extends React.Component {
|
|||
);
|
||||
buttons = <DialogButtons primaryButton={_t('OK')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onCancelClick}
|
||||
onPrimaryButtonClick={this.onCancelClick}
|
||||
focus={true}
|
||||
/>;
|
||||
} else {
|
||||
|
@ -96,17 +93,17 @@ export default class UploadFailureDialog extends React.Component {
|
|||
const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
|
||||
buttons = <DialogButtons
|
||||
primaryButton={_t('Upload %(count)s other files', { count: howManyOthers })}
|
||||
onPrimaryButtonClick={this._onUploadClick}
|
||||
onPrimaryButtonClick={this.onUploadClick}
|
||||
hasCancel={true}
|
||||
cancelButton={_t("Cancel All")}
|
||||
onCancel={this._onCancelClick}
|
||||
onCancel={this.onCancelClick}
|
||||
focus={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_UploadFailureDialog'
|
||||
onFinished={this._onCancelClick}
|
||||
onFinished={this.onCancelClick}
|
||||
title={_t("Upload Error")}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
|
@ -33,6 +33,7 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
|
|||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
||||
export enum UserTab {
|
||||
General = "USER_GENERAL_TAB",
|
||||
|
@ -47,8 +48,7 @@ export enum UserTab {
|
|||
Help = "USER_HELP_TAB",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
interface IProps extends IDialogProps {
|
||||
initialTabId?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ import { IDialogProps } from "./IDialogProps";
|
|||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
widget: Widget;
|
||||
widgetKind: WidgetKind;
|
||||
|
@ -45,17 +47,17 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
|
|||
};
|
||||
}
|
||||
|
||||
private onAllow = () => {
|
||||
private onAllow = (): void => {
|
||||
this.onPermissionSelection(true);
|
||||
};
|
||||
|
||||
private onDeny = () => {
|
||||
private onDeny = (): void => {
|
||||
this.onPermissionSelection(false);
|
||||
};
|
||||
|
||||
private onPermissionSelection(allowed: boolean) {
|
||||
private onPermissionSelection(allowed: boolean): void {
|
||||
if (this.state.rememberSelection) {
|
||||
console.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
|
||||
logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
|
||||
|
||||
WidgetPermissionStore.instance.setOIDCState(
|
||||
this.props.widget, this.props.widgetKind, this.props.inRoomId,
|
||||
|
@ -66,11 +68,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
|
|||
this.props.onFinished(allowed);
|
||||
}
|
||||
|
||||
private onRememberSelectionChange = (newVal: boolean) => {
|
||||
private onRememberSelectionChange = (newVal: boolean): void => {
|
||||
this.setState({ rememberSelection: newVal });
|
||||
};
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_WidgetOpenIDPermissionsDialog'
|
||||
|
|
|
@ -28,6 +28,8 @@ import Spinner from '../../elements/Spinner';
|
|||
import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
interface IProps {
|
||||
accountPassword?: string;
|
||||
tokenLogin?: boolean;
|
||||
|
@ -77,10 +79,10 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
|
|||
// We should never get here: the server should always require
|
||||
// UI auth to upload device signing keys. If we do, we upload
|
||||
// no keys which would be a no-op.
|
||||
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||
} catch (error) {
|
||||
if (!error.data || !error.data.flows) {
|
||||
console.log("uploadDeviceSigningKeys advertised no flows!");
|
||||
logger.log("uploadDeviceSigningKeys advertised no flows!");
|
||||
return;
|
||||
}
|
||||
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
|
||||
|
|
|
@ -16,30 +16,64 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../../index';
|
||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../SecurityManager';
|
||||
import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
|
||||
import * as sdk from '../../../../index';
|
||||
import { IDialogProps } from "../IDialogProps";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||
const RESTORE_TYPE_SECRET_STORAGE = 2;
|
||||
enum RestoreType {
|
||||
Passphrase = "passphrase",
|
||||
RecoveryKey = "recovery_key",
|
||||
SecretStorage = "secret_storage"
|
||||
}
|
||||
|
||||
enum ProgressState {
|
||||
PreFetch = "prefetch",
|
||||
Fetch = "fetch",
|
||||
LoadKeys = "load_keys",
|
||||
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
// if false, will close the dialog as soon as the restore completes succesfully
|
||||
// default: true
|
||||
showSummary?: boolean;
|
||||
// If specified, gather the key from the user but then call the function with the backup
|
||||
// key rather than actually (necessarily) restoring the backup.
|
||||
keyCallback?: (key: Uint8Array) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
backupInfo: IKeyBackupInfo;
|
||||
backupKeyStored: Record<string, ISecretStorageKeyInfo>;
|
||||
loading: boolean;
|
||||
loadError: string;
|
||||
restoreError: {
|
||||
errcode: string;
|
||||
};
|
||||
recoveryKey: string;
|
||||
recoverInfo: IKeyBackupRestoreResult;
|
||||
recoveryKeyValid: boolean;
|
||||
forceRecoveryKey: boolean;
|
||||
passPhrase: string;
|
||||
restoreType: RestoreType;
|
||||
progress: {
|
||||
stage: ProgressState;
|
||||
total?: number;
|
||||
successes?: number;
|
||||
failures?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
||||
*/
|
||||
export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// if false, will close the dialog as soon as the restore completes succesfully
|
||||
// default: true
|
||||
showSummary: PropTypes.bool,
|
||||
// If specified, gather the key from the user but then call the function with the backup
|
||||
// key rather than actually (necessarily) restoring the backup.
|
||||
keyCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
export default class RestoreKeyBackupDialog extends React.PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
showSummary: true,
|
||||
};
|
||||
|
@ -58,58 +92,58 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
forceRecoveryKey: false,
|
||||
passPhrase: '',
|
||||
restoreType: null,
|
||||
progress: { stage: "prefetch" },
|
||||
progress: { stage: ProgressState.PreFetch },
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._loadBackupStatus();
|
||||
public componentDidMount(): void {
|
||||
this.loadBackupStatus();
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
};
|
||||
|
||||
_onDone = () => {
|
||||
private onDone = (): void => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
_onUseRecoveryKeyClick = () => {
|
||||
private onUseRecoveryKeyClick = (): void => {
|
||||
this.setState({
|
||||
forceRecoveryKey: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_progressCallback = (data) => {
|
||||
private progressCallback = (data): void => {
|
||||
this.setState({
|
||||
progress: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onResetRecoveryClick = () => {
|
||||
private onResetRecoveryClick = (): void => {
|
||||
this.props.onFinished(false);
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
}
|
||||
accessSecretStorage(async () => {}, /* forceReset = */ true);
|
||||
};
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
private onRecoveryKeyChange = (e): void => {
|
||||
this.setState({
|
||||
recoveryKey: e.target.value,
|
||||
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseNext = async () => {
|
||||
private onPassPhraseNext = async (): Promise<void> => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
restoreType: RESTORE_TYPE_PASSPHRASE,
|
||||
restoreType: RestoreType.Passphrase,
|
||||
});
|
||||
try {
|
||||
// We do still restore the key backup: we must ensure that the key backup key
|
||||
// is the right one and restoring it is currently the only way we can do this.
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
|
||||
{ progressCallback: this._progressCallback },
|
||||
{ progressCallback: this.progressCallback },
|
||||
);
|
||||
if (this.props.keyCallback) {
|
||||
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
|
||||
|
@ -127,26 +161,26 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
recoverInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
logger.log("Error restoring backup", e);
|
||||
this.setState({
|
||||
loading: false,
|
||||
restoreError: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onRecoveryKeyNext = async () => {
|
||||
private onRecoveryKeyNext = async (): Promise<void> => {
|
||||
if (!this.state.recoveryKeyValid) return;
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
restoreType: RESTORE_TYPE_RECOVERYKEY,
|
||||
restoreType: RestoreType.RecoveryKey,
|
||||
});
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
|
||||
{ progressCallback: this._progressCallback },
|
||||
{ progressCallback: this.progressCallback },
|
||||
);
|
||||
if (this.props.keyCallback) {
|
||||
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
|
||||
|
@ -161,40 +195,39 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
recoverInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
logger.log("Error restoring backup", e);
|
||||
this.setState({
|
||||
loading: false,
|
||||
restoreError: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseChange = (e) => {
|
||||
private onPassPhraseChange = (e): void => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async _restoreWithSecretStorage() {
|
||||
private async restoreWithSecretStorage(): Promise<void> {
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
restoreType: RESTORE_TYPE_SECRET_STORAGE,
|
||||
restoreType: RestoreType.SecretStorage,
|
||||
});
|
||||
try {
|
||||
// `accessSecretStorage` may prompt for storage access as needed.
|
||||
const recoverInfo = await accessSecretStorage(async () => {
|
||||
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||
await accessSecretStorage(async () => {
|
||||
await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||
this.state.backupInfo, undefined, undefined,
|
||||
{ progressCallback: this._progressCallback },
|
||||
{ progressCallback: this.progressCallback },
|
||||
);
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
recoverInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
logger.log("Error restoring backup", e);
|
||||
this.setState({
|
||||
restoreError: e,
|
||||
loading: false,
|
||||
|
@ -202,26 +235,26 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
async _restoreWithCachedKey(backupInfo) {
|
||||
private async restoreWithCachedKey(backupInfo): Promise<boolean> {
|
||||
if (!backupInfo) return false;
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache(
|
||||
undefined, /* targetRoomId */
|
||||
undefined, /* targetSessionId */
|
||||
backupInfo,
|
||||
{ progressCallback: this._progressCallback },
|
||||
{ progressCallback: this.progressCallback },
|
||||
);
|
||||
this.setState({
|
||||
recoverInfo,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("restoreWithCachedKey failed:", e);
|
||||
logger.log("restoreWithCachedKey failed:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
private async loadBackupStatus(): Promise<void> {
|
||||
this.setState({
|
||||
loading: true,
|
||||
loadError: null,
|
||||
|
@ -230,15 +263,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
const cli = MatrixClientPeg.get();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
const has4S = await cli.hasSecretStorageKey();
|
||||
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored();
|
||||
const backupKeyStored = has4S && (await cli.isKeyBackupKeyStored());
|
||||
this.setState({
|
||||
backupInfo,
|
||||
backupKeyStored,
|
||||
});
|
||||
|
||||
const gotCache = await this._restoreWithCachedKey(backupInfo);
|
||||
const gotCache = await this.restoreWithCachedKey(backupInfo);
|
||||
if (gotCache) {
|
||||
console.log("RestoreKeyBackupDialog: found cached backup key");
|
||||
logger.log("RestoreKeyBackupDialog: found cached backup key");
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
|
@ -247,7 +280,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
|
||||
// If the backup key is stored, we can proceed directly to restore.
|
||||
if (backupKeyStored) {
|
||||
return this._restoreWithSecretStorage();
|
||||
return this.restoreWithSecretStorage();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -255,7 +288,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
loading: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error loading backup status", e);
|
||||
logger.log("Error loading backup status", e);
|
||||
this.setState({
|
||||
loadError: e,
|
||||
loading: false,
|
||||
|
@ -263,7 +296,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
// FIXME: Making these into imports will break tests
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
|
@ -279,12 +315,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
if (this.state.loading) {
|
||||
title = _t("Restoring keys from backup");
|
||||
let details;
|
||||
if (this.state.progress.stage === "fetch") {
|
||||
if (this.state.progress.stage === ProgressState.Fetch) {
|
||||
details = _t("Fetching keys from server...");
|
||||
} else if (this.state.progress.stage === "load_keys") {
|
||||
} else if (this.state.progress.stage === ProgressState.LoadKeys) {
|
||||
const { total, successes, failures } = this.state.progress;
|
||||
details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures });
|
||||
} else if (this.state.progress.stage === "prefetch") {
|
||||
} else if (this.state.progress.stage === ProgressState.PreFetch) {
|
||||
details = _t("Fetching keys from server...");
|
||||
}
|
||||
content = <div>
|
||||
|
@ -296,7 +332,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
content = _t("Unable to load backup status");
|
||||
} else if (this.state.restoreError) {
|
||||
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
|
||||
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
|
||||
if (this.state.restoreType === RestoreType.RecoveryKey) {
|
||||
title = _t("Security Key mismatch");
|
||||
content = <div>
|
||||
<p>{ _t(
|
||||
|
@ -321,7 +357,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
title = _t("Error");
|
||||
content = _t("No backup found!");
|
||||
} else if (this.state.recoverInfo) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
title = _t("Keys restored");
|
||||
let failedToDecrypt;
|
||||
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
|
||||
|
@ -334,14 +369,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
<p>{ _t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported }) }</p>
|
||||
{ failedToDecrypt }
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
onPrimaryButtonClick={this.onDone}
|
||||
hasCancel={false}
|
||||
focus={true}
|
||||
/>
|
||||
</div>;
|
||||
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
title = _t("Enter Security Phrase");
|
||||
content = <div>
|
||||
<p>{ _t(
|
||||
|
@ -357,16 +390,16 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
<input type="password"
|
||||
className="mx_RestoreKeyBackupDialog_passPhraseInput"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onChange={this.onPassPhraseChange}
|
||||
value={this.state.passPhrase}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNext}
|
||||
onPrimaryButtonClick={this.onPassPhraseNext}
|
||||
primaryIsSubmit={true}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
focus={false}
|
||||
/>
|
||||
</form>
|
||||
|
@ -379,14 +412,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
button1: s => <AccessibleButton
|
||||
className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onUseRecoveryKeyClick}
|
||||
onClick={this.onUseRecoveryKeyClick}
|
||||
>
|
||||
{ s }
|
||||
</AccessibleButton>,
|
||||
button2: s => <AccessibleButton
|
||||
className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
onClick={this.onResetRecoveryClick}
|
||||
>
|
||||
{ s }
|
||||
</AccessibleButton>,
|
||||
|
@ -394,8 +427,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
</div>;
|
||||
} else {
|
||||
title = _t("Enter Security Key");
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let keyStatus;
|
||||
if (this.state.recoveryKey.length === 0) {
|
||||
|
@ -423,15 +454,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
|
||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
onChange={this.onRecoveryKeyChange}
|
||||
value={this.state.recoveryKey}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{ keyStatus }
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
||||
onPrimaryButtonClick={this.onRecoveryKeyNext}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
focus={false}
|
||||
primaryDisabled={!this.state.recoveryKeyValid}
|
||||
/>
|
||||
|
@ -443,7 +474,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
{
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
onClick={this.onResetRecoveryClick}
|
||||
>
|
||||
{ s }
|
||||
</AccessibleButton>,
|
|
@ -20,6 +20,7 @@ import BaseDialog from '../BaseDialog';
|
|||
import { _t } from '../../../../languageHandler';
|
||||
import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
import { IDialogProps } from "../IDialogProps";
|
||||
|
||||
function iconFromPhase(phase: Phase) {
|
||||
if (phase === Phase.Done) {
|
||||
|
@ -29,12 +30,9 @@ function iconFromPhase(phase: Phase) {
|
|||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {}
|
||||
interface IState {
|
||||
icon: Phase;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
|
||||
import url from 'url';
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -39,33 +38,97 @@ import { MatrixCapabilities } from "matrix-widget-api";
|
|||
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
|
||||
interface IProps {
|
||||
app: IApp;
|
||||
// If room is not specified then it is an account level widget
|
||||
// which bypasses permission prompts as it was added explicitly by that user
|
||||
room: Room;
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth?: boolean;
|
||||
// Optional. If set, renders a smaller view of the widget
|
||||
miniMode?: boolean;
|
||||
// UserId of the current user
|
||||
userId: string;
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: string;
|
||||
waitForIframeLoad: boolean;
|
||||
showMenubar?: boolean;
|
||||
// Optional onEditClickHandler (overrides default behaviour)
|
||||
onEditClick?: () => void;
|
||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||
onDeleteClick?: () => void;
|
||||
// Optionally hide the tile title
|
||||
showTitle?: boolean;
|
||||
// Optionally handle minimise button pointer events (default false)
|
||||
handleMinimisePointerEvents?: boolean;
|
||||
// Optionally hide the popout widget icon
|
||||
showPopout?: boolean;
|
||||
// Is this an instance of a user widget
|
||||
userWidget: boolean;
|
||||
// sets the pointer-events property on the iframe
|
||||
pointerEvents?: string;
|
||||
widgetPageTitle?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
initialising: boolean; // True while we are mangling the widget URL
|
||||
// True while the iframe content is loading
|
||||
loading: boolean;
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: boolean;
|
||||
error: Error;
|
||||
menuDisplayed: boolean;
|
||||
widgetPageTitle: string;
|
||||
}
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@replaceableComponent("views.elements.AppTile")
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
export default class AppTile extends React.Component<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
waitForIframeLoad: true,
|
||||
showMenubar: true,
|
||||
showTitle: true,
|
||||
showPopout: true,
|
||||
handleMinimisePointerEvents: false,
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
};
|
||||
|
||||
private contextMenuButton = createRef<any>();
|
||||
private iframe: HTMLIFrameElement; // ref to the iframe (callback style)
|
||||
private allowedWidgetsWatchRef: string;
|
||||
private persistKey: string;
|
||||
private sgWidget: StopGapWidget;
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = getPersistKey(this.props.app.id);
|
||||
this.persistKey = getPersistKey(this.props.app.id);
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
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;
|
||||
logger.log("Failed to construct widget", e);
|
||||
this.sgWidget = null;
|
||||
}
|
||||
this.iframe = null; // ref to the iframe (callback style)
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
this._contextMenuButton = createRef();
|
||||
this.state = this.getNewState(props);
|
||||
|
||||
this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
|
||||
this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
|
||||
}
|
||||
|
||||
// This is a function to make the impact of calling SettingsStore slightly less
|
||||
hasPermissionToLoad = (props) => {
|
||||
if (this._usingLocalWidget()) return true;
|
||||
private hasPermissionToLoad = (props: IProps): boolean => {
|
||||
if (this.usingLocalWidget()) return true;
|
||||
if (!props.room) return true; // user widgets always have permissions
|
||||
|
||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
||||
|
@ -81,34 +144,34 @@ export default class AppTile extends React.Component {
|
|||
* @param {Object} newProps The new properties of the component
|
||||
* @return {Object} Updated component state to be set with setState
|
||||
*/
|
||||
_getNewState(newProps) {
|
||||
private getNewState(newProps: IProps): IState {
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
// True while the iframe content is loading
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey),
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: this.hasPermissionToLoad(newProps),
|
||||
error: null,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
menuDisplayed: false,
|
||||
widgetPageTitle: this.props.widgetPageTitle,
|
||||
};
|
||||
}
|
||||
|
||||
onAllowedWidgetsChange = () => {
|
||||
private onAllowedWidgetsChange = (): void => {
|
||||
const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
|
||||
|
||||
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
|
||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
if (this._sgWidget) this._sgWidget.stop();
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this.persistKey);
|
||||
if (this.sgWidget) this.sgWidget.stop();
|
||||
}
|
||||
|
||||
this.setState({ hasPermissionToLoad });
|
||||
};
|
||||
|
||||
isMixedContent() {
|
||||
private isMixedContent(): boolean {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.app.url);
|
||||
const childContentProtocol = u.protocol;
|
||||
|
@ -120,69 +183,70 @@ export default class AppTile extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
// Only fetch IM token on mount if we're showing and have permission to load
|
||||
if (this._sgWidget && this.state.hasPermissionToLoad) {
|
||||
this._startWidget();
|
||||
if (this.sgWidget && this.state.hasPermissionToLoad) {
|
||||
this.startWidget();
|
||||
}
|
||||
|
||||
// Widget action listeners
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
// Widget action listeners
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
|
||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) {
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this.persistKey);
|
||||
}
|
||||
|
||||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
if (this.sgWidget) {
|
||||
this.sgWidget.stop();
|
||||
}
|
||||
|
||||
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef);
|
||||
SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
|
||||
}
|
||||
|
||||
_resetWidget(newProps) {
|
||||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
private resetWidget(newProps: IProps): void {
|
||||
if (this.sgWidget) {
|
||||
this.sgWidget.stop();
|
||||
}
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
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;
|
||||
logger.log("Failed to construct widget", e);
|
||||
this.sgWidget = null;
|
||||
}
|
||||
}
|
||||
|
||||
_startWidget() {
|
||||
this._sgWidget.prepare().then(() => {
|
||||
private startWidget(): void {
|
||||
this.sgWidget.prepare().then(() => {
|
||||
this.setState({ initialising: false });
|
||||
});
|
||||
}
|
||||
|
||||
_iframeRefChange = (ref) => {
|
||||
private iframeRefChange = (ref: HTMLIFrameElement): void => {
|
||||
this.iframe = ref;
|
||||
if (ref) {
|
||||
if (this._sgWidget) this._sgWidget.start(ref);
|
||||
if (this.sgWidget) this.sgWidget.start(ref);
|
||||
} else {
|
||||
this._resetWidget(this.props);
|
||||
this.resetWidget(this.props);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase
|
||||
if (nextProps.app.url !== this.props.app.url) {
|
||||
this._getNewState(nextProps);
|
||||
this.getNewState(nextProps);
|
||||
if (this.state.hasPermissionToLoad) {
|
||||
this._resetWidget(nextProps);
|
||||
this.resetWidget(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,7 +262,7 @@ export default class AppTile extends React.Component {
|
|||
* @private
|
||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||
*/
|
||||
async _endWidgetActions() { // widget migration dev note: async to maintain signature
|
||||
private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||
|
@ -217,27 +281,27 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this.persistKey);
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
|
||||
|
||||
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true });
|
||||
if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
|
||||
}
|
||||
|
||||
_onWidgetPrepared = () => {
|
||||
private onWidgetPrepared = (): void => {
|
||||
this.setState({ loading: false });
|
||||
};
|
||||
|
||||
_onWidgetReady = () => {
|
||||
private onWidgetReady = (): void => {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||
this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||
}
|
||||
};
|
||||
|
||||
_onAction = payload => {
|
||||
private onAction = (payload): void => {
|
||||
if (payload.widgetId === this.props.app.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||
dis.dispatch({ action: 'stickerpicker_close' });
|
||||
} else {
|
||||
|
@ -248,7 +312,7 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_grantWidgetPermission = () => {
|
||||
private grantWidgetPermission = (): void => {
|
||||
const roomId = this.props.room.roomId;
|
||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
|
@ -258,14 +322,14 @@ export default class AppTile extends React.Component {
|
|||
this.setState({ hasPermissionToLoad: true });
|
||||
|
||||
// Fetch a token for the integration manager, now that we're allowed to
|
||||
this._startWidget();
|
||||
this.startWidget();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
});
|
||||
};
|
||||
|
||||
formatAppTileName() {
|
||||
private formatAppTileName(): string {
|
||||
let appTileName = "No name";
|
||||
if (this.props.app.name && this.props.app.name.trim()) {
|
||||
appTileName = this.props.app.name.trim();
|
||||
|
@ -278,11 +342,11 @@ export default class AppTile extends React.Component {
|
|||
* actual widget URL
|
||||
* @returns {bool} true If using a local version of the widget
|
||||
*/
|
||||
_usingLocalWidget() {
|
||||
private usingLocalWidget(): boolean {
|
||||
return WidgetType.JITSI.matches(this.props.app.type);
|
||||
}
|
||||
|
||||
_getTileTitle() {
|
||||
private getTileTitle(): JSX.Element {
|
||||
const name = this.formatAppTileName();
|
||||
const titleSpacer = <span> - </span>;
|
||||
let title = '';
|
||||
|
@ -300,32 +364,32 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
// TODO replace with full screen interactions
|
||||
_onPopoutWidgetClick = () => {
|
||||
private onPopoutWidgetClick = (): void => {
|
||||
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
||||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
this._endWidgetActions().then(() => {
|
||||
this.endWidgetActions().then(() => {
|
||||
if (this.iframe) {
|
||||
// Reload iframe
|
||||
this.iframe.src = this._sgWidget.embedUrl;
|
||||
this.iframe.src = this.sgWidget.embedUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
||||
{ target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
||||
};
|
||||
|
||||
_onContextMenuClick = () => {
|
||||
private onContextMenuClick = (): void => {
|
||||
this.setState({ menuDisplayed: true });
|
||||
};
|
||||
|
||||
_closeContextMenu = () => {
|
||||
private closeContextMenu = (): void => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let appTileBody;
|
||||
|
||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||
|
@ -351,7 +415,7 @@ export default class AppTile extends React.Component {
|
|||
<Spinner message={_t("Loading...")} />
|
||||
</div>
|
||||
);
|
||||
if (this._sgWidget === null) {
|
||||
if (this.sgWidget === null) {
|
||||
appTileBody = (
|
||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||
<AppWarning errorMsg={_t("Error loading Widget")} />
|
||||
|
@ -365,9 +429,9 @@ export default class AppTile extends React.Component {
|
|||
<AppPermission
|
||||
roomId={this.props.room.roomId}
|
||||
creatorUserId={this.props.creatorUserId}
|
||||
url={this._sgWidget.embedUrl}
|
||||
url={this.sgWidget.embedUrl}
|
||||
isRoomEncrypted={isEncrypted}
|
||||
onPermissionGranted={this._grantWidgetPermission}
|
||||
onPermissionGranted={this.grantWidgetPermission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -390,8 +454,8 @@ export default class AppTile extends React.Component {
|
|||
{ this.state.loading && loadingElement }
|
||||
<iframe
|
||||
allow={iframeFeatures}
|
||||
ref={this._iframeRefChange}
|
||||
src={this._sgWidget.embedUrl}
|
||||
ref={this.iframeRefChange}
|
||||
src={this.sgWidget.embedUrl}
|
||||
allowFullScreen={true}
|
||||
sandbox={sandboxFlags}
|
||||
/>
|
||||
|
@ -407,7 +471,7 @@ export default class AppTile extends React.Component {
|
|||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
<PersistedElement persistKey={this.persistKey}>
|
||||
{ appTileBody }
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
|
@ -429,9 +493,9 @@ export default class AppTile extends React.Component {
|
|||
if (this.state.menuDisplayed) {
|
||||
contextMenu = (
|
||||
<RoomWidgetContextMenu
|
||||
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)}
|
||||
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)}
|
||||
app={this.props.app}
|
||||
onFinished={this._closeContextMenu}
|
||||
onFinished={this.closeContextMenu}
|
||||
showUnpin={!this.props.userWidget}
|
||||
userWidget={this.props.userWidget}
|
||||
onEditClick={this.props.onEditClick}
|
||||
|
@ -444,21 +508,21 @@ export default class AppTile extends React.Component {
|
|||
<div className={appTileClasses} id={this.props.app.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div className="mx_AppTileMenuBar">
|
||||
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}>
|
||||
{ this.props.showTitle && this._getTileTitle() }
|
||||
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}>
|
||||
{ this.props.showTitle && this.getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ this.props.showPopout && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||
title={_t('Popout widget')}
|
||||
onClick={this._onPopoutWidgetClick}
|
||||
onClick={this.onPopoutWidgetClick}
|
||||
/> }
|
||||
<ContextMenuButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||
label={_t("Options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
inputRef={this._contextMenuButton}
|
||||
onClick={this._onContextMenuClick}
|
||||
inputRef={this.contextMenuButton}
|
||||
onClick={this.onContextMenuClick}
|
||||
/>
|
||||
</span>
|
||||
</div> }
|
||||
|
@ -469,49 +533,3 @@ export default class AppTile extends React.Component {
|
|||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
AppTile.displayName = 'AppTile';
|
||||
|
||||
AppTile.propTypes = {
|
||||
app: PropTypes.object.isRequired,
|
||||
// If room is not specified then it is an account level widget
|
||||
// which bypasses permission prompts as it was added explicitly by that user
|
||||
room: PropTypes.object,
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: PropTypes.bool,
|
||||
// Optional. If set, renders a smaller view of the widget
|
||||
miniMode: PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: PropTypes.string,
|
||||
waitForIframeLoad: PropTypes.bool,
|
||||
showMenubar: PropTypes.bool,
|
||||
// Optional onEditClickHandler (overrides default behaviour)
|
||||
onEditClick: PropTypes.func,
|
||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||
onDeleteClick: PropTypes.func,
|
||||
// Optional onMinimiseClickHandler
|
||||
onMinimiseClick: PropTypes.func,
|
||||
// Optionally hide the tile title
|
||||
showTitle: PropTypes.bool,
|
||||
// Optionally handle minimise button pointer events (default false)
|
||||
handleMinimisePointerEvents: PropTypes.bool,
|
||||
// Optionally hide the popout widget icon
|
||||
showPopout: PropTypes.bool,
|
||||
// Is this an instance of a user widget
|
||||
userWidget: PropTypes.bool,
|
||||
// sets the pointer-events property on the iframe
|
||||
pointerEvents: PropTypes.string,
|
||||
};
|
||||
|
||||
AppTile.defaultProps = {
|
||||
waitForIframeLoad: true,
|
||||
showMenubar: true,
|
||||
showTitle: true,
|
||||
showPopout: true,
|
||||
handleMinimisePointerEvents: false,
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
};
|
|
@ -1,24 +1,20 @@
|
|||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const AppWarning = (props) => {
|
||||
interface IProps {
|
||||
errorMsg?: string;
|
||||
}
|
||||
|
||||
const AppWarning: React.FC<IProps> = (props) => {
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src={require("../../../../res/img/warning.svg")} alt='' />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AppWarning.propTypes = {
|
||||
errorMsg: PropTypes.string,
|
||||
};
|
||||
AppWarning.defaultProps = {
|
||||
errorMsg: 'Error',
|
||||
};
|
||||
|
||||
export default AppWarning;
|
|
@ -17,60 +17,61 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// The primary button which is styled differently and has default focus.
|
||||
primaryButton: React.ReactNode;
|
||||
|
||||
// A node to insert into the cancel button instead of default "Cancel"
|
||||
cancelButton?: React.ReactNode;
|
||||
|
||||
// If true, make the primary button a form submit button (input type="submit")
|
||||
primaryIsSubmit?: boolean;
|
||||
|
||||
// onClick handler for the primary button.
|
||||
onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
|
||||
|
||||
// should there be a cancel button? default: true
|
||||
hasCancel?: boolean;
|
||||
|
||||
// The class of the cancel button, only used if a cancel button is
|
||||
// enabled
|
||||
cancelButtonClass?: string;
|
||||
|
||||
// onClick handler for the cancel button.
|
||||
onCancel?: (...args: any[]) => void;
|
||||
|
||||
focus?: boolean;
|
||||
|
||||
// disables the primary and cancel buttons
|
||||
disabled?: boolean;
|
||||
|
||||
// disables only the primary button
|
||||
primaryDisabled?: boolean;
|
||||
|
||||
// something to stick next to the buttons, optionally
|
||||
additive?: React.ReactNode;
|
||||
|
||||
primaryButtonClass?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic container for buttons in modal dialogs.
|
||||
*/
|
||||
@replaceableComponent("views.elements.DialogButtons")
|
||||
export default class DialogButtons extends React.Component {
|
||||
static propTypes = {
|
||||
// The primary button which is styled differently and has default focus.
|
||||
primaryButton: PropTypes.node.isRequired,
|
||||
|
||||
// A node to insert into the cancel button instead of default "Cancel"
|
||||
cancelButton: PropTypes.node,
|
||||
|
||||
// If true, make the primary button a form submit button (input type="submit")
|
||||
primaryIsSubmit: PropTypes.bool,
|
||||
|
||||
// onClick handler for the primary button.
|
||||
onPrimaryButtonClick: PropTypes.func,
|
||||
|
||||
// should there be a cancel button? default: true
|
||||
hasCancel: PropTypes.bool,
|
||||
|
||||
// The class of the cancel button, only used if a cancel button is
|
||||
// enabled
|
||||
cancelButtonClass: PropTypes.node,
|
||||
|
||||
// onClick handler for the cancel button.
|
||||
onCancel: PropTypes.func,
|
||||
|
||||
focus: PropTypes.bool,
|
||||
|
||||
// disables the primary and cancel buttons
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
// disables only the primary button
|
||||
primaryDisabled: PropTypes.bool,
|
||||
|
||||
// something to stick next to the buttons, optionally
|
||||
additive: PropTypes.element,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class DialogButtons extends React.Component<IProps> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
hasCancel: true,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onCancel();
|
||||
private onCancelClick = (event: React.MouseEvent): void => {
|
||||
this.props.onCancel(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let primaryButtonClassName = "mx_Dialog_primary";
|
||||
if (this.props.primaryButtonClass) {
|
||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||
|
@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
|
|||
// important: the default type is 'submit' and this button comes before the
|
||||
// primary in the DOM so will get form submissions unless we make it not a submit.
|
||||
type="button"
|
||||
onClick={this._onCancelClick}
|
||||
onClick={this.onCancelClick}
|
||||
className={this.props.cancelButtonClass}
|
||||
disabled={this.props.disabled}
|
||||
>
|
|
@ -14,71 +14,73 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import React, { ChangeEvent, createRef } from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onClear?: () => void;
|
||||
onJoinClick?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
showJoinButton?: boolean;
|
||||
initialText?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.DirectorySearchBox")
|
||||
export default class DirectorySearchBox extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._collectInput = this._collectInput.bind(this);
|
||||
this._onClearClick = this._onClearClick.bind(this);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this._onKeyUp = this._onKeyUp.bind(this);
|
||||
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
|
||||
export default class DirectorySearchBox extends React.Component<IProps, IState> {
|
||||
private input = createRef<HTMLInputElement>();
|
||||
|
||||
this.input = null;
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: this.props.initialText || '',
|
||||
};
|
||||
}
|
||||
|
||||
_collectInput(e) {
|
||||
this.input = e;
|
||||
}
|
||||
|
||||
_onClearClick() {
|
||||
private onClearClick = (): void => {
|
||||
this.setState({ value: '' });
|
||||
|
||||
if (this.input) {
|
||||
this.input.focus();
|
||||
if (this.input.current) {
|
||||
this.input.current.focus();
|
||||
|
||||
if (this.props.onClear) {
|
||||
this.props.onClear();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onChange(ev) {
|
||||
if (!this.input) return;
|
||||
private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
|
||||
if (!this.input.current) return;
|
||||
this.setState({ value: ev.target.value });
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(ev.target.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyUp(ev) {
|
||||
private onKeyUp = (ev: React.KeyboardEvent): void => {
|
||||
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
||||
if (this.props.onJoinClick) {
|
||||
this.props.onJoinClick(this.state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onJoinButtonClick() {
|
||||
private onJoinButtonClick = (): void => {
|
||||
if (this.props.onJoinClick) {
|
||||
this.props.onJoinClick(this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const searchboxClasses = {
|
||||
mx_DirectorySearchBox: true,
|
||||
};
|
||||
|
@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
|
|||
let joinButton;
|
||||
if (this.props.showJoinButton) {
|
||||
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
|
||||
onClick={this._onJoinButtonClick}
|
||||
onClick={this.onJoinButtonClick}
|
||||
>{ _t("Join") }</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
|
|||
name="dirsearch"
|
||||
value={this.state.value}
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
ref={this._collectInput}
|
||||
onChange={this._onChange}
|
||||
onKeyUp={this._onKeyUp}
|
||||
ref={this.input}
|
||||
onChange={this.onChange}
|
||||
onKeyUp={this.onKeyUp}
|
||||
placeholder={this.props.placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
{ joinButton }
|
||||
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
||||
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
DirectorySearchBox.propTypes = {
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onClear: PropTypes.func,
|
||||
onJoinClick: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
showJoinButton: PropTypes.bool,
|
||||
initialText: PropTypes.string,
|
||||
};
|
|
@ -16,33 +16,42 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
enum Phases {
|
||||
Display = "display",
|
||||
Edit = "edit",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onValueChanged?: (value: string, shouldSubmit: boolean) => void;
|
||||
initialValue?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
placeholderClassName?: string;
|
||||
// Overrides blurToSubmit if true
|
||||
blurToCancel?: boolean;
|
||||
// Will cause onValueChanged(value, true) to fire on blur
|
||||
blurToSubmit?: boolean;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phases;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.EditableText")
|
||||
export default class EditableText extends React.Component {
|
||||
static propTypes = {
|
||||
onValueChanged: PropTypes.func,
|
||||
initialValue: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
labelClassName: PropTypes.string,
|
||||
placeholderClassName: PropTypes.string,
|
||||
// Overrides blurToSubmit if true
|
||||
blurToCancel: PropTypes.bool,
|
||||
// Will cause onValueChanged(value, true) to fire on blur
|
||||
blurToSubmit: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
};
|
||||
export default class EditableText extends React.Component<IProps, IState> {
|
||||
// we track value as an JS object field rather than in React state
|
||||
// as React doesn't play nice with contentEditable.
|
||||
public value = '';
|
||||
private placeholder = false;
|
||||
private editableDiv = createRef<HTMLDivElement>();
|
||||
|
||||
static Phases = {
|
||||
Display: "display",
|
||||
Edit: "edit",
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
onValueChanged() {},
|
||||
initialValue: '',
|
||||
label: '',
|
||||
|
@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
|
|||
blurToSubmit: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// we track value as an JS object field rather than in React state
|
||||
// as React doesn't play nice with contentEditable.
|
||||
this.value = '';
|
||||
this.placeholder = false;
|
||||
|
||||
this._editable_div = createRef();
|
||||
this.state = {
|
||||
phase: Phases.Display,
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
phase: EditableText.Phases.Display,
|
||||
};
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this._editable_div.current) {
|
||||
if (this.editableDiv.current) {
|
||||
this.showPlaceholder(!this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.value = this.props.initialValue;
|
||||
if (this._editable_div.current) {
|
||||
if (this.editableDiv.current) {
|
||||
this.showPlaceholder(!this.value);
|
||||
}
|
||||
}
|
||||
|
||||
showPlaceholder = show => {
|
||||
private showPlaceholder = (show: boolean): void => {
|
||||
if (show) {
|
||||
this._editable_div.current.textContent = this.props.placeholder;
|
||||
this._editable_div.current.setAttribute("class", this.props.className
|
||||
this.editableDiv.current.textContent = this.props.placeholder;
|
||||
this.editableDiv.current.setAttribute("class", this.props.className
|
||||
+ " " + this.props.placeholderClassName);
|
||||
this.placeholder = true;
|
||||
this.value = '';
|
||||
} else {
|
||||
this._editable_div.current.textContent = this.value;
|
||||
this._editable_div.current.setAttribute("class", this.props.className);
|
||||
this.editableDiv.current.textContent = this.value;
|
||||
this.editableDiv.current.setAttribute("class", this.props.className);
|
||||
this.placeholder = false;
|
||||
}
|
||||
};
|
||||
|
||||
getValue = () => this.value;
|
||||
|
||||
setValue = value => {
|
||||
this.value = value;
|
||||
this.showPlaceholder(!this.value);
|
||||
};
|
||||
|
||||
edit = () => {
|
||||
private cancelEdit = (): void => {
|
||||
this.setState({
|
||||
phase: EditableText.Phases.Edit,
|
||||
});
|
||||
};
|
||||
|
||||
cancelEdit = () => {
|
||||
this.setState({
|
||||
phase: EditableText.Phases.Display,
|
||||
phase: Phases.Display,
|
||||
});
|
||||
this.value = this.props.initialValue;
|
||||
this.showPlaceholder(!this.value);
|
||||
this.onValueChanged(false);
|
||||
this._editable_div.current.blur();
|
||||
this.editableDiv.current.blur();
|
||||
};
|
||||
|
||||
onValueChanged = shouldSubmit => {
|
||||
private onValueChanged = (shouldSubmit: boolean): void => {
|
||||
this.props.onValueChanged(this.value, shouldSubmit);
|
||||
};
|
||||
|
||||
onKeyDown = ev => {
|
||||
private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
|
||||
if (this.placeholder) {
|
||||
|
@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
|
|||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
};
|
||||
|
||||
onKeyUp = ev => {
|
||||
private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
|
||||
if (!ev.target.textContent) {
|
||||
if (!(ev.target as HTMLDivElement).textContent) {
|
||||
this.showPlaceholder(true);
|
||||
} else if (!this.placeholder) {
|
||||
this.value = ev.target.textContent;
|
||||
this.value = (ev.target as HTMLDivElement).textContent;
|
||||
}
|
||||
|
||||
if (ev.key === Key.ENTER) {
|
||||
|
@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
|
|||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
};
|
||||
|
||||
onClickDiv = ev => {
|
||||
private onClickDiv = (): void => {
|
||||
if (!this.props.editable) return;
|
||||
|
||||
this.setState({
|
||||
phase: EditableText.Phases.Edit,
|
||||
phase: Phases.Edit,
|
||||
});
|
||||
};
|
||||
|
||||
onFocus = ev => {
|
||||
private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
||||
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||
|
||||
const node = ev.target.childNodes[0];
|
||||
if (node) {
|
||||
const range = document.createRange();
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, node.length);
|
||||
range.setEnd(node, ev.target.childNodes.length);
|
||||
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onFinish = (ev, shouldSubmit) => {
|
||||
private onFinish = (
|
||||
ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
|
||||
shouldSubmit?: boolean,
|
||||
): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
const submit = (ev.key === Key.ENTER) || shouldSubmit;
|
||||
const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
|
||||
this.setState({
|
||||
phase: EditableText.Phases.Display,
|
||||
phase: Phases.Display,
|
||||
}, () => {
|
||||
if (this.value !== this.props.initialValue) {
|
||||
self.onValueChanged(submit);
|
||||
|
@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onBlur = ev => {
|
||||
private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
|
@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
|
|||
this.showPlaceholder(!this.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const { className, editable, initialValue, label, labelClassName } = this.props;
|
||||
let editableEl;
|
||||
|
||||
if (!editable || (this.state.phase === EditableText.Phases.Display &&
|
||||
if (!editable || (this.state.phase === Phases.Display &&
|
||||
(label || labelClassName) && !this.value)
|
||||
) {
|
||||
// show the label
|
||||
|
@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
|
|||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editableEl = <div
|
||||
ref={this._editable_div}
|
||||
ref={this.editableDiv}
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
|
@ -15,9 +15,34 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from "./Spinner";
|
||||
import EditableText from "./EditableText";
|
||||
|
||||
interface IProps {
|
||||
/* callback to retrieve the initial value. */
|
||||
getInitialValue?: () => Promise<string>;
|
||||
|
||||
/* initial value; used if getInitialValue is not given */
|
||||
initialValue?: string;
|
||||
|
||||
/* placeholder text to use when the value is empty (and not being
|
||||
* edited) */
|
||||
placeholder?: string;
|
||||
|
||||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit?: (value: string) => Promise<{} | void>;
|
||||
|
||||
/* should the input submit when focus is lost? */
|
||||
blurToSubmit?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
errorString: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component which wraps an EditableText, with a spinner while updates take
|
||||
|
@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
* taken from the 'initialValue' property.
|
||||
*/
|
||||
@replaceableComponent("views.elements.EditableTextContainer")
|
||||
export default class EditableTextContainer extends React.Component {
|
||||
constructor(props) {
|
||||
export default class EditableTextContainer extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
initialValue: "",
|
||||
placeholder: "",
|
||||
blurToSubmit: false,
|
||||
onSubmit: () => { return Promise.resolve(); },
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
this.state = {
|
||||
busy: false,
|
||||
errorString: null,
|
||||
value: props.initialValue,
|
||||
};
|
||||
this._onValueChanged = this._onValueChanged.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.getInitialValue === undefined) {
|
||||
// use whatever was given in the initialValue property.
|
||||
return;
|
||||
}
|
||||
public async componentDidMount(): Promise<void> {
|
||||
// use whatever was given in the initialValue property.
|
||||
if (this.props.getInitialValue === undefined) return;
|
||||
|
||||
this.setState({ busy: true });
|
||||
|
||||
this.props.getInitialValue().then(
|
||||
(result) => {
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
busy: false,
|
||||
value: result,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
try {
|
||||
const initialValue = await this.props.getInitialValue();
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
busy: false,
|
||||
value: initialValue,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
_onValueChanged(value, shouldSubmit) {
|
||||
private onValueChanged = (value: string, shouldSubmit: boolean): void => {
|
||||
if (!shouldSubmit) {
|
||||
return;
|
||||
}
|
||||
|
@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
|
|||
|
||||
this.props.onSubmit(value).then(
|
||||
() => {
|
||||
if (this._unmounted) { return; }
|
||||
if (this.unmounted) { return; }
|
||||
this.setState({
|
||||
busy: false,
|
||||
value: value,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
if (this._unmounted) { return; }
|
||||
if (this.unmounted) { return; }
|
||||
this.setState({
|
||||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.state.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
<Spinner />
|
||||
);
|
||||
} else if (this.state.errorString) {
|
||||
return (
|
||||
<div className="error">{ this.state.errorString }</div>
|
||||
);
|
||||
} else {
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
return (
|
||||
<EditableText initialValue={this.state.value}
|
||||
placeholder={this.props.placeholder}
|
||||
onValueChanged={this._onValueChanged}
|
||||
onValueChanged={this.onValueChanged}
|
||||
blurToSubmit={this.props.blurToSubmit}
|
||||
/>
|
||||
);
|
||||
|
@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
EditableTextContainer.propTypes = {
|
||||
/* callback to retrieve the initial value. */
|
||||
getInitialValue: PropTypes.func,
|
||||
|
||||
/* initial value; used if getInitialValue is not given */
|
||||
initialValue: PropTypes.string,
|
||||
|
||||
/* placeholder text to use when the value is empty (and not being
|
||||
* edited) */
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit: PropTypes.func,
|
||||
|
||||
/* should the input submit when focus is lost? */
|
||||
blurToSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
EditableTextContainer.defaultProps = {
|
||||
initialValue: "",
|
||||
placeholder: "",
|
||||
blurToSubmit: false,
|
||||
onSubmit: function(v) {return Promise.resolve(); },
|
||||
};
|
|
@ -34,6 +34,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
||||
import { IDialogProps } from '../dialogs/IDialogProps';
|
||||
import UIStore from '../../../stores/UIStore';
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
|
@ -44,6 +45,13 @@ const ZOOM_COEFFICIENT = 0.0025;
|
|||
// If we have moved only this much we can zoom
|
||||
const ZOOM_DISTANCE = 10;
|
||||
|
||||
// Height of mx_ImageView_panel
|
||||
const getPanelHeight = (): number => {
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue("--image-view-panel-height");
|
||||
// Return the value as a number without the unit
|
||||
return parseInt(value.slice(0, value.length - 2));
|
||||
};
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
src: string; // the source of the image being displayed
|
||||
name?: string; // the main title ('name') for the image
|
||||
|
@ -56,8 +64,15 @@ interface IProps extends IDialogProps {
|
|||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||
// properties above, which let us use lightboxes to display images which aren't associated
|
||||
// with events.
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
mxEvent?: MatrixEvent;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
|
||||
thumbnailInfo?: {
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -75,13 +90,25 @@ interface IState {
|
|||
export default class ImageView extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { thumbnailInfo } = this.props;
|
||||
|
||||
this.state = {
|
||||
zoom: 0,
|
||||
zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize
|
||||
minZoom: MAX_SCALE,
|
||||
maxZoom: MAX_SCALE,
|
||||
rotation: 0,
|
||||
translationX: 0,
|
||||
translationY: 0,
|
||||
translationX: (
|
||||
thumbnailInfo?.positionX +
|
||||
(thumbnailInfo?.width / 2) -
|
||||
(UIStore.instance.windowWidth / 2)
|
||||
) ?? 0,
|
||||
translationY: (
|
||||
thumbnailInfo?.positionY +
|
||||
(thumbnailInfo?.height / 2) -
|
||||
(UIStore.instance.windowHeight / 2) -
|
||||
(getPanelHeight() / 2)
|
||||
) ?? 0,
|
||||
moving: false,
|
||||
contextMenuDisplayed: false,
|
||||
};
|
||||
|
@ -98,6 +125,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
private previousX = 0;
|
||||
private previousY = 0;
|
||||
|
||||
private animatingLoading = false;
|
||||
private imageIsLoaded = false;
|
||||
|
||||
componentDidMount() {
|
||||
// We have to use addEventListener() because the listener
|
||||
// needs to be passive in order to work with Chromium
|
||||
|
@ -105,15 +135,37 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// We want to recalculate zoom whenever the window's size changes
|
||||
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.recalculateZoom);
|
||||
this.image.current.addEventListener("load", this.imageLoaded);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||
window.removeEventListener("resize", this.recalculateZoom);
|
||||
this.image.current.removeEventListener("load", this.recalculateZoom);
|
||||
this.image.current.removeEventListener("load", this.imageLoaded);
|
||||
}
|
||||
|
||||
private imageLoaded = () => {
|
||||
// First, we calculate the zoom, so that the image has the same size as
|
||||
// the thumbnail
|
||||
const { thumbnailInfo } = this.props;
|
||||
if (thumbnailInfo?.width) {
|
||||
this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth });
|
||||
}
|
||||
|
||||
// Once the zoom is set, we the image is considered loaded and we can
|
||||
// start animating it into the center of the screen
|
||||
this.imageIsLoaded = true;
|
||||
this.animatingLoading = true;
|
||||
this.setZoomAndRotation();
|
||||
this.setState({
|
||||
translationX: 0,
|
||||
translationY: 0,
|
||||
});
|
||||
|
||||
// Once the position is set, there is no need to animate anymore
|
||||
this.animatingLoading = false;
|
||||
};
|
||||
|
||||
private recalculateZoom = () => {
|
||||
this.setZoomAndRotation();
|
||||
};
|
||||
|
@ -360,16 +412,17 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
const showEventMeta = !!this.props.mxEvent;
|
||||
const zoomingDisabled = this.state.maxZoom === this.state.minZoom;
|
||||
|
||||
let transitionClassName;
|
||||
if (this.animatingLoading) transitionClassName = "mx_ImageView_image_animatingLoading";
|
||||
else if (this.state.moving || !this.imageIsLoaded) transitionClassName = "";
|
||||
else transitionClassName = "mx_ImageView_image_animating";
|
||||
|
||||
let cursor;
|
||||
if (this.state.moving) {
|
||||
cursor= "grabbing";
|
||||
} else if (zoomingDisabled) {
|
||||
cursor = "default";
|
||||
} else if (this.state.zoom === this.state.minZoom) {
|
||||
cursor = "zoom-in";
|
||||
} else {
|
||||
cursor = "zoom-out";
|
||||
}
|
||||
if (this.state.moving) cursor = "grabbing";
|
||||
else if (zoomingDisabled) cursor = "default";
|
||||
else if (this.state.zoom === this.state.minZoom) cursor = "zoom-in";
|
||||
else cursor = "zoom-out";
|
||||
|
||||
const rotationDegrees = this.state.rotation + "deg";
|
||||
const zoom = this.state.zoom;
|
||||
const translatePixelsX = this.state.translationX + "px";
|
||||
|
@ -380,7 +433,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// image causing it translate in the wrong direction.
|
||||
const style = {
|
||||
cursor: cursor,
|
||||
transition: this.state.moving ? null : "transform 200ms ease 0s",
|
||||
transform: `translateX(${translatePixelsX})
|
||||
translateY(${translatePixelsY})
|
||||
scale(${zoom})
|
||||
|
@ -528,7 +580,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
style={style}
|
||||
alt={this.props.name}
|
||||
ref={this.image}
|
||||
className="mx_ImageView_image"
|
||||
className={`mx_ImageView_image ${transitionClassName}`}
|
||||
draggable={true}
|
||||
onMouseDown={this.onStartMoving}
|
||||
/>
|
||||
|
|
|
@ -16,13 +16,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from "./Spinner";
|
||||
import Dropdown from "./Dropdown";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||
|
@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
|
|||
return false;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
onOptionChange: (language: string) => void;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
searchQuery: string;
|
||||
langs: string[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.LanguageDropdown")
|
||||
export default class LanguageDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
export default class LanguageDropdown extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
|
@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||
langs.sort(function(a, b) {
|
||||
if (a.label < b.label) return -1;
|
||||
|
@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
private onSearchChange = (search: string): void => {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.state.langs === null) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let displayedLanguages;
|
||||
if (this.state.searchQuery) {
|
||||
displayedLanguages = this.state.langs.filter((lang) => {
|
||||
|
@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
|
|||
id="mx_LanguageDropdown"
|
||||
className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
onSearchChange={this.onSearchChange}
|
||||
searchEnabled={true}
|
||||
value={value}
|
||||
label={_t("Language Dropdown")}
|
||||
|
@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
LanguageDropdown.propTypes = {
|
||||
className: PropTypes.string,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
|
@ -15,17 +15,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
class ItemRange {
|
||||
constructor(topCount, renderCount, bottomCount) {
|
||||
this.topCount = topCount;
|
||||
this.renderCount = renderCount;
|
||||
this.bottomCount = bottomCount;
|
||||
}
|
||||
constructor(
|
||||
public topCount: number,
|
||||
public renderCount: number,
|
||||
public bottomCount: number,
|
||||
) { }
|
||||
|
||||
contains(range) {
|
||||
public contains(range: ItemRange): boolean {
|
||||
// don't contain empty ranges
|
||||
// as it will prevent clearing the list
|
||||
// once it is scrolled far enough out of view
|
||||
|
@ -36,7 +35,7 @@ class ItemRange {
|
|||
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
|
||||
}
|
||||
|
||||
expand(amount) {
|
||||
public expand(amount: number): ItemRange {
|
||||
// don't expand ranges that won't render anything
|
||||
if (this.renderCount === 0) {
|
||||
return this;
|
||||
|
@ -51,20 +50,55 @@ class ItemRange {
|
|||
);
|
||||
}
|
||||
|
||||
totalSize() {
|
||||
public totalSize(): number {
|
||||
return this.topCount + this.renderCount + this.bottomCount;
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps<T> {
|
||||
// height in pixels of the component returned by `renderItem`
|
||||
itemHeight: number;
|
||||
// function to turn an element of `items` into a react component
|
||||
renderItem: (item: T) => JSX.Element;
|
||||
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
|
||||
scrollTop: number;
|
||||
// the height of the viewport this content is scrolled in
|
||||
height: number;
|
||||
// all items for the list. These should not be react components, see `renderItem`.
|
||||
items?: T[];
|
||||
// the amount of items to scroll before causing a rerender,
|
||||
// should typically be less than `overflowItems` unless applying
|
||||
// margins in the parent component when using multiple LazyRenderList in one viewport.
|
||||
// use 0 to only rerender when items will come into view.
|
||||
overflowMargin?: number;
|
||||
// the amount of items to add at the top and bottom to render,
|
||||
// so not every scroll of causes a rerender.
|
||||
overflowItems?: number;
|
||||
|
||||
element?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
renderRange: ItemRange;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.LazyRenderList")
|
||||
export default class LazyRenderList extends React.Component {
|
||||
constructor(props) {
|
||||
export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
|
||||
public static defaultProps: Partial<IProps<unknown>> = {
|
||||
overflowItems: 20,
|
||||
overflowMargin: 5,
|
||||
};
|
||||
|
||||
constructor(props: IProps<T>) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
this.state = {
|
||||
renderRange: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> {
|
||||
const range = LazyRenderList.getVisibleRangeFromProps(props);
|
||||
const intersectRange = range.expand(props.overflowMargin);
|
||||
const renderRange = range.expand(props.overflowItems);
|
||||
|
@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
static getVisibleRangeFromProps(props) {
|
||||
private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange {
|
||||
const { items, itemHeight, scrollTop, height } = props;
|
||||
const length = items ? items.length : 0;
|
||||
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
|
||||
|
@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
|
|||
return new ItemRange(topCount, renderCount, bottomCount);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const { itemHeight, items, renderItem } = this.props;
|
||||
const { renderRange } = this.state;
|
||||
const { topCount, renderCount, bottomCount } = renderRange;
|
||||
|
@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
LazyRenderList.defaultProps = {
|
||||
overflowItems: 20,
|
||||
overflowMargin: 5,
|
||||
};
|
||||
|
||||
LazyRenderList.propTypes = {
|
||||
// height in pixels of the component returned by `renderItem`
|
||||
itemHeight: PropTypes.number.isRequired,
|
||||
// function to turn an element of `items` into a react component
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
// the height of the viewport this content is scrolled in
|
||||
height: PropTypes.number.isRequired,
|
||||
// all items for the list. These should not be react components, see `renderItem`.
|
||||
items: PropTypes.array,
|
||||
// the amount of items to scroll before causing a rerender,
|
||||
// should typically be less than `overflowItems` unless applying
|
||||
// margins in the parent component when using multiple LazyRenderList in one viewport.
|
||||
// use 0 to only rerender when items will come into view.
|
||||
overflowMargin: PropTypes.number,
|
||||
// the amount of items to add at the top and bottom to render,
|
||||
// so not every scroll of causes a rerender.
|
||||
overflowItems: PropTypes.number,
|
||||
};
|
|
@ -135,7 +135,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
|
||||
const desc = formatCommaSeparatedList(descs);
|
||||
|
||||
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
|
||||
return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
|
||||
});
|
||||
|
||||
if (!summaries) {
|
||||
|
|
|
@ -16,25 +16,26 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { throttle } from "lodash";
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
||||
export const getPersistKey = (appId: string) => 'widget_' + appId;
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
function getContainer(containerId) {
|
||||
return document.getElementById(containerId);
|
||||
function getContainer(containerId: string): HTMLDivElement {
|
||||
return document.getElementById(containerId) as HTMLDivElement;
|
||||
}
|
||||
|
||||
function getOrCreateContainer(containerId) {
|
||||
function getOrCreateContainer(containerId: string): HTMLDivElement {
|
||||
let container = getContainer(containerId);
|
||||
|
||||
if (!container) {
|
||||
|
@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
|
|||
return container;
|
||||
}
|
||||
|
||||
/*
|
||||
interface IProps {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: string;
|
||||
|
||||
// z-index for the element. Defaults to 9.
|
||||
zIndex?: number;
|
||||
|
||||
style?: React.StyleHTMLAttributes<HTMLDivElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||
* in a container element appended to document.body.
|
||||
*
|
||||
|
@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
|
|||
* bounding rect as the parent of PE.
|
||||
*/
|
||||
@replaceableComponent("views.elements.PersistedElement")
|
||||
export default class PersistedElement extends React.Component {
|
||||
static propTypes = {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
export default class PersistedElement extends React.Component<IProps> {
|
||||
private resizeObserver: ResizeObserver;
|
||||
private dispatcherRef: string;
|
||||
private childContainer: HTMLDivElement;
|
||||
private child: HTMLDivElement;
|
||||
|
||||
// z-index for the element. Defaults to 9.
|
||||
zIndex: PropTypes.number,
|
||||
};
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||
this.collectChild = this.collectChild.bind(this);
|
||||
this._repositionChild = this._repositionChild.bind(this);
|
||||
this._onAction = this._onAction.bind(this);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this._repositionChild);
|
||||
this.resizeObserver = new ResizeObserver(this.repositionChild);
|
||||
// Annoyingly, a resize observer is insufficient, since we also care
|
||||
// about when the element moves on the screen without changing its
|
||||
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
||||
// for this, so we bodge it by listening for document resize and
|
||||
// the timeline_resize action.
|
||||
window.addEventListener('resize', this._repositionChild);
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
window.addEventListener('resize', this.repositionChild);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the DOM elements created when a PersistedElement with the given
|
||||
* persistKey was mounted. The DOM elements will be re-added if another
|
||||
* PeristedElement is mounted in the future.
|
||||
* PersistedElement is mounted in the future.
|
||||
*
|
||||
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
||||
*/
|
||||
static destroyElement(persistKey) {
|
||||
public static destroyElement(persistKey: string): void {
|
||||
const container = getContainer('mx_persistedElement_' + persistKey);
|
||||
if (container) {
|
||||
container.remove();
|
||||
|
@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
|
|||
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
||||
}
|
||||
|
||||
collectChildContainer(ref) {
|
||||
private collectChildContainer = (ref: HTMLDivElement): void => {
|
||||
if (this.childContainer) {
|
||||
this.resizeObserver.unobserve(this.childContainer);
|
||||
}
|
||||
|
@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
|
|||
if (ref) {
|
||||
this.resizeObserver.observe(ref);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
collectChild(ref) {
|
||||
private collectChild = (ref: HTMLDivElement): void => {
|
||||
this.child = ref;
|
||||
this.updateChild();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.updateChild();
|
||||
this.renderApp();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
public componentDidUpdate(): void {
|
||||
this.updateChild();
|
||||
this.renderApp();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
this.updateChildVisibility(this.child, false);
|
||||
this.resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', this._repositionChild);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
window.removeEventListener('resize', this.repositionChild);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction(payload) {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.action === 'timeline_resize') {
|
||||
this._repositionChild();
|
||||
this.repositionChild();
|
||||
} else if (payload.action === 'logout') {
|
||||
PersistedElement.destroyElement(this.props.persistKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_repositionChild() {
|
||||
private repositionChild = (): void => {
|
||||
this.updateChildPosition(this.child, this.childContainer);
|
||||
}
|
||||
};
|
||||
|
||||
updateChild() {
|
||||
private updateChild(): void {
|
||||
this.updateChildPosition(this.child, this.childContainer);
|
||||
this.updateChildVisibility(this.child, true);
|
||||
}
|
||||
|
||||
renderApp() {
|
||||
private renderApp(): void {
|
||||
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
|
||||
<div ref={this.collectChild} style={this.props.style}>
|
||||
{ this.props.children }
|
||||
|
@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
|
|||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
}
|
||||
|
||||
updateChildVisibility(child, visible) {
|
||||
private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
|
||||
if (!child) return;
|
||||
child.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
updateChildPosition = throttle((child, parent) => {
|
||||
private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
|
||||
if (!child || !parent) return;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
|
|||
});
|
||||
}, 100, { trailing: true, leading: true });
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
return <div ref={this.collectChildContainer} />;
|
||||
}
|
||||
}
|
||||
|
||||
export const getPersistKey = (appId) => 'widget_' + appId;
|
|
@ -17,61 +17,74 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
import AppTile from "./AppTile";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
interface IState {
|
||||
roomId: string;
|
||||
persistentWidgetId: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.PersistentApp")
|
||||
export default class PersistentApp extends React.Component {
|
||||
state = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
};
|
||||
export default class PersistentApp extends React.Component<{}, IState> {
|
||||
private roomStoreToken: EventSubscription;
|
||||
|
||||
componentDidMount() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
|
||||
constructor() {
|
||||
super({});
|
||||
|
||||
this.state = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
public componentDidMount(): void {
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
||||
ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomViewStoreUpdate = payload => {
|
||||
private onRoomViewStoreUpdate = (): void => {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
_onActiveWidgetStoreUpdate = () => {
|
||||
private onActiveWidgetStoreUpdate = (): void => {
|
||||
this.setState({
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
});
|
||||
};
|
||||
|
||||
_onMyMembership = async (room, membership) => {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
private onMyMembership = async (room: Room, membership: string): Promise<void> => {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
|
||||
if (membership !== "join") {
|
||||
// we're not in the room anymore - delete
|
||||
if (room.roomId === persistentWidgetInRoomId) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
|
||||
if (room .roomId === persistentWidgetInRoomId) {
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.state.persistentWidgetId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
|
||||
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
|
||||
|
@ -83,13 +96,12 @@ export default class PersistentApp extends React.Component {
|
|||
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||
persistentWidgetInRoomId, appEvent.getId(),
|
||||
);
|
||||
const AppTile = sdk.getComponent('elements.AppTile');
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
app={app}
|
|
@ -15,40 +15,52 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Roles from '../../../Roles';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "./Field";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
|
||||
|
||||
interface IProps {
|
||||
value: number;
|
||||
// The maximum value that can be set with the power selector
|
||||
maxValue: number;
|
||||
|
||||
// Default user power level for the room
|
||||
usersDefault: number;
|
||||
|
||||
// should the user be able to change the value? false by default.
|
||||
disabled?: boolean;
|
||||
onChange?: (value: number, powerLevelKey: string) => void;
|
||||
|
||||
// Optional key to pass as the second argument to `onChange`
|
||||
powerLevelKey?: string;
|
||||
|
||||
// The name to annotate the selector with
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
levelRoleMap: {};
|
||||
// List of power levels to show in the drop-down
|
||||
options: number[];
|
||||
|
||||
customValue: number;
|
||||
selectValue: number | string;
|
||||
custom?: boolean;
|
||||
customLevel?: number;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.PowerSelector")
|
||||
export default class PowerSelector extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
// The maximum value that can be set with the power selector
|
||||
maxValue: PropTypes.number.isRequired,
|
||||
|
||||
// Default user power level for the room
|
||||
usersDefault: PropTypes.number.isRequired,
|
||||
|
||||
// should the user be able to change the value? false by default.
|
||||
disabled: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
|
||||
// Optional key to pass as the second argument to `onChange`
|
||||
powerLevelKey: PropTypes.string,
|
||||
|
||||
// The name to annotate the selector with
|
||||
label: PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
export default class PowerSelector extends React.Component<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
maxValue: Infinity,
|
||||
usersDefault: 0,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this._initStateFromProps(this.props);
|
||||
// eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
|
||||
public UNSAFE_componentWillMount(): void {
|
||||
this.initStateFromProps(this.props);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
this._initStateFromProps(newProps);
|
||||
// eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
|
||||
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
||||
this.initStateFromProps(newProps);
|
||||
}
|
||||
|
||||
_initStateFromProps(newProps) {
|
||||
private initStateFromProps(newProps: IProps): void {
|
||||
// This needs to be done now because levelRoleMap has translated strings
|
||||
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
||||
const options = Object.keys(levelRoleMap).filter(level => {
|
||||
return (
|
||||
level === undefined ||
|
||||
level <= newProps.maxValue ||
|
||||
level == newProps.value
|
||||
parseInt(level) <= newProps.maxValue ||
|
||||
parseInt(level) == newProps.value
|
||||
);
|
||||
});
|
||||
}).map(level => parseInt(level));
|
||||
|
||||
const isCustom = levelRoleMap[newProps.value] === undefined;
|
||||
|
||||
|
@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
|
|||
options,
|
||||
custom: isCustom,
|
||||
customLevel: newProps.value,
|
||||
selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value,
|
||||
selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
|
||||
});
|
||||
}
|
||||
|
||||
onSelectChange = event => {
|
||||
const isCustom = event.target.value === "SELECT_VALUE_CUSTOM";
|
||||
private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
const isCustom = event.target.value === CUSTOM_VALUE;
|
||||
if (isCustom) {
|
||||
this.setState({ custom: true });
|
||||
} else {
|
||||
this.props.onChange(event.target.value, this.props.powerLevelKey);
|
||||
this.setState({ selectValue: event.target.value });
|
||||
const powerLevel = parseInt(event.target.value);
|
||||
this.props.onChange(powerLevel, this.props.powerLevelKey);
|
||||
this.setState({ selectValue: powerLevel });
|
||||
}
|
||||
};
|
||||
|
||||
onCustomChange = event => {
|
||||
this.setState({ customValue: event.target.value });
|
||||
private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({ customValue: parseInt(event.target.value) });
|
||||
};
|
||||
|
||||
onCustomBlur = event => {
|
||||
private onCustomBlur = (event: React.FocusEvent): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
|
||||
this.props.onChange(this.state.customValue, this.props.powerLevelKey);
|
||||
};
|
||||
|
||||
onCustomKeyDown = event => {
|
||||
private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (event.key === Key.ENTER) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
|
|||
// raising a dialog which causes a blur which causes a dialog which causes a blur and
|
||||
// so on. By not causing the onChange to be called here, we avoid the loop because we
|
||||
// handle the onBlur safely.
|
||||
event.target.blur();
|
||||
(event.target as HTMLInputElement).blur();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let picker;
|
||||
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
||||
if (this.state.custom) {
|
||||
|
@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
|
|||
);
|
||||
} else {
|
||||
// Each level must have a definition in this.state.levelRoleMap
|
||||
let options = this.state.options.map((level) => {
|
||||
const options = this.state.options.map((level) => {
|
||||
return {
|
||||
value: level,
|
||||
value: String(level),
|
||||
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||
};
|
||||
});
|
||||
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
||||
options = options.map((op) => {
|
||||
options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
|
||||
const optionsElements = options.map((op) => {
|
||||
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
||||
});
|
||||
|
||||
|
@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
|
|||
value={String(this.state.selectValue)}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{ options }
|
||||
{ optionsElements }
|
||||
</Field>
|
||||
);
|
||||
}
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
@ -35,6 +37,12 @@ import ReplyTile from "../rooms/ReplyTile";
|
|||
import Pill from './Pill';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
/**
|
||||
* This number is based on the previous behavior - if we have message of height
|
||||
* over 60px then we want to show button that will allow to expand it.
|
||||
*/
|
||||
const SHOW_EXPAND_QUOTE_PIXELS = 60;
|
||||
|
||||
interface IProps {
|
||||
// the latest event in this chain of replies
|
||||
parentEv?: MatrixEvent;
|
||||
|
@ -45,6 +53,8 @@ interface IProps {
|
|||
layout?: Layout;
|
||||
// Whether to always show a timestamp
|
||||
alwaysShowTimestamps?: boolean;
|
||||
isQuoteExpanded?: boolean;
|
||||
setQuoteExpanded: (isExpanded: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -66,6 +76,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
static contextType = MatrixClientContext;
|
||||
private unmounted = false;
|
||||
private room: Room;
|
||||
private blockquoteRef = React.createRef<HTMLElement>();
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -80,7 +91,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||
}
|
||||
|
||||
public static getParentEventId(ev: MatrixEvent): string {
|
||||
public static getParentEventId(ev: MatrixEvent): string | undefined {
|
||||
if (!ev || ev.isRedacted()) return;
|
||||
|
||||
// XXX: For newer relations (annotations, replacements, etc.), we now
|
||||
|
@ -88,7 +99,13 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
// could be used here for replies as well... However, the helper
|
||||
// currently assumes the relation has a `rel_type`, which older replies
|
||||
// do not, so this block is left as-is for now.
|
||||
const mRelatesTo = ev.getWireContent()['m.relates_to'];
|
||||
//
|
||||
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||
// we grab the latest edit with potentially new relations. But we also
|
||||
// can't just rely on ev.getContent() by itself because historically we
|
||||
// still show the reply from the original message even though the edit
|
||||
// event does not include the relation reply.
|
||||
const mRelatesTo = ev.getContent()['m.relates_to'] || ev.getWireContent()['m.relates_to'];
|
||||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||
const mInReplyTo = mRelatesTo['m.in_reply_to'];
|
||||
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
|
||||
|
@ -131,7 +148,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
public static getNestedReplyText(
|
||||
ev: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
): { body: string, html: string } {
|
||||
): { body: string, html: string } | null {
|
||||
if (!ev) return null;
|
||||
|
||||
let { body, formatted_body: html } = ev.getContent();
|
||||
|
@ -231,37 +248,38 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
return replyMixin;
|
||||
}
|
||||
|
||||
public static makeThread(
|
||||
parentEv: MatrixEvent,
|
||||
onHeightChanged: () => void,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
ref: React.RefObject<ReplyThread>,
|
||||
layout: Layout,
|
||||
alwaysShowTimestamps: boolean,
|
||||
): JSX.Element {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) return null;
|
||||
return <ReplyThread
|
||||
parentEv={parentEv}
|
||||
onHeightChanged={onHeightChanged}
|
||||
ref={ref}
|
||||
permalinkCreator={permalinkCreator}
|
||||
layout={layout}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
/>;
|
||||
public static hasThreadReply(event: MatrixEvent) {
|
||||
return Boolean(ReplyThread.getParentEventId(event));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initialize();
|
||||
this.trySetExpandableQuotes();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.onHeightChanged();
|
||||
this.trySetExpandableQuotes();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private trySetExpandableQuotes() {
|
||||
if (this.props.isQuoteExpanded === undefined && this.blockquoteRef.current) {
|
||||
const el: HTMLElement | null = this.blockquoteRef.current.querySelector('.mx_EventTile_body');
|
||||
if (el) {
|
||||
const code: HTMLElement | null = el.querySelector('code');
|
||||
const isCodeEllipsisShown = code ? code.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS : false;
|
||||
const isElipsisShown = el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || isCodeEllipsisShown;
|
||||
if (isElipsisShown) {
|
||||
this.props.setQuoteExpanded(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
const { parentEv } = this.props;
|
||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||
|
@ -315,7 +333,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
this.initialize();
|
||||
};
|
||||
|
||||
private onQuoteClick = async (): Promise<void> => {
|
||||
private onQuoteClick = async (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => {
|
||||
const events = [this.state.loadedEv, ...this.state.events];
|
||||
|
||||
let loadedEv = null;
|
||||
|
@ -367,14 +385,26 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||
header = <Spinner w={16} h={16} />;
|
||||
}
|
||||
|
||||
const { isQuoteExpanded } = this.props;
|
||||
const evTiles = this.state.events.map((ev) => {
|
||||
return <blockquote className={`mx_ReplyThread ${this.getReplyThreadColorClass(ev)}`} key={ev.getId()}>
|
||||
<ReplyTile
|
||||
mxEvent={ev}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>
|
||||
</blockquote>;
|
||||
const classname = classNames({
|
||||
'mx_ReplyThread': true,
|
||||
[this.getReplyThreadColorClass(ev)]: true,
|
||||
// We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
|
||||
'mx_ReplyThread--expanded': isQuoteExpanded === true,
|
||||
// We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
|
||||
'mx_ReplyThread--collapsed': isQuoteExpanded === false,
|
||||
});
|
||||
return (
|
||||
<blockquote ref={this.blockquoteRef} className={classname} key={ev.getId()}>
|
||||
<ReplyTile
|
||||
mxEvent={ev}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
toggleExpandedQuote={() => this.props.setQuoteExpanded(!this.props.isQuoteExpanded)}
|
||||
/>
|
||||
</blockquote>
|
||||
);
|
||||
});
|
||||
|
||||
return <div className="mx_ReplyThread_wrapper">
|
||||
|
|
|
@ -17,25 +17,34 @@
|
|||
import React from 'react';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
reason?: string;
|
||||
contentHtml: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.Spoiler")
|
||||
export default class Spoiler extends React.Component {
|
||||
constructor(props) {
|
||||
export default class Spoiler extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleVisible(e) {
|
||||
private toggleVisible = (e: React.MouseEvent): void => {
|
||||
if (!this.state.visible) {
|
||||
// we are un-blurring, we don't want this click to propagate to potential child pills
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
this.setState({ visible: !this.state.visible });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const reason = this.props.reason ? (
|
||||
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
|
||||
) : null;
|
||||
|
@ -43,7 +52,7 @@ export default class Spoiler extends React.Component {
|
|||
// as such, we pass the this.props.contentHtml instead and then set the raw
|
||||
// HTML content. This is secure as the contents have already been parsed previously
|
||||
return (
|
||||
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}>
|
||||
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
|
||||
{ reason }
|
||||
|
||||
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
|
@ -15,40 +15,40 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { highlightBlock } from 'highlight.js';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.SyntaxHighlight")
|
||||
export default class SyntaxHighlight extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
export default class SyntaxHighlight extends React.Component<IProps> {
|
||||
private el: HTMLPreElement = null;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._ref = this._ref.bind(this);
|
||||
}
|
||||
|
||||
// componentDidUpdate used here for reusability
|
||||
componentDidUpdate() {
|
||||
if (this._el) highlightBlock(this._el);
|
||||
public componentDidUpdate(): void {
|
||||
if (this.el) highlightBlock(this.el);
|
||||
}
|
||||
|
||||
// call componentDidUpdate because _ref is fired on initial render
|
||||
// which does not fire componentDidUpdate
|
||||
_ref(el) {
|
||||
this._el = el;
|
||||
private ref = (el: HTMLPreElement): void => {
|
||||
this.el = el;
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const { className, children } = this.props;
|
||||
|
||||
return <pre className={`${className} mx_SyntaxHighlight`} ref={this._ref}>
|
||||
return <pre className={`${className} mx_SyntaxHighlight`} ref={this.ref}>
|
||||
<code>{ children }</code>
|
||||
</pre>;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,42 +15,44 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
interface IProps {
|
||||
class?: string;
|
||||
tooltipClass?: string;
|
||||
tooltip: React.ReactNode;
|
||||
tooltipProps?: {};
|
||||
onClick?: (ev?: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.TextWithTooltip")
|
||||
export default class TextWithTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
class: PropTypes.string,
|
||||
tooltipClass: PropTypes.string,
|
||||
tooltip: PropTypes.node.isRequired,
|
||||
tooltipProps: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
export default class TextWithTooltip extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
private onMouseOver = (): void => {
|
||||
this.setState({ hover: true });
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
private onMouseLeave = (): void => {
|
||||
this.setState({ hover: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick} className={className}>
|
||||
{ children }
|
||||
{ this.state.hover && <Tooltip
|
||||
{...tooltipProps}
|
|
@ -15,20 +15,20 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
import QRCode from "../QRCode";
|
||||
import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
|
||||
interface IProps {
|
||||
qrCodeData: QRCodeData;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||
export default class VerificationQRCode extends React.PureComponent {
|
||||
static propTypes = {
|
||||
qrCodeData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
export default class VerificationQRCode extends React.PureComponent<IProps> {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<QRCode
|
||||
data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]}
|
||||
data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
|
||||
className="mx_VerificationQRCode"
|
||||
width={196} />
|
||||
);
|
|
@ -15,107 +15,112 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
import { formatTime } from '../../../DateUtils';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import classNames from 'classnames';
|
||||
import RedactedBody from "./RedactedBody";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
|
||||
import ViewSource from "../../structures/ViewSource";
|
||||
|
||||
function getReplacedContent(event) {
|
||||
const originalContent = event.getOriginalContent();
|
||||
return originalContent["m.new_content"] || originalContent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.EditHistoryMessage")
|
||||
export default class EditHistoryMessage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// the message event being edited
|
||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
previousEdit: PropTypes.instanceOf(MatrixEvent),
|
||||
isBaseEvent: PropTypes.bool,
|
||||
};
|
||||
interface IProps {
|
||||
// the message event being edited
|
||||
mxEvent: MatrixEvent;
|
||||
previousEdit?: MatrixEvent;
|
||||
isBaseEvent?: boolean;
|
||||
isTwelveHour?: boolean;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IState {
|
||||
canRedact: boolean;
|
||||
sendStatus: EventStatus;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.EditHistoryMessage")
|
||||
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
|
||||
private content = createRef<HTMLDivElement>();
|
||||
private pills: Element[] = [];
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const { userId } = cli.credentials;
|
||||
const event = this.props.mxEvent;
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().on("status", this._onAssociatedStatusChanged);
|
||||
event.localRedactionEvent().on("status", this.onAssociatedStatusChanged);
|
||||
}
|
||||
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
|
||||
this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
|
||||
|
||||
this._content = createRef();
|
||||
this._pills = [];
|
||||
}
|
||||
|
||||
_onAssociatedStatusChanged = () => {
|
||||
private onAssociatedStatusChanged = (): void => {
|
||||
this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() });
|
||||
};
|
||||
|
||||
_onRedactClick = async () => {
|
||||
private onRedactClick = async (): Promise<void> => {
|
||||
const event = this.props.mxEvent;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ConfirmAndWaitRedactDialog = sdk.getComponent("dialogs.ConfirmAndWaitRedactDialog");
|
||||
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, {
|
||||
redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
};
|
||||
|
||||
_onViewSourceClick = () => {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
private onViewSourceClick = (): void => {
|
||||
Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
};
|
||||
|
||||
pillifyLinks() {
|
||||
private pillifyLinks(): void {
|
||||
// not present for redacted events
|
||||
if (this._content.current) {
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills);
|
||||
if (this.content.current) {
|
||||
pillifyLinks(this.content.current.children, this.props.mxEvent, this.pills);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.pillifyLinks();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unmountPills(this._pills);
|
||||
public componentWillUnmount(): void {
|
||||
unmountPills(this.pills);
|
||||
const event = this.props.mxEvent;
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
||||
event.localRedactionEvent().off("status", this.onAssociatedStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
public componentDidUpdate(): void {
|
||||
this.pillifyLinks();
|
||||
}
|
||||
|
||||
_renderActionBar() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
private renderActionBar(): JSX.Element {
|
||||
// hide the button when already redacted
|
||||
let redactButton;
|
||||
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<AccessibleButton onClick={this._onRedactClick}>
|
||||
<AccessibleButton onClick={this.onRedactClick}>
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
const viewSourceButton = (
|
||||
<AccessibleButton onClick={this._onViewSourceClick}>
|
||||
<AccessibleButton onClick={this.onViewSourceClick}>
|
||||
{ _t("View Source") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -128,7 +133,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const { mxEvent } = this.props;
|
||||
const content = getReplacedContent(mxEvent);
|
||||
let contentContainer;
|
||||
|
@ -139,18 +144,22 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
if (this.props.previousEdit) {
|
||||
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
|
||||
} else {
|
||||
contentElements = HtmlUtils.bodyToHtml(content, null, { stripReplyFallback: true });
|
||||
contentElements = HtmlUtils.bodyToHtml(
|
||||
content,
|
||||
null,
|
||||
{ stripReplyFallback: true, returnString: false },
|
||||
);
|
||||
}
|
||||
if (mxEvent.getContent().msgtype === "m.emote") {
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
contentContainer = (
|
||||
<div className="mx_EventTile_content" ref={this._content}>*
|
||||
<div className="mx_EventTile_content" ref={this.content}>*
|
||||
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||
{ contentElements }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{ contentElements }</div>;
|
||||
contentContainer = <div className="mx_EventTile_content" ref={this.content}>{ contentElements }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,7 +176,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
<div className="mx_EventTile_line">
|
||||
<span className="mx_MessageTimestamp">{ timestamp }</span>
|
||||
{ contentContainer }
|
||||
{ this._renderActionBar() }
|
||||
{ this.renderActionBar() }
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -29,6 +29,8 @@ import { IBodyProps } from "./IBodyProps";
|
|||
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||
|
||||
async function cacheDownloadIcon() {
|
||||
|
@ -283,7 +285,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
if (["application/pdf"].includes(fileType) && !fileTooBig) {
|
||||
// We want to force a download on this type, so use an onClick handler.
|
||||
downloadProps["onClick"] = (e) => {
|
||||
console.log(`Downloading ${fileType} as blob (unencrypted)`);
|
||||
logger.log(`Downloading ${fileType} as blob (unencrypted)`);
|
||||
|
||||
// Avoid letting the <a> do its thing
|
||||
e.preventDefault();
|
||||
|
|
|
@ -117,6 +117,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
params.fileSize = content.info.size;
|
||||
}
|
||||
|
||||
if (this.image.current) {
|
||||
const clientRect = this.image.current.getBoundingClientRect();
|
||||
|
||||
params.thumbnailInfo = {
|
||||
width: clientRect.width,
|
||||
height: clientRect.height,
|
||||
positionX: clientRect.x,
|
||||
positionY: clientRect.y,
|
||||
};
|
||||
}
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -16,44 +16,50 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { getNameForEventRoom, userLabelForEventRoom }
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationConclusion")
|
||||
export default class MKeyVerificationConclusion extends React.Component {
|
||||
constructor(props) {
|
||||
export default class MKeyVerificationConclusion extends React.Component<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.on("change", this._onRequestChanged);
|
||||
request.on("change", this.onRequestChanged);
|
||||
}
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this._onTrustChanged);
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this.onTrustChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.off("change", this._onRequestChanged);
|
||||
request.off("change", this.onRequestChanged);
|
||||
}
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("userTrustStatusChanged", this._onTrustChanged);
|
||||
cli.removeListener("userTrustStatusChanged", this.onTrustChanged);
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestChanged = () => {
|
||||
private onRequestChanged = (): void => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_onTrustChanged = (userId, status) => {
|
||||
private onTrustChanged = (userId: string): void => {
|
||||
const { mxEvent } = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
if (!request || request.otherUserId !== userId) {
|
||||
|
@ -62,17 +68,17 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_shouldRender(mxEvent, request) {
|
||||
public static shouldRender(mxEvent: MatrixEvent, request: VerificationRequest): boolean {
|
||||
// normally should not happen
|
||||
if (!request) {
|
||||
return false;
|
||||
}
|
||||
// .cancel event that was sent after the verification finished, ignore
|
||||
if (mxEvent.getType() === "m.key.verification.cancel" && !request.cancelled) {
|
||||
if (mxEvent.getType() === EventType.KeyVerificationCancel && !request.cancelled) {
|
||||
return false;
|
||||
}
|
||||
// .done event that was sent after the verification cancelled, ignore
|
||||
if (mxEvent.getType() === "m.key.verification.done" && !request.done) {
|
||||
if (mxEvent.getType() === EventType.KeyVerificationDone && !request.done) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -89,11 +95,11 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const { mxEvent } = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
|
||||
if (!this._shouldRender(mxEvent, request)) {
|
||||
if (!MKeyVerificationConclusion.shouldRender(mxEvent, request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -103,15 +109,18 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
let title;
|
||||
|
||||
if (request.done) {
|
||||
title = _t("You verified %(name)s", { name: getNameForEventRoom(request.otherUserId, mxEvent) });
|
||||
title = _t(
|
||||
"You verified %(name)s",
|
||||
{ name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) },
|
||||
);
|
||||
} else if (request.cancelled) {
|
||||
const userId = request.cancellingUserId;
|
||||
if (userId === myUserId) {
|
||||
title = _t("You cancelled verifying %(name)s",
|
||||
{ name: getNameForEventRoom(request.otherUserId, mxEvent) });
|
||||
{ name: getNameForEventRoom(request.otherUserId, mxEvent.getRoomId()) });
|
||||
} else {
|
||||
title = _t("%(name)s cancelled verifying",
|
||||
{ name: getNameForEventRoom(userId, mxEvent) });
|
||||
{ name: getNameForEventRoom(userId, mxEvent.getRoomId()) });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,8 +138,3 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MKeyVerificationConclusion.propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
|
@ -27,6 +27,8 @@ import { IMediaEventContent } from "../../../customisations/models/IMediaEventCo
|
|||
import { IBodyProps } from "./IBodyProps";
|
||||
import MFileBody from "./MFileBody";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
interface IState {
|
||||
decryptedUrl?: string;
|
||||
decryptedThumbnailUrl?: string;
|
||||
|
@ -152,7 +154,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
try {
|
||||
const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
|
||||
if (autoplay) {
|
||||
console.log("Preloading video");
|
||||
logger.log("Preloading video");
|
||||
this.setState({
|
||||
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
|
@ -160,7 +162,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
|||
});
|
||||
this.props.onHeightChanged();
|
||||
} else {
|
||||
console.log("NOT preloading video");
|
||||
logger.log("NOT preloading video");
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
this.setState({
|
||||
// For Chrome and Electron, we need to set some non-empty `src` to
|
||||
|
|
|
@ -17,7 +17,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import type { Relations } from 'matrix-js-sdk/src/models/relations';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -35,13 +36,17 @@ import Resend from "../../../Resend";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import DownloadActionButton from "./DownloadActionButton";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import classNames from 'classnames';
|
||||
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import ReplyThread from '../elements/ReplyThread';
|
||||
|
||||
interface IOptionsButtonProps {
|
||||
mxEvent: MatrixEvent;
|
||||
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
|
||||
// TODO: Types
|
||||
getTile: () => any | null;
|
||||
getReplyThread: () => ReplyThread;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
onFocusChange: (menuDisplayed: boolean) => void;
|
||||
|
@ -57,8 +62,6 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
|
|||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
|
@ -90,7 +93,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
|
|||
|
||||
interface IReactButtonProps {
|
||||
mxEvent: MatrixEvent;
|
||||
reactions: any; // TODO: types
|
||||
reactions: Relations;
|
||||
onFocusChange: (menuDisplayed: boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -127,12 +130,14 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
|||
|
||||
interface IMessageActionBarProps {
|
||||
mxEvent: MatrixEvent;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions?: any; // TODO: types
|
||||
reactions?: Relations;
|
||||
// TODO: Types
|
||||
getTile: () => any | null;
|
||||
getReplyThread: () => ReplyThread | undefined;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
|
||||
getReplyThread?: () => ReplyThread;
|
||||
onFocusChange?: (menuDisplayed: boolean) => void;
|
||||
onFocusChange: (menuDisplayed: boolean) => void;
|
||||
isQuoteExpanded?: boolean;
|
||||
toggleThreadExpanded: () => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MessageActionBar")
|
||||
|
@ -324,6 +329,20 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
toolbarOpts.push(cancelSendingButton);
|
||||
}
|
||||
|
||||
if (this.props.isQuoteExpanded !== undefined && ReplyThread.hasThreadReply(this.props.mxEvent)) {
|
||||
const expandClassName = classNames({
|
||||
'mx_MessageActionBar_maskButton': true,
|
||||
'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded,
|
||||
'mx_MessageActionBar_collapseMessageButton': this.props.isQuoteExpanded,
|
||||
});
|
||||
toolbarOpts.push(<RovingAccessibleTooltipButton
|
||||
className={expandClassName}
|
||||
title={this.props.isQuoteExpanded ? _t("Collapse quotes │ ⇧+click") : _t("Expand quotes │ ⇧+click")}
|
||||
onClick={this.props.toggleThreadExpanded}
|
||||
key="expand"
|
||||
/>);
|
||||
}
|
||||
|
||||
// The menu button should be last, so dump it there.
|
||||
toolbarOpts.push(<OptionsButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
|
|
@ -15,22 +15,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onMessageAllowed: () => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MjolnirBody")
|
||||
export default class MjolnirBody extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
onMessageAllowed: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
_onAllowClick = (e) => {
|
||||
export default class MjolnirBody extends React.Component<IProps> {
|
||||
private onAllowClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -39,11 +35,11 @@ export default class MjolnirBody extends React.Component {
|
|||
this.props.onMessageAllowed();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className='mx_MjolnirBody'><i>{ _t(
|
||||
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
|
||||
{}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{ sub }</a> },
|
||||
{}, { a: (sub) => <a href="#" onClick={this.onAllowClick}>{ sub }</a> },
|
||||
) }</i></div>
|
||||
);
|
||||
}
|
|
@ -106,31 +106,20 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
|
|||
}
|
||||
|
||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||
let label;
|
||||
let label: string;
|
||||
if (room) {
|
||||
const senders = [];
|
||||
for (const reactionEvent of reactionEvents) {
|
||||
const member = room.getMember(reactionEvent.getSender());
|
||||
const name = member ? member.name : reactionEvent.getSender();
|
||||
senders.push(name);
|
||||
senders.push(member?.name || reactionEvent.getSender());
|
||||
}
|
||||
|
||||
const reactors = formatCommaSeparatedList(senders, 6);
|
||||
if (content) {
|
||||
label = _t("%(reactors)s reacted with %(content)s", { reactors, content });
|
||||
} else {
|
||||
label = reactors;
|
||||
}
|
||||
label = _t(
|
||||
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
||||
{
|
||||
content,
|
||||
},
|
||||
{
|
||||
reactors: () => {
|
||||
return formatCommaSeparatedList(senders, 6);
|
||||
},
|
||||
reactedWith: (sub) => {
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
return sub;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
const isPeeking = room.getMyMembership() !== "join";
|
||||
return <AccessibleButton
|
||||
|
|
|
@ -16,13 +16,18 @@ limitations under the License.
|
|||
|
||||
import React, { useContext } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
|
||||
const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent }, ref) => {
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
const RedactedBody = React.forwardRef<any, IProps | IBodyProps>(({ mxEvent }, ref) => {
|
||||
const cli: MatrixClient = useContext(MatrixClientContext);
|
||||
|
||||
let text = _t("Message deleted");
|
||||
|
|
|
@ -17,23 +17,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import ImageView from "../elements/ImageView";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.RoomAvatarEvent")
|
||||
export default class RoomAvatarEvent extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
onAvatarClick = () => {
|
||||
export default class RoomAvatarEvent extends React.Component<IProps> {
|
||||
private onAvatarClick = (): void => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
|
||||
|
@ -44,7 +45,6 @@ export default class RoomAvatarEvent extends React.Component {
|
|||
roomName: room ? room.name : '',
|
||||
});
|
||||
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: text,
|
||||
|
@ -52,10 +52,9 @@ export default class RoomAvatarEvent extends React.Component {
|
|||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const ev = this.props.mxEvent;
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
|
||||
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
|
||||
return (
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
|
@ -24,15 +23,16 @@ import { _t } from '../../../languageHandler';
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.RoomCreate")
|
||||
export default class RoomCreate extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
_onLinkClicked = e => {
|
||||
export default class RoomCreate extends React.Component<IProps> {
|
||||
private onLinkClicked = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
|
@ -45,7 +45,7 @@ export default class RoomCreate extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
if (predecessor === undefined) {
|
||||
return <div />; // We should never have been instantiated in this case
|
||||
|
@ -55,7 +55,7 @@ export default class RoomCreate extends React.Component {
|
|||
permalinkCreator.load();
|
||||
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
|
||||
const link = (
|
||||
<a href={predecessorPermalink} onClick={this._onLinkClicked}>
|
||||
<a href={predecessorPermalink} onClick={this.onLinkClicked}>
|
||||
{ _t("Click here to see older messages.") }
|
||||
</a>
|
||||
);
|
|
@ -138,6 +138,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
// If it's less than 30% we don't add the expansion button.
|
||||
// We also round the number as it sometimes can be 29.99...
|
||||
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
|
||||
// TODO: additionally show the button if it's an expanded quoted message
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
|
|
@ -71,6 +71,8 @@ import UIStore from "../../../stores/UIStore";
|
|||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
ambiguous?: boolean;
|
||||
|
@ -557,7 +559,7 @@ const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdat
|
|||
cli.kick(member.roomId, member.userId, reason || undefined).then(() => {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Kick success");
|
||||
logger.log("Kick success");
|
||||
}, function(err) {
|
||||
console.error("Kick error: " + err);
|
||||
Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
|
||||
|
@ -684,7 +686,7 @@ const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpda
|
|||
promise.then(() => {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Ban success");
|
||||
logger.log("Ban success");
|
||||
}, function(err) {
|
||||
console.error("Ban error: " + err);
|
||||
Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
|
||||
|
@ -757,7 +759,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({ member, room, powerLevels,
|
|||
cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Mute toggle success");
|
||||
logger.log("Mute toggle success");
|
||||
}, function(err) {
|
||||
console.error("Mute error: " + err);
|
||||
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
|
||||
|
@ -917,7 +919,7 @@ const GroupAdminToolsSection: React.FC<{
|
|||
_t('Failed to withdraw invitation') :
|
||||
_t('Failed to remove user from community'),
|
||||
});
|
||||
console.log(e);
|
||||
logger.log(e);
|
||||
}).finally(() => {
|
||||
stopUpdating();
|
||||
});
|
||||
|
@ -1052,8 +1054,7 @@ const PowerLevelEditor: React.FC<{
|
|||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
||||
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
||||
const powerLevel = parseInt(powerLevelStr, 10);
|
||||
const onPowerChange = useCallback(async (powerLevel: number) => {
|
||||
setSelectedPowerLevel(powerLevel);
|
||||
|
||||
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||
|
@ -1061,7 +1062,7 @@ const PowerLevelEditor: React.FC<{
|
|||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Power change success");
|
||||
logger.log("Power change success");
|
||||
}, function(err) {
|
||||
console.error("Failed to change power level " + err);
|
||||
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
||||
|
|
|
@ -28,7 +28,7 @@ import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS";
|
|||
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import E2EIcon, { E2EState } from "../rooms/E2EIcon";
|
||||
import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -189,7 +189,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
||||
body = <React.Fragment>
|
||||
<p>{ description }</p>
|
||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
||||
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
|
@ -252,7 +252,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
||||
<h3>{ _t("Verified") }</h3>
|
||||
<p>{ description }</p>
|
||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
||||
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||
{ text ? <p>{ text }</p> : null }
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
||||
{ _t("Got it") }
|
||||
|
|
|
@ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
<AppTile
|
||||
app={app}
|
||||
fullWidth
|
||||
show
|
||||
showMenubar={false}
|
||||
room={room}
|
||||
userId={cli.getUserId()}
|
||||
|
|
|
@ -15,27 +15,43 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import * as sdk from "../../../index";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AvatarSetting from "../settings/AvatarSetting";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
originalDisplayName: string;
|
||||
displayName: string;
|
||||
originalAvatarUrl: string;
|
||||
avatarUrl: string;
|
||||
avatarFile: File;
|
||||
originalTopic: string;
|
||||
topic: string;
|
||||
enableProfileSave: boolean;
|
||||
canSetName: boolean;
|
||||
canSetTopic: boolean;
|
||||
canSetAvatar: boolean;
|
||||
}
|
||||
|
||||
// TODO: Merge with ProfileSettings?
|
||||
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
||||
export default class RoomProfileSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class RoomProfileSettings extends React.Component<IProps, IState> {
|
||||
private avatarUpload = createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(props.roomId);
|
||||
if (!room) throw new Error("Expected a room for ID: ", props.roomId);
|
||||
if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`);
|
||||
|
||||
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
||||
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
|
||||
|
@ -60,17 +76,15 @@ export default class RoomProfileSettings extends React.Component {
|
|||
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
||||
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
||||
};
|
||||
|
||||
this._avatarUpload = createRef();
|
||||
}
|
||||
|
||||
_uploadAvatar = () => {
|
||||
this._avatarUpload.current.click();
|
||||
private uploadAvatar = (): void => {
|
||||
this.avatarUpload.current.click();
|
||||
};
|
||||
|
||||
_removeAvatar = () => {
|
||||
private removeAvatar = (): void => {
|
||||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
|
@ -78,7 +92,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_cancelProfileChanges = async (e) => {
|
||||
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -92,7 +106,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
private saveProfile = async (e: React.FormEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -100,35 +114,46 @@ export default class RoomProfileSettings extends React.Component {
|
|||
this.setState({ enableProfileSave: false });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const newState = {};
|
||||
|
||||
let originalDisplayName: string;
|
||||
let avatarUrl: string;
|
||||
let originalAvatarUrl: string;
|
||||
let originalTopic: string;
|
||||
let avatarFile: File;
|
||||
|
||||
// TODO: What do we do about errors?
|
||||
const displayName = this.state.displayName.trim();
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setRoomName(this.props.roomId, displayName);
|
||||
newState.originalDisplayName = displayName;
|
||||
newState.displayName = displayName;
|
||||
originalDisplayName = displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
|
||||
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||
originalAvatarUrl = avatarUrl;
|
||||
avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
||||
}
|
||||
|
||||
if (this.state.originalTopic !== this.state.topic) {
|
||||
await client.setRoomTopic(this.props.roomId, this.state.topic);
|
||||
newState.originalTopic = this.state.topic;
|
||||
originalTopic = this.state.topic;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
this.setState({
|
||||
originalAvatarUrl,
|
||||
avatarUrl,
|
||||
originalDisplayName,
|
||||
originalTopic,
|
||||
displayName,
|
||||
avatarFile,
|
||||
});
|
||||
};
|
||||
|
||||
_onDisplayNameChanged = (e) => {
|
||||
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({ displayName: e.target.value });
|
||||
if (this.state.originalDisplayName === e.target.value) {
|
||||
this.setState({ enableProfileSave: false });
|
||||
|
@ -137,7 +162,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onTopicChanged = (e) => {
|
||||
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({ topic: e.target.value });
|
||||
if (this.state.originalTopic === e.target.value) {
|
||||
this.setState({ enableProfileSave: false });
|
||||
|
@ -146,7 +171,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onAvatarChanged = (e) => {
|
||||
private onAvatarChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (!e.target.files || !e.target.files.length) {
|
||||
this.setState({
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
|
@ -160,7 +185,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
this.setState({
|
||||
avatarUrl: ev.target.result,
|
||||
avatarUrl: String(ev.target.result),
|
||||
avatarFile: file,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
|
@ -168,10 +193,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
|
||||
public render(): JSX.Element {
|
||||
let profileSettingsButtons;
|
||||
if (
|
||||
this.state.canSetName ||
|
||||
|
@ -181,14 +203,14 @@ export default class RoomProfileSettings extends React.Component {
|
|||
profileSettingsButtons = (
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._cancelProfileChanges}
|
||||
onClick={this.cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
onClick={this.saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
|
@ -200,16 +222,16 @@ export default class RoomProfileSettings extends React.Component {
|
|||
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
onSubmit={this.saveProfile}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
ref={this._avatarUpload}
|
||||
ref={this.avatarUpload}
|
||||
className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged}
|
||||
onChange={this.onAvatarChanged}
|
||||
accept="image/*"
|
||||
/>
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
|
@ -219,7 +241,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
type="text"
|
||||
value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged}
|
||||
onChange={this.onDisplayNameChanged}
|
||||
disabled={!this.state.canSetName}
|
||||
/>
|
||||
<Field
|
||||
|
@ -230,7 +252,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
type="text"
|
||||
value={this.state.topic}
|
||||
autoComplete="off"
|
||||
onChange={this._onTopicChanged}
|
||||
onChange={this.onTopicChanged}
|
||||
element="textarea"
|
||||
/>
|
||||
</div>
|
||||
|
@ -238,8 +260,8 @@ export default class RoomProfileSettings extends React.Component {
|
|||
avatarUrl={this.state.avatarUrl}
|
||||
avatarName={this.state.displayName || this.props.roomId}
|
||||
avatarAltText={_t("Room avatar")}
|
||||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
||||
uploadAvatar={this.state.canSetAvatar ? this.uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this.removeAvatar : undefined} />
|
||||
</div>
|
||||
{ profileSettingsButtons }
|
||||
</form>
|
|
@ -18,8 +18,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
@ -27,21 +25,22 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|||
import { Action } from "../../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.room_settings.UrlPreviewSettings")
|
||||
export default class UrlPreviewSettings extends React.Component {
|
||||
static propTypes = {
|
||||
room: PropTypes.object,
|
||||
};
|
||||
|
||||
_onClickUserSettings = (e) => {
|
||||
export default class UrlPreviewSettings extends React.Component<IProps> {
|
||||
private onClickUserSettings = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
|
||||
render() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
public render(): JSX.Element {
|
||||
const roomId = this.props.room.roomId;
|
||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
||||
|
||||
|
@ -54,18 +53,18 @@ export default class UrlPreviewSettings extends React.Component {
|
|||
if (accountEnabled) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
|
||||
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
|
||||
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Resizable } from "re-resizable";
|
||||
|
||||
|
@ -26,8 +25,6 @@ import * as sdk from '../../../index';
|
|||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer from "../../../resizer/resizer";
|
||||
|
@ -37,60 +34,74 @@ import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
|||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
||||
interface IProps {
|
||||
userId: string;
|
||||
room: Room;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
showApps?: boolean; // Should apps be rendered
|
||||
maxHeight: number;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
apps: IApp[];
|
||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
|
||||
resizing: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.AppsDrawer")
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
||||
showApps: PropTypes.bool, // Should apps be rendered
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
private resizeContainer: HTMLDivElement;
|
||||
private resizer: Resizer;
|
||||
private dispatcherRef: string;
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
showApps: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
apps: this._getApps(),
|
||||
resizingVertical: false, // true when changing the height of the apps drawer
|
||||
resizingHorizontal: false, // true when chagning the distribution of the width between widgets
|
||||
apps: this.getApps(),
|
||||
resizingVertical: false,
|
||||
resizingHorizontal: false,
|
||||
resizing: false,
|
||||
};
|
||||
|
||||
this._resizeContainer = null;
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer = this.createResizer();
|
||||
|
||||
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
ScalarMessaging.startListening();
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
ScalarMessaging.stopListening();
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
if (this._resizeContainer) {
|
||||
if (this.resizeContainer) {
|
||||
this.resizer.detach();
|
||||
}
|
||||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
onIsResizing = (resizing) => {
|
||||
private onIsResizing = (resizing: boolean): void => {
|
||||
// This one is the vertical, ie. change height of apps drawer
|
||||
this.setState({ resizingVertical: resizing });
|
||||
if (!resizing) {
|
||||
this._relaxResizer();
|
||||
this.relaxResizer();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
private createResizer(): Resizer {
|
||||
// This is the horizontal one, changing the distribution of the width between the app tiles
|
||||
// (ie. a vertical resize handle because, the handle itself is vertical...)
|
||||
const classNames = {
|
||||
|
@ -100,11 +111,11 @@ export default class AppsDrawer extends React.Component {
|
|||
};
|
||||
const collapseConfig = {
|
||||
onResizeStart: () => {
|
||||
this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||
this.resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||
this.setState({ resizingHorizontal: true });
|
||||
},
|
||||
onResizeStop: () => {
|
||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
|
@ -113,13 +124,13 @@ export default class AppsDrawer extends React.Component {
|
|||
},
|
||||
};
|
||||
// pass a truthy container for now, we won't call attach until we update it
|
||||
const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
|
||||
const resizer = new Resizer(null, PercentageDistributor, collapseConfig);
|
||||
resizer.setClassNames(classNames);
|
||||
return resizer;
|
||||
}
|
||||
|
||||
_collectResizer = (ref) => {
|
||||
if (this._resizeContainer) {
|
||||
private collectResizer = (ref: HTMLDivElement): void => {
|
||||
if (this.resizeContainer) {
|
||||
this.resizer.detach();
|
||||
}
|
||||
|
||||
|
@ -127,22 +138,22 @@ export default class AppsDrawer extends React.Component {
|
|||
this.resizer.container = ref;
|
||||
this.resizer.attach();
|
||||
}
|
||||
this._resizeContainer = ref;
|
||||
this._loadResizerPreferences();
|
||||
this.resizeContainer = ref;
|
||||
this.loadResizerPreferences();
|
||||
};
|
||||
|
||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
// Room has changed, update apps
|
||||
this._updateApps();
|
||||
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||
this._loadResizerPreferences();
|
||||
this.updateApps();
|
||||
} else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
|
||||
this.loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
_relaxResizer = () => {
|
||||
private relaxResizer = (): void => {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
|
||||
// relax all items if they had any overconstrained flexboxes
|
||||
|
@ -150,7 +161,7 @@ export default class AppsDrawer extends React.Component {
|
|||
distributors.forEach(d => d.finish());
|
||||
};
|
||||
|
||||
_loadResizerPreferences = () => {
|
||||
private loadResizerPreferences = (): void => {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
|
@ -168,11 +179,11 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
isResizing() {
|
||||
private isResizing(): boolean {
|
||||
return this.state.resizingVertical || this.state.resizingHorizontal;
|
||||
}
|
||||
|
||||
onAction = (action) => {
|
||||
private onAction = (action: ActionPayload): void => {
|
||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
|
@ -190,23 +201,15 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
|
||||
_updateApps = () => {
|
||||
private updateApps = (): void => {
|
||||
this.setState({
|
||||
apps: this._getApps(),
|
||||
apps: this.getApps(),
|
||||
});
|
||||
};
|
||||
|
||||
_launchManageIntegrations() {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll();
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (!this.props.showApps) return <div />;
|
||||
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
|
@ -257,7 +260,7 @@ export default class AppsDrawer extends React.Component {
|
|||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className="mx_AppsContainer" ref={this._collectResizer}>
|
||||
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||
{ apps.map((app, i) => {
|
||||
if (i < 1) return app;
|
||||
return <React.Fragment key={app.key}>
|
||||
|
@ -273,7 +276,18 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const PersistentVResizer = ({
|
||||
interface IPersistentResizerProps {
|
||||
room: Room;
|
||||
minHeight: number;
|
||||
maxHeight: number;
|
||||
className: string;
|
||||
handleWrapperClass: string;
|
||||
handleClass: string;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||
room,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
|
@ -303,7 +317,7 @@ const PersistentVResizer = ({
|
|||
});
|
||||
|
||||
return <Resizable
|
||||
size={{ height: Math.min(height, maxHeight) }}
|
||||
size={{ height: Math.min(height, maxHeight), width: undefined }}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
|
@ -181,16 +181,18 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
if (data) {
|
||||
const { partCreator } = model;
|
||||
const moveStart = emoticonMatch[0][0] === " " ? 1 : 0;
|
||||
const moveEnd = emoticonMatch[0].length - emoticonMatch.length - moveStart;
|
||||
const firstMatch = emoticonMatch[0];
|
||||
const moveStart = firstMatch[0] === " " ? 1 : 0;
|
||||
|
||||
// we need the range to only comprise of the emoticon
|
||||
// because we'll replace the whole range with an emoji,
|
||||
// so move the start forward to the start of the emoticon.
|
||||
// Take + 1 because index is reported without the possible preceding space.
|
||||
range.moveStartForwards(emoticonMatch.index + moveStart);
|
||||
// and move end backwards so that we don't replace the trailing space/newline
|
||||
range.moveEndBackwards(moveEnd);
|
||||
// If the end is a trailing space/newline move end backwards, so that we don't replace it
|
||||
if (["\n", " "].includes(firstMatch[firstMatch.length - 1])) {
|
||||
range.moveEndBackwards(1);
|
||||
}
|
||||
|
||||
// this returns the amount of added/removed characters during the replace
|
||||
// so the caret position can be adjusted.
|
||||
|
|
|
@ -16,41 +16,51 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
||||
export const E2E_STATE = {
|
||||
VERIFIED: "verified",
|
||||
WARNING: "warning",
|
||||
UNKNOWN: "unknown",
|
||||
NORMAL: "normal",
|
||||
UNAUTHENTICATED: "unauthenticated",
|
||||
export enum E2EState {
|
||||
Verified = "verified",
|
||||
Warning = "warning",
|
||||
Unknown = "unknown",
|
||||
Normal = "normal",
|
||||
Unauthenticated = "unauthenticated",
|
||||
}
|
||||
|
||||
const crossSigningUserTitles: { [key in E2EState]?: string } = {
|
||||
[E2EState.Warning]: _td("This user has not verified all of their sessions."),
|
||||
[E2EState.Normal]: _td("You have not verified this user."),
|
||||
[E2EState.Verified]: _td("You have verified this user. This user has verified all of their sessions."),
|
||||
};
|
||||
const crossSigningRoomTitles: { [key in E2EState]?: string } = {
|
||||
[E2EState.Warning]: _td("Someone is using an unknown session"),
|
||||
[E2EState.Normal]: _td("This room is end-to-end encrypted"),
|
||||
[E2EState.Verified]: _td("Everyone in this room is verified"),
|
||||
};
|
||||
|
||||
const crossSigningUserTitles = {
|
||||
[E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."),
|
||||
[E2E_STATE.NORMAL]: _td("You have not verified this user."),
|
||||
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."),
|
||||
};
|
||||
const crossSigningRoomTitles = {
|
||||
[E2E_STATE.WARNING]: _td("Someone is using an unknown session"),
|
||||
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
|
||||
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
|
||||
};
|
||||
interface IProps {
|
||||
isUser?: boolean;
|
||||
status?: E2EState | E2EStatus;
|
||||
className?: string;
|
||||
size?: number;
|
||||
onClick?: () => void;
|
||||
hideTooltip?: boolean;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const classes = classNames({
|
||||
mx_E2EIcon: true,
|
||||
mx_E2EIcon_bordered: bordered,
|
||||
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
|
||||
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
|
||||
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
|
||||
mx_E2EIcon_warning: status === E2EState.Warning,
|
||||
mx_E2EIcon_normal: status === E2EState.Normal,
|
||||
mx_E2EIcon_verified: status === E2EState.Verified,
|
||||
}, className);
|
||||
|
||||
let e2eTitle;
|
||||
|
@ -92,12 +102,4 @@ const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, border
|
|||
</div>;
|
||||
};
|
||||
|
||||
E2EIcon.propTypes = {
|
||||
isUser: PropTypes.bool,
|
||||
status: PropTypes.oneOf(Object.values(E2E_STATE)),
|
||||
className: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default E2EIcon;
|
|
@ -27,7 +27,7 @@ import { findEditableEvent } from '../../../utils/EventUtils';
|
|||
import { parseEvent } from '../../../editor/deserialize';
|
||||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
@ -42,6 +42,9 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
|||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -307,7 +310,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
description: errText,
|
||||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
logger.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
@ -315,6 +318,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
private sendEdit = async (): Promise<void> => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
|
||||
// Replace emoticon at the end of the message
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
const caret = this.editorRef.current?.getCaret();
|
||||
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||
}
|
||||
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _td } from '../../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
import E2EIcon from './E2EIcon';
|
||||
import E2EIcon, { E2EState } from './E2EIcon';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import PresenceLabel from "./PresenceLabel";
|
||||
|
@ -75,7 +75,7 @@ interface IProps {
|
|||
suppressOnHover?: boolean;
|
||||
showPresence?: boolean;
|
||||
subtextLabel?: string;
|
||||
e2eStatus?: string;
|
||||
e2eStatus?: E2EState;
|
||||
powerStatus?: PowerStatus;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -33,7 +33,7 @@ import { formatTime } from "../../../DateUtils";
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { E2E_STATE } from "./E2EIcon";
|
||||
import { E2EState } from "./E2EIcon";
|
||||
import { toRem } from "../../../utils/units";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
|
@ -58,6 +58,7 @@ import ReactionsRow from '../messages/ReactionsRow';
|
|||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
|
@ -144,8 +145,7 @@ export function getHandlerTile(ev) {
|
|||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
|
||||
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
||||
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
||||
if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +322,7 @@ interface IState {
|
|||
reactions: Relations;
|
||||
|
||||
hover: boolean;
|
||||
|
||||
isQuoteExpanded?: boolean;
|
||||
thread?: Thread;
|
||||
}
|
||||
|
||||
|
@ -330,7 +330,8 @@ interface IState {
|
|||
export default class EventTile extends React.Component<IProps, IState> {
|
||||
private suppressReadReceiptAnimation: boolean;
|
||||
private isListeningForReceipts: boolean;
|
||||
private tile = React.createRef();
|
||||
// TODO: Types
|
||||
private tile = React.createRef<unknown>();
|
||||
private replyThread = React.createRef<ReplyThread>();
|
||||
|
||||
public readonly ref = createRef<HTMLElement>();
|
||||
|
@ -464,8 +465,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_thread")) {
|
||||
this.props.mxEvent.once("Thread.ready", this.updateThread);
|
||||
this.props.mxEvent.on("Thread.update", this.updateThread);
|
||||
this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
|
||||
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,7 +522,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
const thread = this.state.thread;
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!thread || this.props.showThreadInfo === false) {
|
||||
if (!thread || this.props.showThreadInfo === false || thread.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -605,7 +606,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
if (encryptionInfo.mismatchedSender) {
|
||||
// something definitely wrong is going on here
|
||||
this.setState({
|
||||
verified: E2E_STATE.WARNING,
|
||||
verified: E2EState.Warning,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
@ -613,7 +614,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
if (!userTrust.isCrossSigningVerified()) {
|
||||
// user is not verified, so default to everything is normal
|
||||
this.setState({
|
||||
verified: E2E_STATE.NORMAL,
|
||||
verified: E2EState.Normal,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
@ -623,27 +624,27 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
);
|
||||
if (!eventSenderTrust) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNKNOWN,
|
||||
verified: E2EState.Unknown,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventSenderTrust.isVerified()) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.WARNING,
|
||||
verified: E2EState.Warning,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!encryptionInfo.authenticated) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNAUTHENTICATED,
|
||||
verified: E2EState.Unauthenticated,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
verified: E2EState.Verified,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
}
|
||||
|
||||
|
@ -850,13 +851,13 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted()) {
|
||||
if (this.state.verified === E2E_STATE.NORMAL) {
|
||||
if (this.state.verified === E2EState.Normal) {
|
||||
return; // no icon if we've not even cross-signed the user
|
||||
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
||||
} else if (this.state.verified === E2EState.Verified) {
|
||||
return; // no icon for verified
|
||||
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
|
||||
} else if (this.state.verified === E2EState.Unauthenticated) {
|
||||
return (<E2ePadlockUnauthenticated />);
|
||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
||||
} else if (this.state.verified === E2EState.Unknown) {
|
||||
return (<E2ePadlockUnknown />);
|
||||
} else {
|
||||
return (<E2ePadlockUnverified />);
|
||||
|
@ -888,8 +889,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
actionBarFocused: focused,
|
||||
});
|
||||
};
|
||||
|
||||
getTile = () => this.tile.current;
|
||||
// TODO: Types
|
||||
getTile: () => any | null = () => this.tile.current;
|
||||
|
||||
getReplyThread = () => this.replyThread.current;
|
||||
|
||||
|
@ -914,6 +915,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private setQuoteExpanded = (expanded: boolean) => {
|
||||
this.setState({
|
||||
isQuoteExpanded: expanded,
|
||||
});
|
||||
};
|
||||
render() {
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
const eventType = this.props.mxEvent.getType() as EventType;
|
||||
|
@ -923,6 +929,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
isInfoMessage,
|
||||
isLeftAlignedBubbleMessage,
|
||||
} = getEventDisplayInfo(this.props.mxEvent);
|
||||
const { isQuoteExpanded } = this.state;
|
||||
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
|
@ -935,6 +942,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const EventTileType = sdk.getComponent(tileHandler);
|
||||
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
|
@ -961,9 +969,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
|
||||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2EState.Verified,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2EState.Warning,
|
||||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2EState.Unknown,
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
mx_EventTile_noSender: this.props.hideSender,
|
||||
|
@ -1054,6 +1062,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
getTile={this.getTile}
|
||||
getReplyThread={this.getReplyThread}
|
||||
onFocusChange={this.onActionBarFocusChange}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
||||
/> : undefined;
|
||||
|
||||
const showTimestamp = this.props.mxEvent.getTs()
|
||||
|
@ -1192,20 +1202,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
default: {
|
||||
let thread;
|
||||
// When the "showHiddenEventsInTimeline" lab is enabled,
|
||||
// avoid showing replies for hidden events (events without tiles)
|
||||
if (haveTileForEvent(this.props.mxEvent)) {
|
||||
thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
this.props.layout,
|
||||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
}
|
||||
|
||||
const thread = haveTileForEvent(this.props.mxEvent) &&
|
||||
ReplyThread.hasThreadReply(this.props.mxEvent) ? (
|
||||
<ReplyThread
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyThread}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
/>) : null;
|
||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React, { ComponentProps, createRef } from 'react';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
||||
|
@ -36,6 +36,7 @@ interface IProps {
|
|||
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
||||
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||
private readonly description = createRef<HTMLDivElement>();
|
||||
private image = createRef<HTMLImageElement>();
|
||||
|
||||
componentDidMount() {
|
||||
if (this.description.current) {
|
||||
|
@ -59,7 +60,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
src = mediaFromMxc(src).srcHttp;
|
||||
}
|
||||
|
||||
const params = {
|
||||
const params: Omit<ComponentProps<typeof ImageView>, "onFinished"> = {
|
||||
src: src,
|
||||
width: p["og:image:width"],
|
||||
height: p["og:image:height"],
|
||||
|
@ -68,6 +69,17 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
link: this.props.link,
|
||||
};
|
||||
|
||||
if (this.image.current) {
|
||||
const clientRect = this.image.current.getBoundingClientRect();
|
||||
|
||||
params.thumbnailInfo = {
|
||||
width: clientRect.width,
|
||||
height: clientRect.height,
|
||||
positionX: clientRect.x,
|
||||
positionY: clientRect.y,
|
||||
};
|
||||
}
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
};
|
||||
|
||||
|
@ -100,7 +112,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
let img;
|
||||
if (image) {
|
||||
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
|
||||
<img ref={this.image} style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ interface IProps {
|
|||
highlights?: string[];
|
||||
highlightLink?: string;
|
||||
onHeightChanged?(): void;
|
||||
toggleExpandedQuote?: () => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.ReplyTile")
|
||||
|
@ -82,12 +83,17 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
// This allows the permalink to be opened in a new tab/window or copied as
|
||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
// Expand thread on shift key
|
||||
if (this.props.toggleExpandedQuote && e.shiftKey) {
|
||||
this.props.toggleExpandedQuote();
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -14,8 +14,13 @@ 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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -27,91 +32,102 @@ import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore
|
|||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import InviteReason from "../elements/InviteReason";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||
|
||||
const MessageCase = Object.freeze({
|
||||
NotLoggedIn: "NotLoggedIn",
|
||||
Joining: "Joining",
|
||||
Loading: "Loading",
|
||||
Rejecting: "Rejecting",
|
||||
Kicked: "Kicked",
|
||||
Banned: "Banned",
|
||||
OtherThreePIDError: "OtherThreePIDError",
|
||||
InvitedEmailNotFoundInAccount: "InvitedEmailNotFoundInAccount",
|
||||
InvitedEmailNoIdentityServer: "InvitedEmailNoIdentityServer",
|
||||
InvitedEmailMismatch: "InvitedEmailMismatch",
|
||||
Invite: "Invite",
|
||||
ViewingRoom: "ViewingRoom",
|
||||
RoomNotFound: "RoomNotFound",
|
||||
OtherError: "OtherError",
|
||||
});
|
||||
enum MessageCase {
|
||||
NotLoggedIn = "NotLoggedIn",
|
||||
Joining = "Joining",
|
||||
Loading = "Loading",
|
||||
Rejecting = "Rejecting",
|
||||
Kicked = "Kicked",
|
||||
Banned = "Banned",
|
||||
OtherThreePIDError = "OtherThreePIDError",
|
||||
InvitedEmailNotFoundInAccount = "InvitedEmailNotFoundInAccount",
|
||||
InvitedEmailNoIdentityServer = "InvitedEmailNoIdentityServer",
|
||||
InvitedEmailMismatch = "InvitedEmailMismatch",
|
||||
Invite = "Invite",
|
||||
ViewingRoom = "ViewingRoom",
|
||||
RoomNotFound = "RoomNotFound",
|
||||
OtherError = "OtherError",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifying inviterName
|
||||
inviterName?: string;
|
||||
|
||||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail?: string;
|
||||
|
||||
// For third party invites, information passed about the room out-of-band
|
||||
oobData?: IOOBData;
|
||||
|
||||
// For third party invites, a URL for a 3pid invite signing service
|
||||
signUrl?: string;
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error?: MatrixError;
|
||||
|
||||
canPreview?: boolean;
|
||||
previewLoading?: boolean;
|
||||
room?: Room;
|
||||
|
||||
loading?: boolean;
|
||||
joining?: boolean;
|
||||
rejecting?: boolean;
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias?: string;
|
||||
|
||||
onJoinClick?(): void;
|
||||
onRejectClick?(): void;
|
||||
onRejectAndIgnoreClick?(): void;
|
||||
onForgetClick?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
accountEmails?: string[];
|
||||
invitedEmailMxid?: string;
|
||||
threePidFetchError?: MatrixError;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.RoomPreviewBar")
|
||||
export default class RoomPreviewBar extends React.Component {
|
||||
static propTypes = {
|
||||
onJoinClick: PropTypes.func,
|
||||
onRejectClick: PropTypes.func,
|
||||
onRejectAndIgnoreClick: PropTypes.func,
|
||||
onForgetClick: PropTypes.func,
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifiying inviterName
|
||||
inviterName: PropTypes.string,
|
||||
|
||||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail: PropTypes.string,
|
||||
|
||||
// For third party invites, information passed about the room out-of-band
|
||||
oobData: PropTypes.object,
|
||||
|
||||
// For third party invites, a URL for a 3pid invite signing service
|
||||
signUrl: PropTypes.string,
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error: PropTypes.object,
|
||||
|
||||
canPreview: PropTypes.bool,
|
||||
previewLoading: PropTypes.bool,
|
||||
room: PropTypes.object,
|
||||
|
||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
||||
// purpose of the spinner.
|
||||
spinner: PropTypes.bool,
|
||||
spinnerState: PropTypes.oneOf(["joining"]),
|
||||
loading: PropTypes.bool,
|
||||
joining: PropTypes.bool,
|
||||
rejecting: PropTypes.bool,
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias: PropTypes.string,
|
||||
};
|
||||
|
||||
export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onJoinClick() {},
|
||||
};
|
||||
|
||||
state = {
|
||||
busy: false,
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._checkInvitedEmail();
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
|
||||
this.checkInvitedEmail();
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
||||
this._checkInvitedEmail();
|
||||
this.checkInvitedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
async _checkInvitedEmail() {
|
||||
private async checkInvitedEmail() {
|
||||
// If this is an invite and we've been told what email address was
|
||||
// invited, fetch the user's account emails and discovery bindings so we
|
||||
// can check them against the email that was invited.
|
||||
|
@ -121,8 +137,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
// Gather the account 3PIDs
|
||||
const account3pids = await MatrixClientPeg.get().getThreePids();
|
||||
this.setState({
|
||||
accountEmails: account3pids.threepids
|
||||
.filter(b => b.medium === 'email').map(b => b.address),
|
||||
accountEmails: account3pids.threepids.filter(b => b.medium === 'email').map(b => b.address),
|
||||
});
|
||||
// If we have an IS connected, use that to lookup the email and
|
||||
// check the bound MXID.
|
||||
|
@ -146,21 +161,21 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onCommunityUpdate = (roomId) => {
|
||||
private onCommunityUpdate = (roomId: string): void => {
|
||||
if (this.props.room && this.props.room.roomId !== roomId) {
|
||||
return;
|
||||
}
|
||||
this.forceUpdate(); // we have nothing to update
|
||||
};
|
||||
|
||||
_getMessageCase() {
|
||||
private getMessageCase(): MessageCase {
|
||||
const isGuest = MatrixClientPeg.get().isGuest();
|
||||
|
||||
if (isGuest) {
|
||||
return MessageCase.NotLoggedIn;
|
||||
}
|
||||
|
||||
const myMember = this._getMyMember();
|
||||
const myMember = this.getMyMember();
|
||||
|
||||
if (myMember) {
|
||||
if (myMember.isKicked()) {
|
||||
|
@ -195,7 +210,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
return MessageCase.Invite;
|
||||
} else if (this.props.error) {
|
||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
||||
if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
|
||||
return MessageCase.RoomNotFound;
|
||||
} else {
|
||||
return MessageCase.OtherError;
|
||||
|
@ -205,8 +220,8 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getKickOrBanInfo() {
|
||||
const myMember = this._getMyMember();
|
||||
private getKickOrBanInfo(): { memberName?: string, reason?: string } {
|
||||
const myMember = this.getMyMember();
|
||||
if (!myMember) {
|
||||
return {};
|
||||
}
|
||||
|
@ -219,24 +234,19 @@ export default class RoomPreviewBar extends React.Component {
|
|||
return { memberName, reason };
|
||||
}
|
||||
|
||||
_joinRule() {
|
||||
const room = this.props.room;
|
||||
if (room) {
|
||||
const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
|
||||
if (joinRules) {
|
||||
return joinRules.getContent().join_rule;
|
||||
}
|
||||
}
|
||||
private joinRule(): JoinRule {
|
||||
return this.props.room?.currentState
|
||||
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||
}
|
||||
|
||||
_communityProfile() {
|
||||
private communityProfile(): { displayName?: string, avatarMxc?: string } {
|
||||
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
||||
return { displayName: null, avatarMxc: null };
|
||||
}
|
||||
|
||||
_roomName(atStart = false) {
|
||||
private roomName(atStart = false): string {
|
||||
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||
const profile = this._communityProfile();
|
||||
const profile = this.communityProfile();
|
||||
if (profile.displayName) name = profile.displayName;
|
||||
if (name) {
|
||||
return name;
|
||||
|
@ -247,14 +257,11 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getMyMember() {
|
||||
return (
|
||||
this.props.room &&
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId())
|
||||
);
|
||||
private getMyMember(): RoomMember {
|
||||
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
||||
}
|
||||
|
||||
_getInviteMember() {
|
||||
private getInviteMember(): RoomMember {
|
||||
const { room } = this.props;
|
||||
if (!room) {
|
||||
return;
|
||||
|
@ -268,8 +275,8 @@ export default class RoomPreviewBar extends React.Component {
|
|||
return room.currentState.getMember(inviterUserId);
|
||||
}
|
||||
|
||||
_isDMInvite() {
|
||||
const myMember = this._getMyMember();
|
||||
private isDMInvite(): boolean {
|
||||
const myMember = this.getMyMember();
|
||||
if (!myMember) {
|
||||
return false;
|
||||
}
|
||||
|
@ -278,7 +285,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||
}
|
||||
|
||||
_makeScreenAfterLogin() {
|
||||
private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
|
||||
return {
|
||||
screen: 'room',
|
||||
params: {
|
||||
|
@ -291,18 +298,16 @@ export default class RoomPreviewBar extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onLoginClick = () => {
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
|
||||
private onLoginClick = () => {
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
onRegisterClick = () => {
|
||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
|
||||
private onRegisterClick = () => {
|
||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let showSpinner = false;
|
||||
let title;
|
||||
|
@ -315,10 +320,10 @@ export default class RoomPreviewBar extends React.Component {
|
|||
let footer;
|
||||
const extraComponents = [];
|
||||
|
||||
const messageCase = this._getMessageCase();
|
||||
const messageCase = this.getMessageCase();
|
||||
switch (messageCase) {
|
||||
case MessageCase.Joining: {
|
||||
title = _t("Joining room …");
|
||||
title = this.props.oobData?.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
|
@ -349,12 +354,12 @@ export default class RoomPreviewBar extends React.Component {
|
|||
break;
|
||||
}
|
||||
case MessageCase.Kicked: {
|
||||
const { memberName, reason } = this._getKickOrBanInfo();
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
title = _t("You were kicked from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName: this._roomName() });
|
||||
{ memberName, roomName: this.roomName() });
|
||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||
|
||||
if (this._joinRule() === "invite") {
|
||||
if (this.joinRule() === "invite") {
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
} else {
|
||||
|
@ -366,9 +371,9 @@ export default class RoomPreviewBar extends React.Component {
|
|||
break;
|
||||
}
|
||||
case MessageCase.Banned: {
|
||||
const { memberName, reason } = this._getKickOrBanInfo();
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName: this._roomName() });
|
||||
{ memberName, roomName: this.roomName() });
|
||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
|
@ -376,8 +381,8 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
case MessageCase.OtherThreePIDError: {
|
||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
||||
{ roomName: this._roomName() });
|
||||
const joinRule = this._joinRule();
|
||||
{ roomName: this.roomName() });
|
||||
const joinRule = this.joinRule();
|
||||
const errCodeMessage = _t(
|
||||
"An error (%(errcode)s) was returned while trying to validate your " +
|
||||
"invite. You could try to pass this information on to a room admin.",
|
||||
|
@ -410,7 +415,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||
"associated with your account",
|
||||
{
|
||||
roomName: this._roomName(),
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
|
@ -427,7 +432,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName: this._roomName(),
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
|
@ -443,7 +448,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName: this._roomName(),
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
|
@ -458,11 +463,11 @@ export default class RoomPreviewBar extends React.Component {
|
|||
case MessageCase.Invite: {
|
||||
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
||||
const oobData = Object.assign({}, this.props.oobData, {
|
||||
avatarUrl: this._communityProfile().avatarMxc,
|
||||
avatarUrl: this.communityProfile().avatarMxc,
|
||||
});
|
||||
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
|
||||
|
||||
const inviteMember = this._getInviteMember();
|
||||
const inviteMember = this.getInviteMember();
|
||||
let inviterElement;
|
||||
if (inviteMember) {
|
||||
inviterElement = <span>
|
||||
|
@ -474,7 +479,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
|
||||
}
|
||||
|
||||
const isDM = this._isDMInvite();
|
||||
const isDM = this.isDMInvite();
|
||||
if (isDM) {
|
||||
title = _t("Do you want to chat with %(user)s?",
|
||||
{ user: inviteMember.name });
|
||||
|
@ -485,7 +490,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
primaryActionLabel = _t("Start chatting");
|
||||
} else {
|
||||
title = _t("Do you want to join %(roomName)s?",
|
||||
{ roomName: this._roomName() });
|
||||
{ roomName: this.roomName() });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
||||
|
@ -519,22 +524,22 @@ export default class RoomPreviewBar extends React.Component {
|
|||
case MessageCase.ViewingRoom: {
|
||||
if (this.props.canPreview) {
|
||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
||||
{ roomName: this._roomName() });
|
||||
{ roomName: this.roomName() });
|
||||
} else {
|
||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
||||
{ roomName: this._roomName(true) });
|
||||
{ roomName: this.roomName(true) });
|
||||
}
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.RoomNotFound: {
|
||||
title = _t("%(roomName)s does not exist.", { roomName: this._roomName(true) });
|
||||
title = _t("%(roomName)s does not exist.", { roomName: this.roomName(true) });
|
||||
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
||||
break;
|
||||
}
|
||||
case MessageCase.OtherError: {
|
||||
title = _t("%(roomName)s is not accessible at this time.", { roomName: this._roomName(true) });
|
||||
title = _t("%(roomName)s is not accessible at this time.", { roomName: this.roomName(true) });
|
||||
subTitle = [
|
||||
_t("Try again later, or ask a room admin to check if you have access."),
|
||||
_t(
|
|
@ -56,6 +56,8 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
|||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
replyToEvent: MatrixEvent,
|
||||
|
@ -162,6 +164,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps): void {
|
||||
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
|
||||
if (replyToEventChanged) {
|
||||
this.model.reset([]);
|
||||
}
|
||||
|
||||
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model.reset(parts);
|
||||
this.editorRef.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent): void => {
|
||||
// ignore any keypress while doing IME compositions
|
||||
if (this.editorRef.current?.isComposing(event)) {
|
||||
|
@ -341,7 +357,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
description: errText,
|
||||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
logger.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
@ -482,7 +498,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
private get editorStateKey() {
|
||||
return `mx_cider_state_${this.props.room.roomId}`;
|
||||
let key = `mx_cider_state_${this.props.room.roomId}`;
|
||||
const thread = this.props.replyToEvent?.getThread();
|
||||
if (thread) {
|
||||
key += `_${thread.id}`;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private clearStoredEditorState(): void {
|
||||
|
@ -490,6 +511,10 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
private restoreStoredEditorState(partCreator: PartCreator): Part[] {
|
||||
if (this.props.replyInThread && !this.props.replyToEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const json = localStorage.getItem(this.editorStateKey);
|
||||
if (json) {
|
||||
try {
|
||||
|
|
|
@ -32,6 +32,9 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||
// We sit in a context menu, so this should be given to the context menu.
|
||||
|
@ -98,11 +101,11 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private removeStickerpickerWidgets = async (): Promise<void> => {
|
||||
const scalarClient = await this.acquireScalarClient();
|
||||
console.log('Removing Stickerpicker widgets');
|
||||
logger.log('Removing Stickerpicker widgets');
|
||||
if (this.state.widgetId) {
|
||||
if (scalarClient) {
|
||||
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
|
||||
console.log('Assets disabled');
|
||||
logger.log('Assets disabled');
|
||||
}).catch((err) => {
|
||||
console.error('Failed to disable assets');
|
||||
});
|
||||
|
@ -256,12 +259,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
||||
|
||||
// FIXME: could this use the same code as other apps?
|
||||
const stickerApp = {
|
||||
const stickerApp: IApp = {
|
||||
id: stickerpickerWidget.id,
|
||||
url: stickerpickerWidget.content.url,
|
||||
name: stickerpickerWidget.content.name,
|
||||
type: stickerpickerWidget.content.type,
|
||||
data: stickerpickerWidget.content.data,
|
||||
roomId: stickerpickerWidget.content.roomId,
|
||||
eventId: stickerpickerWidget.content.eventId,
|
||||
avatar_url: stickerpickerWidget.content.avatar_url,
|
||||
creatorUserId: stickerpickerWidget.content.creatorUserId,
|
||||
};
|
||||
|
||||
stickersContent = (
|
||||
|
@ -287,9 +294,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
onEditClick={this.launchManageIntegrations}
|
||||
onDeleteClick={this.removeStickerpickerWidgets}
|
||||
showTitle={false}
|
||||
showCancel={false}
|
||||
showPopout={false}
|
||||
onMinimiseClick={this.onHideStickersClick}
|
||||
handleMinimisePointerEvents={true}
|
||||
userWidget={true}
|
||||
/>
|
||||
|
@ -345,16 +350,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger hiding of the sticker picker overlay
|
||||
* @param {Event} ev Event that triggered the function call
|
||||
*/
|
||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the window is resized
|
||||
*/
|
||||
|
|
|
@ -17,78 +17,81 @@ limitations under the License.
|
|||
|
||||
import Field from "../elements/Field";
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import withValidation from '../elements/Validation';
|
||||
import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import PassphraseField from "../auth/PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import SetEmailDialog from "../dialogs/SetEmailDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
|
||||
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||
|
||||
enum Phase {
|
||||
Edit = "edit",
|
||||
Uploading = "uploading",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished?: ({ didSetEmail: boolean }?) => void;
|
||||
onError?: (error: {error: string}) => void;
|
||||
rowClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonKind?: string;
|
||||
buttonLabel?: string;
|
||||
confirm?: boolean;
|
||||
// Whether to autoFocus the new password input
|
||||
autoFocusNewPasswordInput?: boolean;
|
||||
className?: string;
|
||||
shouldAskForEmail?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
fieldValid: {};
|
||||
phase: Phase;
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
newPasswordConfirm: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.ChangePassword")
|
||||
export default class ChangePassword extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func,
|
||||
onError: PropTypes.func,
|
||||
onCheckPassword: PropTypes.func,
|
||||
rowClassName: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
buttonKind: PropTypes.string,
|
||||
buttonLabel: PropTypes.string,
|
||||
confirm: PropTypes.bool,
|
||||
// Whether to autoFocus the new password input
|
||||
autoFocusNewPasswordInput: PropTypes.bool,
|
||||
};
|
||||
|
||||
static Phases = {
|
||||
Edit: "edit",
|
||||
Uploading: "uploading",
|
||||
Error: "error",
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
onFinished() {},
|
||||
onError() {},
|
||||
onCheckPassword(oldPass, newPass, confirmPass) {
|
||||
if (newPass !== confirmPass) {
|
||||
return {
|
||||
error: _t("New passwords don't match"),
|
||||
};
|
||||
} else if (!newPass || newPass.length === 0) {
|
||||
return {
|
||||
error: _t("Passwords can't be empty"),
|
||||
};
|
||||
}
|
||||
},
|
||||
confirm: true,
|
||||
}
|
||||
|
||||
state = {
|
||||
fieldValid: {},
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
confirm: true,
|
||||
};
|
||||
|
||||
changePassword(oldPassword, newPassword) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fieldValid: {},
|
||||
phase: Phase.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
};
|
||||
}
|
||||
|
||||
private onChangePassword(oldPassword: string, newPassword: string): void {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (!this.props.confirm) {
|
||||
this._changePassword(cli, oldPassword, newPassword);
|
||||
this.changePassword(cli, oldPassword, newPassword);
|
||||
return;
|
||||
}
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
|
@ -109,20 +112,20 @@ export default class ChangePassword extends React.Component {
|
|||
<button
|
||||
key="exportRoomKeys"
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}
|
||||
onClick={this.onExportE2eKeysClicked}
|
||||
>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this._changePassword(cli, oldPassword, newPassword);
|
||||
this.changePassword(cli, oldPassword, newPassword);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_changePassword(cli, oldPassword, newPassword) {
|
||||
private changePassword(cli: MatrixClient, oldPassword: string, newPassword: string): void {
|
||||
const authDict = {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
|
@ -136,12 +139,12 @@ export default class ChangePassword extends React.Component {
|
|||
};
|
||||
|
||||
this.setState({
|
||||
phase: ChangePassword.Phases.Uploading,
|
||||
phase: Phase.Uploading,
|
||||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this._optionallySetEmail().then((confirmed) => {
|
||||
return this.optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
didSetEmail: confirmed,
|
||||
});
|
||||
|
@ -153,7 +156,7 @@ export default class ChangePassword extends React.Component {
|
|||
this.props.onError(err);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
phase: Phase.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
|
@ -161,16 +164,27 @@ export default class ChangePassword extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_optionallySetEmail() {
|
||||
private checkPassword(oldPass: string, newPass: string, confirmPass: string): {error: string} {
|
||||
if (newPass !== confirmPass) {
|
||||
return {
|
||||
error: _t("New passwords don't match"),
|
||||
};
|
||||
} else if (!newPass || newPass.length === 0) {
|
||||
return {
|
||||
error: _t("Passwords can't be empty"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private optionallySetEmail(): Promise<boolean> {
|
||||
// Ask for an email otherwise the user has no way to reset their password
|
||||
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
||||
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
||||
title: _t('Do you want to set an email address?'),
|
||||
});
|
||||
return modal.finished.then(([confirmed]) => confirmed);
|
||||
}
|
||||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{
|
||||
|
@ -179,7 +193,7 @@ export default class ChangePassword extends React.Component {
|
|||
);
|
||||
};
|
||||
|
||||
markFieldValid(fieldID, valid) {
|
||||
private markFieldValid(fieldID: string, valid: boolean): void {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
|
@ -187,19 +201,19 @@ export default class ChangePassword extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onChangeOldPassword = (ev) => {
|
||||
private onChangeOldPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
oldPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onOldPasswordValidate = async fieldState => {
|
||||
private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validateOldPasswordRules(fieldState);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validateOldPasswordRules = withValidation({
|
||||
private validateOldPasswordRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -209,29 +223,29 @@ export default class ChangePassword extends React.Component {
|
|||
],
|
||||
});
|
||||
|
||||
onChangeNewPassword = (ev) => {
|
||||
private onChangeNewPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onNewPasswordValidate = result => {
|
||||
private onNewPasswordValidate = (result: IValidationResult): void => {
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||
};
|
||||
|
||||
onChangeNewPasswordConfirm = (ev) => {
|
||||
private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPasswordConfirm: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onNewPasswordConfirmValidate = async fieldState => {
|
||||
private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validatePasswordConfirmRules = withValidation({
|
||||
private validatePasswordConfirmRules = withValidation<this>({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -248,7 +262,7 @@ export default class ChangePassword extends React.Component {
|
|||
],
|
||||
});
|
||||
|
||||
onClickChange = async (ev) => {
|
||||
private onClickChange = async (ev: React.MouseEvent | React.FormEvent): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
|
@ -260,20 +274,20 @@ export default class ChangePassword extends React.Component {
|
|||
const oldPassword = this.state.oldPassword;
|
||||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
const err = this.props.onCheckPassword(
|
||||
const err = this.checkPassword(
|
||||
oldPassword, newPassword, confirmPassword,
|
||||
);
|
||||
if (err) {
|
||||
this.props.onError(err);
|
||||
} else {
|
||||
this.changePassword(oldPassword, newPassword);
|
||||
this.onChangePassword(oldPassword, newPassword);
|
||||
}
|
||||
};
|
||||
|
||||
async verifyFieldsBeforeSubmit() {
|
||||
private async verifyFieldsBeforeSubmit(): Promise<boolean> {
|
||||
// Blur the active element if any, so we first run its blur validation,
|
||||
// which is less strict than the pass we're about to do below for all fields.
|
||||
const activeElement = document.activeElement;
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
@ -300,7 +314,7 @@ export default class ChangePassword extends React.Component {
|
|||
|
||||
// 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));
|
||||
await new Promise<void>((resolve) => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
|
@ -319,7 +333,7 @@ export default class ChangePassword extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
allFieldsValid() {
|
||||
private allFieldsValid(): boolean {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (!this.state.fieldValid[keys[i]]) {
|
||||
|
@ -329,7 +343,7 @@ export default class ChangePassword extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
findFirstInvalidField(fieldIDs) {
|
||||
private findFirstInvalidField(fieldIDs: string[]): Field {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
|
@ -338,12 +352,12 @@ export default class ChangePassword extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
|
||||
switch (this.state.phase) {
|
||||
case ChangePassword.Phases.Edit:
|
||||
case Phase.Edit:
|
||||
return (
|
||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
<div className={rowClassName}>
|
||||
|
@ -385,7 +399,7 @@ export default class ChangePassword extends React.Component {
|
|||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
case ChangePassword.Phases.Uploading:
|
||||
case Phase.Uploading:
|
||||
return (
|
||||
<div className="mx_Dialog_content">
|
||||
<Spinner />
|
|
@ -97,9 +97,9 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
|
|||
const secretStorage = cli.crypto.secretStorage;
|
||||
const crossSigningPublicKeysOnDevice = Boolean(crossSigning.getId());
|
||||
const crossSigningPrivateKeysInStorage = Boolean(await crossSigning.isStoredInSecretStorage(secretStorage));
|
||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
|
||||
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
|
||||
const masterPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("master")));
|
||||
const selfSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("self_signing")));
|
||||
const userSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("user_signing")));
|
||||
const homeserverSupportsCrossSigning =
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
const crossSigningReady = await cli.isCrossSigningReady();
|
||||
|
|
|
@ -262,7 +262,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
|||
}
|
||||
|
||||
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
|
||||
if (beforeChange && !await beforeChange(joinRule)) return;
|
||||
if (beforeChange && !(await beforeChange(joinRule))) return;
|
||||
|
||||
const newContent: IJoinRuleEventContent = {
|
||||
join_rule: joinRule,
|
||||
|
|
|
@ -480,7 +480,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
|||
return masterSwitch;
|
||||
}
|
||||
|
||||
const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email)
|
||||
const emailSwitches = (this.state.threepids || []).filter(t => t.medium === ThreepidMedium.Email)
|
||||
.map(e => <LabelledToggleSwitch
|
||||
key={e.address}
|
||||
value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)}
|
||||
|
|
|
@ -27,6 +27,8 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import AvatarSetting from './AvatarSetting';
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
interface IState {
|
||||
userId?: string;
|
||||
originalDisplayName?: string;
|
||||
|
@ -104,7 +106,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
|
|||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
|
||||
` (${this.state.avatarFile.size}) bytes`);
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
|
@ -116,7 +118,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
|
|||
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Failed to save profile", err);
|
||||
logger.log("Failed to save profile", err);
|
||||
Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, {
|
||||
title: _t("Failed to save your profile"),
|
||||
description: ((err && err.message) ? err.message : _t("The operation could not be completed")),
|
||||
|
|
|
@ -27,13 +27,31 @@ import QuestionDialog from '../dialogs/QuestionDialog';
|
|||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||
import { accessSecretStorage } from '../../../SecurityManager';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
error: null;
|
||||
backupKeyStored: boolean;
|
||||
backupKeyCached: boolean;
|
||||
backupKeyWellFormed: boolean;
|
||||
secretStorageKeyInAccount: boolean;
|
||||
secretStorageReady: boolean;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
backupSigStatus: TrustInfo;
|
||||
sessionsRemaining: number;
|
||||
}
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@replaceableComponent("views.settings.SecureBackupPanel")
|
||||
export default class SecureBackupPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
|
@ -48,42 +66,42 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._checkKeyBackupStatus();
|
||||
public componentDidMount(): void {
|
||||
this.checkKeyBackupStatus();
|
||||
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on(
|
||||
'crypto.keyBackupSessionsRemaining',
|
||||
this._onKeyBackupSessionsRemaining,
|
||||
this.onKeyBackupSessionsRemaining,
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener(
|
||||
'crypto.keyBackupSessionsRemaining',
|
||||
this._onKeyBackupSessionsRemaining,
|
||||
this.onKeyBackupSessionsRemaining,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyBackupSessionsRemaining = (sessionsRemaining) => {
|
||||
private onKeyBackupSessionsRemaining = (sessionsRemaining: number): void => {
|
||||
this.setState({
|
||||
sessionsRemaining,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyBackupStatus = () => {
|
||||
private onKeyBackupStatus = (): void => {
|
||||
// This just loads the current backup status rather than forcing
|
||||
// a re-check otherwise we risk causing infinite loops
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
this.loadBackupStatus();
|
||||
};
|
||||
|
||||
async _checkKeyBackupStatus() {
|
||||
this._getUpdatedDiagnostics();
|
||||
private async checkKeyBackupStatus(): Promise<void> {
|
||||
this.getUpdatedDiagnostics();
|
||||
try {
|
||||
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
|
||||
this.setState({
|
||||
|
@ -93,8 +111,8 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
backupSigStatus: trustInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch check backup status", e);
|
||||
if (this._unmounted) return;
|
||||
logger.log("Unable to fetch check backup status", e);
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
|
@ -104,13 +122,13 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
private async loadBackupStatus(): Promise<void> {
|
||||
this.setState({ loading: true });
|
||||
this._getUpdatedDiagnostics();
|
||||
this.getUpdatedDiagnostics();
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: null,
|
||||
|
@ -118,8 +136,8 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
backupSigStatus,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
if (this._unmounted) return;
|
||||
logger.log("Unable to fetch key backup status", e);
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
|
@ -129,7 +147,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
async _getUpdatedDiagnostics() {
|
||||
private async getUpdatedDiagnostics(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secretStorage = cli.crypto.secretStorage;
|
||||
|
||||
|
@ -140,7 +158,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const secretStorageReady = await cli.isSecretStorageReady();
|
||||
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
|
@ -150,18 +168,18 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
_startNewBackup = () => {
|
||||
private startNewBackup = (): void => {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_deleteBackup = () => {
|
||||
private deleteBackup = (): void => {
|
||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||
title: _t('Delete Backup'),
|
||||
description: _t(
|
||||
|
@ -174,33 +192,33 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
if (!proceed) return;
|
||||
this.setState({ loading: true });
|
||||
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
|
||||
this._loadBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_restoreBackup = async () => {
|
||||
private restoreBackup = async (): Promise<void> => {
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_resetSecretStorage = async () => {
|
||||
private resetSecretStorage = async (): Promise<void> => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage(() => { }, /* forceReset = */ true);
|
||||
await accessSecretStorage(async () => { }, /* forceReset = */ true);
|
||||
} catch (e) {
|
||||
console.error("Error resetting secret storage", e);
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
this.setState({ error: e });
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
if (this.unmounted) return;
|
||||
this.loadBackupStatus();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
|
@ -261,7 +279,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
</div>;
|
||||
}
|
||||
|
||||
let backupSigStatuses = backupSigStatus.sigs.map((sig, i) => {
|
||||
let backupSigStatuses: React.ReactNode = backupSigStatus.sigs.map((sig, i) => {
|
||||
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
||||
const validity = sub =>
|
||||
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
||||
|
@ -369,14 +387,14 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
</>;
|
||||
|
||||
actions.push(
|
||||
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
|
||||
<AccessibleButton key="restore" kind="primary" onClick={this.restoreBackup}>
|
||||
{ restoreButtonCaption }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
||||
if (!isSecureBackupRequired()) {
|
||||
actions.push(
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this.deleteBackup}>
|
||||
{ _t("Delete Backup") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
@ -390,7 +408,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
|
||||
</>;
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this.startNewBackup}>
|
||||
{ _t("Set up") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
@ -398,7 +416,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
|
||||
if (secretStorageKeyInAccount) {
|
||||
actions.push(
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this.resetSecretStorage}>
|
||||
{ _t("Reset") }
|
||||
</AccessibleButton>,
|
||||
);
|
|
@ -16,16 +16,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import Field from "../../elements/Field";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import * as Email from "../../../../email";
|
||||
import AddThreepid from "../../../../AddThreepid";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
|
||||
/*
|
||||
TODO: Improve the UX for everything in here.
|
||||
|
@ -39,42 +39,45 @@ places to communicate errors - these should be replaced with inline validation w
|
|||
that is available.
|
||||
*/
|
||||
|
||||
export class ExistingEmailAddress extends React.Component {
|
||||
static propTypes = {
|
||||
email: PropTypes.object.isRequired,
|
||||
onRemoved: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IExistingEmailAddressProps {
|
||||
email: IThreepid;
|
||||
onRemoved: (emails: IThreepid) => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IExistingEmailAddressState {
|
||||
verifyRemove: boolean;
|
||||
}
|
||||
|
||||
export class ExistingEmailAddress extends React.Component<IExistingEmailAddressProps, IExistingEmailAddressState> {
|
||||
constructor(props: IExistingEmailAddressProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: true });
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
private onDontRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
|
||||
return this.props.onRemoved(this.props.email);
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to remove contact information: " + err);
|
||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||
title: _t("Unable to remove contact information"),
|
||||
|
@ -83,7 +86,7 @@ export class ExistingEmailAddress extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.state.verifyRemove) {
|
||||
return (
|
||||
<div className="mx_ExistingEmailAddress">
|
||||
|
@ -91,14 +94,14 @@ export class ExistingEmailAddress extends React.Component {
|
|||
{ _t("Remove %(email)s?", { email: this.props.email.address } ) }
|
||||
</span>
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
onClick={this.onActuallyRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
>
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
onClick={this.onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
>
|
||||
|
@ -111,7 +114,7 @@ export class ExistingEmailAddress extends React.Component {
|
|||
return (
|
||||
<div className="mx_ExistingEmailAddress">
|
||||
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -119,14 +122,21 @@ export class ExistingEmailAddress extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.account.EmailAddresses")
|
||||
export default class EmailAddresses extends React.Component {
|
||||
static propTypes = {
|
||||
emails: PropTypes.array.isRequired,
|
||||
onEmailsChange: PropTypes.func.isRequired,
|
||||
}
|
||||
interface IProps {
|
||||
emails: IThreepid[];
|
||||
onEmailsChange: (emails: Partial<IThreepid>[]) => void;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IState {
|
||||
verifying: boolean;
|
||||
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||
continueDisabled: boolean;
|
||||
newEmailAddress: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.account.EmailAddresses")
|
||||
export default class EmailAddresses extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -137,24 +147,23 @@ export default class EmailAddresses extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
_onRemoved = (address) => {
|
||||
private onRemoved = (address): void => {
|
||||
const emails = this.props.emails.filter((e) => e !== address);
|
||||
this.props.onEmailsChange(emails);
|
||||
};
|
||||
|
||||
_onChangeNewEmailAddress = (e) => {
|
||||
private onChangeNewEmailAddress = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newEmailAddress: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onAddClick = (e) => {
|
||||
private onAddClick = (e: React.FormEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.newEmailAddress) return;
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const email = this.state.newEmailAddress;
|
||||
|
||||
// TODO: Inline field validation
|
||||
|
@ -181,7 +190,7 @@ export default class EmailAddresses extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onContinueClick = (e) => {
|
||||
private onContinueClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -192,7 +201,7 @@ export default class EmailAddresses extends React.Component {
|
|||
const email = this.state.newEmailAddress;
|
||||
const emails = [
|
||||
...this.props.emails,
|
||||
{ address: email, medium: "email" },
|
||||
{ address: email, medium: ThreepidMedium.Email },
|
||||
];
|
||||
this.props.onEmailsChange(emails);
|
||||
newEmailAddress = "";
|
||||
|
@ -205,7 +214,6 @@ export default class EmailAddresses extends React.Component {
|
|||
});
|
||||
}).catch((err) => {
|
||||
this.setState({ continueDisabled: false });
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
|
||||
title: _t("Your email address hasn't been verified yet"),
|
||||
|
@ -222,13 +230,13 @@ export default class EmailAddresses extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const existingEmailElements = this.props.emails.map((e) => {
|
||||
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
|
||||
return <ExistingEmailAddress email={e} onRemoved={this.onRemoved} key={e.address} />;
|
||||
});
|
||||
|
||||
let addButton = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -237,7 +245,7 @@ export default class EmailAddresses extends React.Component {
|
|||
<div>
|
||||
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
|
||||
<AccessibleButton
|
||||
onClick={this._onContinueClick}
|
||||
onClick={this.onContinueClick}
|
||||
kind="primary"
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
|
@ -251,7 +259,7 @@ export default class EmailAddresses extends React.Component {
|
|||
<div className="mx_EmailAddresses">
|
||||
{ existingEmailElements }
|
||||
<form
|
||||
onSubmit={this._onAddClick}
|
||||
onSubmit={this.onAddClick}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EmailAddresses_new"
|
||||
|
@ -262,7 +270,7 @@ export default class EmailAddresses extends React.Component {
|
|||
autoComplete="off"
|
||||
disabled={this.state.verifying}
|
||||
value={this.state.newEmailAddress}
|
||||
onChange={this._onChangeNewEmailAddress}
|
||||
onChange={this.onChangeNewEmailAddress}
|
||||
/>
|
||||
{ addButton }
|
||||
</form>
|
|
@ -16,16 +16,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import Field from "../../elements/Field";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import AddThreepid from "../../../../AddThreepid";
|
||||
import CountryDropdown from "../../auth/CountryDropdown";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||
import { PhoneNumberCountryDefinition } from "../../../../phonenumber";
|
||||
|
||||
/*
|
||||
TODO: Improve the UX for everything in here.
|
||||
|
@ -34,42 +35,45 @@ This is a copy/paste of EmailAddresses, mostly.
|
|||
|
||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||
|
||||
export class ExistingPhoneNumber extends React.Component {
|
||||
static propTypes = {
|
||||
msisdn: PropTypes.object.isRequired,
|
||||
onRemoved: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IExistingPhoneNumberProps {
|
||||
msisdn: IThreepid;
|
||||
onRemoved: (phoneNumber: IThreepid) => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IExistingPhoneNumberState {
|
||||
verifyRemove: boolean;
|
||||
}
|
||||
|
||||
export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberProps, IExistingPhoneNumberState> {
|
||||
constructor(props: IExistingPhoneNumberProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: true });
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
private onDontRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ verifyRemove: false });
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
|
||||
return this.props.onRemoved(this.props.msisdn);
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to remove contact information: " + err);
|
||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||
title: _t("Unable to remove contact information"),
|
||||
|
@ -78,7 +82,7 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.state.verifyRemove) {
|
||||
return (
|
||||
<div className="mx_ExistingPhoneNumber">
|
||||
|
@ -86,14 +90,14 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
|
||||
</span>
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
onClick={this.onActuallyRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||
>
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
onClick={this.onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||
>
|
||||
|
@ -106,7 +110,7 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
return (
|
||||
<div className="mx_ExistingPhoneNumber">
|
||||
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -114,19 +118,30 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.account.PhoneNumbers")
|
||||
export default class PhoneNumbers extends React.Component {
|
||||
static propTypes = {
|
||||
msisdns: PropTypes.array.isRequired,
|
||||
onMsisdnsChange: PropTypes.func.isRequired,
|
||||
}
|
||||
interface IProps {
|
||||
msisdns: IThreepid[];
|
||||
onMsisdnsChange: (phoneNumbers: Partial<IThreepid>[]) => void;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IState {
|
||||
verifying: boolean;
|
||||
verifyError: string;
|
||||
verifyMsisdn: string;
|
||||
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||
continueDisabled: boolean;
|
||||
phoneCountry: string;
|
||||
newPhoneNumber: string;
|
||||
newPhoneNumberCode: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.account.PhoneNumbers")
|
||||
export default class PhoneNumbers extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
verifying: false,
|
||||
verifyError: false,
|
||||
verifyError: null,
|
||||
verifyMsisdn: "",
|
||||
addTask: null,
|
||||
continueDisabled: false,
|
||||
|
@ -136,30 +151,29 @@ export default class PhoneNumbers extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
_onRemoved = (address) => {
|
||||
private onRemoved = (address: IThreepid): void => {
|
||||
const msisdns = this.props.msisdns.filter((e) => e !== address);
|
||||
this.props.onMsisdnsChange(msisdns);
|
||||
};
|
||||
|
||||
_onChangeNewPhoneNumber = (e) => {
|
||||
private onChangeNewPhoneNumber = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPhoneNumber: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onChangeNewPhoneNumberCode = (e) => {
|
||||
private onChangeNewPhoneNumberCode = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPhoneNumberCode: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onAddClick = (e) => {
|
||||
private onAddClick = (e: React.MouseEvent | React.FormEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.newPhoneNumber) return;
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const phoneNumber = this.state.newPhoneNumber;
|
||||
const phoneCountry = this.state.phoneCountry;
|
||||
|
||||
|
@ -178,7 +192,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onContinueClick = (e) => {
|
||||
private onContinueClick = (e: React.MouseEvent | React.FormEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -190,7 +204,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
if (finished) {
|
||||
const msisdns = [
|
||||
...this.props.msisdns,
|
||||
{ address, medium: "msisdn" },
|
||||
{ address, medium: ThreepidMedium.Phone },
|
||||
];
|
||||
this.props.onMsisdnsChange(msisdns);
|
||||
newPhoneNumber = "";
|
||||
|
@ -207,7 +221,6 @@ export default class PhoneNumbers extends React.Component {
|
|||
}).catch((err) => {
|
||||
this.setState({ continueDisabled: false });
|
||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify phone number: " + err);
|
||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||
title: _t("Unable to verify phone number."),
|
||||
|
@ -219,17 +232,17 @@ export default class PhoneNumbers extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onCountryChanged = (e) => {
|
||||
this.setState({ phoneCountry: e.iso2 });
|
||||
private onCountryChanged = (country: PhoneNumberCountryDefinition): void => {
|
||||
this.setState({ phoneCountry: country.iso2 });
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const existingPhoneElements = this.props.msisdns.map((p) => {
|
||||
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />;
|
||||
return <ExistingPhoneNumber msisdn={p} onRemoved={this.onRemoved} key={p.address} />;
|
||||
});
|
||||
|
||||
let addVerifySection = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -243,17 +256,17 @@ export default class PhoneNumbers extends React.Component {
|
|||
<br />
|
||||
{ this.state.verifyError }
|
||||
</div>
|
||||
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
|
||||
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
|
||||
<Field
|
||||
type="text"
|
||||
label={_t("Verification code")}
|
||||
autoComplete="off"
|
||||
disabled={this.state.continueDisabled}
|
||||
value={this.state.newPhoneNumberCode}
|
||||
onChange={this._onChangeNewPhoneNumberCode}
|
||||
onChange={this.onChangeNewPhoneNumberCode}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this._onContinueClick}
|
||||
onClick={this.onContinueClick}
|
||||
kind="primary"
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
|
@ -264,7 +277,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const phoneCountry = <CountryDropdown onOptionChange={this._onCountryChanged}
|
||||
const phoneCountry = <CountryDropdown onOptionChange={this.onCountryChanged}
|
||||
className="mx_PhoneNumbers_country"
|
||||
value={this.state.phoneCountry}
|
||||
disabled={this.state.verifying}
|
||||
|
@ -275,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
return (
|
||||
<div className="mx_PhoneNumbers">
|
||||
{ existingPhoneElements }
|
||||
<form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
||||
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
||||
<div className="mx_PhoneNumbers_input">
|
||||
<Field
|
||||
type="text"
|
||||
|
@ -284,7 +297,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
disabled={this.state.verifying}
|
||||
prefixComponent={phoneCountry}
|
||||
value={this.state.newPhoneNumber}
|
||||
onChange={this._onChangeNewPhoneNumber}
|
||||
onChange={this.onChangeNewPhoneNumber}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
|
@ -16,14 +16,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
import AddThreepid from '../../../../AddThreepid';
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
|
||||
/*
|
||||
TODO: Improve the UX for everything in here.
|
||||
|
@ -41,12 +42,19 @@ that is available.
|
|||
TODO: Reduce all the copying between account vs. discovery components.
|
||||
*/
|
||||
|
||||
export class EmailAddress extends React.Component {
|
||||
static propTypes = {
|
||||
email: PropTypes.object.isRequired,
|
||||
};
|
||||
interface IEmailAddressProps {
|
||||
email: IThreepid;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IEmailAddressState {
|
||||
verifying: boolean;
|
||||
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||
continueDisabled: boolean;
|
||||
bound: boolean;
|
||||
}
|
||||
|
||||
export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddressState> {
|
||||
constructor(props: IEmailAddressProps) {
|
||||
super(props);
|
||||
|
||||
const { bound } = props.email;
|
||||
|
@ -60,17 +68,17 @@ export class EmailAddress extends React.Component {
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
public UNSAFE_componentWillReceiveProps(nextProps: IEmailAddressProps): void {
|
||||
const { bound } = nextProps.email;
|
||||
this.setState({ bound });
|
||||
}
|
||||
|
||||
async changeBinding({ bind, label, errorTitle }) {
|
||||
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const { medium, address } = this.props.email;
|
||||
|
||||
try {
|
||||
|
@ -103,8 +111,7 @@ export class EmailAddress extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||
const { medium, address } = this.props.email;
|
||||
|
||||
const task = new AddThreepid();
|
||||
|
@ -139,7 +146,7 @@ export class EmailAddress extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onRevokeClick = (e) => {
|
||||
private onRevokeClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -147,9 +154,9 @@ export class EmailAddress extends React.Component {
|
|||
label: "revoke",
|
||||
errorTitle: _t("Unable to revoke sharing for email address"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onShareClick = (e) => {
|
||||
private onShareClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -157,9 +164,9 @@ export class EmailAddress extends React.Component {
|
|||
label: "share",
|
||||
errorTitle: _t("Unable to share email address"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onContinueClick = async (e) => {
|
||||
private onContinueClick = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -173,7 +180,6 @@ export class EmailAddress extends React.Component {
|
|||
});
|
||||
} catch (err) {
|
||||
this.setState({ continueDisabled: false });
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
|
||||
title: _t("Your email address hasn't been verified yet"),
|
||||
|
@ -188,10 +194,9 @@ export class EmailAddress extends React.Component {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
public render(): JSX.Element {
|
||||
const { address } = this.props.email;
|
||||
const { verifying, bound } = this.state;
|
||||
|
||||
|
@ -234,14 +239,13 @@ export class EmailAddress extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
interface IProps {
|
||||
emails: IThreepid[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.discovery.EmailAddresses")
|
||||
export default class EmailAddresses extends React.Component {
|
||||
static propTypes = {
|
||||
emails: PropTypes.array.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
export default class EmailAddresses extends React.Component<IProps> {
|
||||
public render(): JSX.Element {
|
||||
let content;
|
||||
if (this.props.emails.length > 0) {
|
||||
content = this.props.emails.map((e) => {
|
|
@ -16,14 +16,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../../index';
|
||||
import Modal from '../../../../Modal';
|
||||
import AddThreepid from '../../../../AddThreepid';
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||
import Field from "../../elements/Field";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
|
||||
/*
|
||||
TODO: Improve the UX for everything in here.
|
||||
|
@ -32,12 +34,21 @@ This is a copy/paste of EmailAddresses, mostly.
|
|||
|
||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||
|
||||
export class PhoneNumber extends React.Component {
|
||||
static propTypes = {
|
||||
msisdn: PropTypes.object.isRequired,
|
||||
};
|
||||
interface IPhoneNumberProps {
|
||||
msisdn: IThreepid;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IPhoneNumberState {
|
||||
verifying: boolean;
|
||||
verificationCode: string;
|
||||
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||
continueDisabled: boolean;
|
||||
bound: boolean;
|
||||
verifyError: string;
|
||||
}
|
||||
|
||||
export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumberState> {
|
||||
constructor(props: IPhoneNumberProps) {
|
||||
super(props);
|
||||
|
||||
const { bound } = props.msisdn;
|
||||
|
@ -48,21 +59,22 @@ export class PhoneNumber extends React.Component {
|
|||
addTask: null,
|
||||
continueDisabled: false,
|
||||
bound,
|
||||
verifyError: null,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
public UNSAFE_componentWillReceiveProps(nextProps: IPhoneNumberProps): void {
|
||||
const { bound } = nextProps.msisdn;
|
||||
this.setState({ bound });
|
||||
}
|
||||
|
||||
async changeBinding({ bind, label, errorTitle }) {
|
||||
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const { medium, address } = this.props.msisdn;
|
||||
|
||||
try {
|
||||
|
@ -99,8 +111,7 @@ export class PhoneNumber extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||
const { medium, address } = this.props.msisdn;
|
||||
|
||||
const task = new AddThreepid();
|
||||
|
@ -139,7 +150,7 @@ export class PhoneNumber extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onRevokeClick = (e) => {
|
||||
private onRevokeClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -147,9 +158,9 @@ export class PhoneNumber extends React.Component {
|
|||
label: "revoke",
|
||||
errorTitle: _t("Unable to revoke sharing for phone number"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onShareClick = (e) => {
|
||||
private onShareClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.changeBinding({
|
||||
|
@ -157,15 +168,15 @@ export class PhoneNumber extends React.Component {
|
|||
label: "share",
|
||||
errorTitle: _t("Unable to share phone number"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onVerificationCodeChange = (e) => {
|
||||
private onVerificationCodeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
verificationCode: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onContinueClick = async (e) => {
|
||||
private onContinueClick = async (e: React.MouseEvent | React.FormEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -183,7 +194,6 @@ export class PhoneNumber extends React.Component {
|
|||
} catch (err) {
|
||||
this.setState({ continueDisabled: false });
|
||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Unable to verify phone number: " + err);
|
||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||
title: _t("Unable to verify phone number."),
|
||||
|
@ -193,11 +203,9 @@ export class PhoneNumber extends React.Component {
|
|||
this.setState({ verifyError: _t("Incorrect verification code") });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
public render(): JSX.Element {
|
||||
const { address } = this.props.msisdn;
|
||||
const { verifying, bound } = this.state;
|
||||
|
||||
|
@ -247,13 +255,13 @@ export class PhoneNumber extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
||||
export default class PhoneNumbers extends React.Component {
|
||||
static propTypes = {
|
||||
msisdns: PropTypes.array.isRequired,
|
||||
}
|
||||
interface IProps {
|
||||
msisdns: IThreepid[];
|
||||
}
|
||||
|
||||
render() {
|
||||
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
||||
export default class PhoneNumbers extends React.Component<IProps> {
|
||||
public render(): JSX.Element {
|
||||
let content;
|
||||
if (this.props.msisdns.length > 0) {
|
||||
content = this.props.msisdns.map((e) => {
|
|
@ -15,45 +15,46 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
import * as sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
|
||||
import RelatedGroupSettings from "../../../room_settings/RelatedGroupSettings";
|
||||
import AliasSettings from "../../../room_settings/AliasSettings";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isRoomPublished: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class GeneralRoomSettingsTab extends React.Component<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isRoomPublished: false, // loaded async
|
||||
};
|
||||
}
|
||||
|
||||
_onLeaveClick = () => {
|
||||
private onLeaveClick = (): void => {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
||||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
|
||||
public render(): JSX.Element {
|
||||
const client = this.context;
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
|
@ -110,7 +111,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
|
||||
<AccessibleButton kind='danger' onClick={this.onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
</AccessibleButton>
|
||||
</div>
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
@ -24,16 +23,21 @@ import SettingsStore from '../../../../../settings/SettingsStore';
|
|||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
currentSound: string;
|
||||
uploadedFile: File;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
|
||||
export default class NotificationsSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class NotificationsSettingsTab extends React.Component<IProps, IState> {
|
||||
private soundUpload = createRef<HTMLInputElement>();
|
||||
|
||||
_soundUpload = createRef();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
currentSound: "default",
|
||||
|
@ -42,7 +46,8 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
public UNSAFE_componentWillMount(): void {
|
||||
const soundData = Notifier.getSoundForRoom(this.props.roomId);
|
||||
if (!soundData) {
|
||||
return;
|
||||
|
@ -50,14 +55,14 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
this.setState({ currentSound: soundData.name || soundData.url });
|
||||
}
|
||||
|
||||
async _triggerUploader(e) {
|
||||
private triggerUploader = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this._soundUpload.current.click();
|
||||
}
|
||||
this.soundUpload.current.click();
|
||||
};
|
||||
|
||||
async _onSoundUploadChanged(e) {
|
||||
private onSoundUploadChanged = (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
if (!e.target.files || !e.target.files.length) {
|
||||
this.setState({
|
||||
uploadedFile: null,
|
||||
|
@ -69,23 +74,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
this.setState({
|
||||
uploadedFile: file,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async _onClickSaveSound(e) {
|
||||
private onClickSaveSound = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await this._saveSound();
|
||||
await this.saveSound();
|
||||
} catch (ex) {
|
||||
console.error(
|
||||
`Unable to save notification sound for ${this.props.roomId}`,
|
||||
);
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async _saveSound() {
|
||||
private async saveSound(): Promise<void> {
|
||||
if (!this.state.uploadedFile) {
|
||||
return;
|
||||
}
|
||||
|
@ -122,7 +127,7 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_clearSound(e) {
|
||||
private clearSound = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
SettingsStore.setValue(
|
||||
|
@ -135,9 +140,9 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
this.setState({
|
||||
currentSound: "default",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let currentUploadedFile = null;
|
||||
if (this.state.uploadedFile) {
|
||||
currentUploadedFile = (
|
||||
|
@ -154,23 +159,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
|
||||
<div>
|
||||
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
|
||||
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
|
||||
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this.clearSound} kind="primary">
|
||||
{ _t("Reset") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{ _t("Set a new custom sound") }</h3>
|
||||
<form autoComplete="off" noValidate={true}>
|
||||
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
||||
<input ref={this.soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this.onSoundUploadChanged} accept="audio/*" />
|
||||
</form>
|
||||
|
||||
{ currentUploadedFile }
|
||||
|
||||
<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
|
||||
<AccessibleButton className="mx_NotificationSound_browse" onClick={this.triggerUploader} kind="primary">
|
||||
{ _t("Browse") }
|
||||
</AccessibleButton>
|
||||
|
||||
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
|
||||
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this.onClickSaveSound} kind="primary">
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
<br />
|
|
@ -137,7 +137,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
}
|
||||
}
|
||||
|
||||
private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => {
|
||||
private onPowerLevelsChanged = (value: number, powerLevelKey: string) => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
||||
|
@ -148,8 +148,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
|
||||
const eventsLevelPrefix = "event_levels_";
|
||||
|
||||
const value = parseInt(inputValue);
|
||||
|
||||
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
|
||||
// deep copy "events" object, Object.assign itself won't deep copy
|
||||
plContent["events"] = Object.assign({}, plContent["events"] || {});
|
||||
|
@ -181,7 +179,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
});
|
||||
};
|
||||
|
||||
private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => {
|
||||
private onUserPowerLevelChanged = (value: number, powerLevelKey: string) => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
||||
|
|
|
@ -25,13 +25,11 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
|
|||
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||
import PropTypes from "prop-types";
|
||||
import PlatformPeg from "../../../../../PlatformPeg";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../..";
|
||||
import Modal from "../../../../../Modal";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { Service, startTermsFlow } from "../../../../../Terms";
|
||||
import { Policies, Service, startTermsFlow } from "../../../../../Terms";
|
||||
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||
import IdentityAuthClient from "../../../../../IdentityAuthClient";
|
||||
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
|
||||
|
@ -40,15 +38,50 @@ import Spinner from "../../../elements/Spinner";
|
|||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||
import ErrorDialog from "../../../dialogs/ErrorDialog";
|
||||
import AccountPhoneNumbers from "../../account/PhoneNumbers";
|
||||
import AccountEmailAddresses from "../../account/EmailAddresses";
|
||||
import DiscoveryEmailAddresses from "../../discovery/EmailAddresses";
|
||||
import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers";
|
||||
import ChangePassword from "../../ChangePassword";
|
||||
import InlineTermsAgreement from "../../../terms/InlineTermsAgreement";
|
||||
import SetIdServer from "../../SetIdServer";
|
||||
import SetIntegrationManager from "../../SetIntegrationManager";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
language: string;
|
||||
spellCheckLanguages: string[];
|
||||
haveIdServer: boolean;
|
||||
serverSupportsSeparateAddAndBind: boolean;
|
||||
idServerHasUnsignedTerms: boolean;
|
||||
requiredPolicyInfo: { // This object is passed along to a component for handling
|
||||
hasTerms: boolean;
|
||||
policiesAndServices: {
|
||||
service: Service;
|
||||
policies: Policies;
|
||||
}[]; // From the startTermsFlow callback
|
||||
agreedUrls: string[]; // From the startTermsFlow callback
|
||||
resolve: (values: string[]) => void; // Promise resolve function for startTermsFlow callback
|
||||
};
|
||||
emails: IThreepid[];
|
||||
msisdns: IThreepid[];
|
||||
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
|
||||
canChangePassword: boolean;
|
||||
idServerName: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
|
||||
export default class GeneralUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
||||
private readonly dispatcherRef: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
language: languageHandler.getCurrentLanguage(),
|
||||
|
@ -58,20 +91,23 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
idServerHasUnsignedTerms: false,
|
||||
requiredPolicyInfo: { // This object is passed along to a component for handling
|
||||
hasTerms: false,
|
||||
// policiesAndServices, // From the startTermsFlow callback
|
||||
// agreedUrls, // From the startTermsFlow callback
|
||||
// resolve, // Promise resolve function for startTermsFlow callback
|
||||
policiesAndServices: null, // From the startTermsFlow callback
|
||||
agreedUrls: null, // From the startTermsFlow callback
|
||||
resolve: null, // Promise resolve function for startTermsFlow callback
|
||||
},
|
||||
emails: [],
|
||||
msisdns: [],
|
||||
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
||||
canChangePassword: false,
|
||||
idServerName: null,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
async UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
public async UNSAFE_componentWillMount(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
|
||||
|
@ -86,10 +122,10 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
|
||||
|
||||
this._getThreepidState();
|
||||
this.getThreepidState();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
public async componentDidMount(): Promise<void> {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
this.setState({
|
||||
|
@ -98,30 +134,30 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.action === 'id_server_changed') {
|
||||
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
|
||||
this._getThreepidState();
|
||||
this.getThreepidState();
|
||||
}
|
||||
};
|
||||
|
||||
_onEmailsChange = (emails) => {
|
||||
private onEmailsChange = (emails: IThreepid[]): void => {
|
||||
this.setState({ emails });
|
||||
};
|
||||
|
||||
_onMsisdnsChange = (msisdns) => {
|
||||
private onMsisdnsChange = (msisdns: IThreepid[]): void => {
|
||||
this.setState({ msisdns });
|
||||
};
|
||||
|
||||
async _getThreepidState() {
|
||||
private async getThreepidState(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
// Check to see if terms need accepting
|
||||
this._checkTerms();
|
||||
this.checkTerms();
|
||||
|
||||
// Need to get 3PIDs generally for Account section and possibly also for
|
||||
// Discovery (assuming we have an IS and terms are agreed).
|
||||
|
@ -143,7 +179,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
async _checkTerms() {
|
||||
private async checkTerms(): Promise<void> {
|
||||
if (!this.state.haveIdServer) {
|
||||
this.setState({ idServerHasUnsignedTerms: false });
|
||||
return;
|
||||
|
@ -176,6 +212,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
this.setState({
|
||||
requiredPolicyInfo: {
|
||||
hasTerms: false,
|
||||
...this.state.requiredPolicyInfo,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -187,19 +224,19 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onLanguageChange = (newLanguage) => {
|
||||
private onLanguageChange = (newLanguage: string): void => {
|
||||
if (this.state.language === newLanguage) return;
|
||||
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
|
||||
this.setState({ language: newLanguage });
|
||||
const platform = PlatformPeg.get();
|
||||
if (platform) {
|
||||
platform.setLanguage(newLanguage);
|
||||
platform.setLanguage([newLanguage]);
|
||||
platform.reload();
|
||||
}
|
||||
};
|
||||
|
||||
_onSpellCheckLanguagesChange = (languages) => {
|
||||
private onSpellCheckLanguagesChange = (languages: string[]): void => {
|
||||
this.setState({ spellCheckLanguages: languages });
|
||||
|
||||
const plaf = PlatformPeg.get();
|
||||
|
@ -208,7 +245,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onPasswordChangeError = (err) => {
|
||||
private onPasswordChangeError = (err): void => {
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
let errMsg = err.error || err.message || "";
|
||||
if (err.httpStatus === 403) {
|
||||
|
@ -216,7 +253,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
} else if (!errMsg) {
|
||||
errMsg += ` (HTTP status ${err.httpStatus})`;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change password: " + errMsg);
|
||||
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
|
@ -224,9 +260,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onPasswordChanged = () => {
|
||||
private onPasswordChanged = (): void => {
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
|
||||
title: _t("Success"),
|
||||
description: _t(
|
||||
|
@ -236,7 +271,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onDeactivateClicked = () => {
|
||||
private onDeactivateClicked = (): void => {
|
||||
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {
|
||||
onFinished: (success) => {
|
||||
if (success) this.props.closeSettingsFn();
|
||||
|
@ -244,7 +279,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_renderProfileSection() {
|
||||
private renderProfileSection(): JSX.Element {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<ProfileSettings />
|
||||
|
@ -252,18 +287,14 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderAccountSection() {
|
||||
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||
const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses");
|
||||
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
|
||||
|
||||
private renderAccountSection(): JSX.Element {
|
||||
let passwordChangeForm = (
|
||||
<ChangePassword
|
||||
className="mx_GeneralUserSettingsTab_changePassword"
|
||||
rowClassName=""
|
||||
buttonKind="primary"
|
||||
onError={this._onPasswordChangeError}
|
||||
onFinished={this._onPasswordChanged} />
|
||||
onError={this.onPasswordChangeError}
|
||||
onFinished={this.onPasswordChanged} />
|
||||
);
|
||||
|
||||
let threepidSection = null;
|
||||
|
@ -278,15 +309,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
) {
|
||||
const emails = this.state.loading3pids
|
||||
? <Spinner />
|
||||
: <EmailAddresses
|
||||
: <AccountEmailAddresses
|
||||
emails={this.state.emails}
|
||||
onEmailsChange={this._onEmailsChange}
|
||||
onEmailsChange={this.onEmailsChange}
|
||||
/>;
|
||||
const msisdns = this.state.loading3pids
|
||||
? <Spinner />
|
||||
: <PhoneNumbers
|
||||
: <AccountPhoneNumbers
|
||||
msisdns={this.state.msisdns}
|
||||
onMsisdnsChange={this._onMsisdnsChange}
|
||||
onMsisdnsChange={this.onMsisdnsChange}
|
||||
/>;
|
||||
threepidSection = <div>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
||||
|
@ -318,37 +349,34 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderLanguageSection() {
|
||||
private renderLanguageSection(): JSX.Element {
|
||||
// TODO: Convert to new-styled Field
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
|
||||
<LanguageDropdown
|
||||
className="mx_GeneralUserSettingsTab_languageInput"
|
||||
onOptionChange={this._onLanguageChange}
|
||||
onOptionChange={this.onLanguageChange}
|
||||
value={this.state.language}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSpellCheckSection() {
|
||||
private renderSpellCheckSection(): JSX.Element {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
|
||||
<SpellCheckSettings
|
||||
languages={this.state.spellCheckLanguages}
|
||||
onLanguagesChange={this._onSpellCheckLanguagesChange}
|
||||
onLanguagesChange={this.onSpellCheckLanguagesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderDiscoverySection() {
|
||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
||||
|
||||
private renderDiscoverySection(): JSX.Element {
|
||||
if (this.state.requiredPolicyInfo.hasTerms) {
|
||||
const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
|
||||
const intro = <span className="mx_SettingsTab_subsectionText">
|
||||
{ _t(
|
||||
"Agree to the identity server (%(serverName)s) Terms of Service to " +
|
||||
|
@ -370,11 +398,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses");
|
||||
const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers");
|
||||
|
||||
const emails = this.state.loading3pids ? <Spinner /> : <EmailAddresses emails={this.state.emails} />;
|
||||
const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
|
||||
const emails = this.state.loading3pids ? <Spinner /> : <DiscoveryEmailAddresses emails={this.state.emails} />;
|
||||
const msisdns = this.state.loading3pids ? <Spinner /> : <DiscoveryPhoneNumbers msisdns={this.state.msisdns} />;
|
||||
|
||||
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
||||
|
@ -388,12 +413,12 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
<div className="mx_SettingsTab_section">
|
||||
{ threepidSection }
|
||||
{ /* has its own heading as it includes the current identity server */ }
|
||||
<SetIdServer />
|
||||
<SetIdServer missingTerms={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderManagementSection() {
|
||||
private renderManagementSection(): JSX.Element {
|
||||
// TODO: Improve warning text for account deactivation
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
|
@ -401,18 +426,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
<span className="mx_SettingsTab_subsectionText">
|
||||
{ _t("Deactivating your account is a permanent action - be careful!") }
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
|
||||
<AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
|
||||
{ _t("Deactivate Account") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderIntegrationManagerSection() {
|
||||
private renderIntegrationManagerSection(): JSX.Element {
|
||||
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
||||
|
||||
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
{ /* has its own heading as it includes the current integration manager */ }
|
||||
|
@ -421,7 +444,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const plaf = PlatformPeg.get();
|
||||
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
||||
|
||||
|
@ -439,7 +462,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
if (SettingsStore.getValue(UIFeature.Deactivate)) {
|
||||
accountManagementSection = <>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
|
||||
{ this._renderManagementSection() }
|
||||
{ this.renderManagementSection() }
|
||||
</>;
|
||||
}
|
||||
|
||||
|
@ -447,19 +470,19 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
discoverySection = <>
|
||||
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
|
||||
{ this._renderDiscoverySection() }
|
||||
{ this.renderDiscoverySection() }
|
||||
</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||
{ this._renderProfileSection() }
|
||||
{ this._renderAccountSection() }
|
||||
{ this._renderLanguageSection() }
|
||||
{ supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null }
|
||||
{ this.renderProfileSection() }
|
||||
{ this.renderAccountSection() }
|
||||
{ this.renderLanguageSection() }
|
||||
{ supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null }
|
||||
{ discoverySection }
|
||||
{ this._renderIntegrationManagerSection() /* Has its own title */ }
|
||||
{ this.renderIntegrationManagerSection() /* Has its own title */ }
|
||||
{ accountManagementSection }
|
||||
</div>
|
||||
);
|
|
@ -32,6 +32,8 @@ import { toRightOf } from "../../../../structures/ContextMenu";
|
|||
import BugReportDialog from '../../../dialogs/BugReportDialog';
|
||||
import GenericTextContextMenu from "../../../context_menus/GenericTextContextMenu";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn: () => void;
|
||||
}
|
||||
|
@ -88,7 +90,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
|
||||
// Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly
|
||||
// stopping in the middle of the logs.
|
||||
console.log("Clear cache & reload clicked");
|
||||
logger.log("Clear cache & reload clicked");
|
||||
MatrixClientPeg.get().stopClient();
|
||||
MatrixClientPeg.get().store.deleteAllData().then(() => {
|
||||
PlatformPeg.get().reload();
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import PropTypes from "prop-types";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
|
@ -26,28 +25,32 @@ import BetaCard from "../../../beta/BetaCard";
|
|||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||
|
||||
export class LabsSettingToggle extends React.Component {
|
||||
static propTypes = {
|
||||
featureId: PropTypes.string.isRequired,
|
||||
};
|
||||
interface ILabsSettingToggleProps {
|
||||
featureId: string;
|
||||
}
|
||||
|
||||
_onChange = async (checked) => {
|
||||
export class LabsSettingToggle extends React.Component<ILabsSettingToggleProps> {
|
||||
private onChange = async (checked: boolean): Promise<void> => {
|
||||
await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const label = SettingsStore.getDisplayName(this.props.featureId);
|
||||
const value = SettingsStore.getValue(this.props.featureId);
|
||||
const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
|
||||
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} disabled={!canChange} />;
|
||||
return <LabelledToggleSwitch value={value} label={label} onChange={this.onChange} disabled={!canChange} />;
|
||||
}
|
||||
}
|
||||
|
||||
interface IState {
|
||||
showHiddenReadReceipts: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.LabsUserSettingsTab")
|
||||
export default class LabsUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
|
||||
this.setState({ showHiddenReadReceipts });
|
||||
|
@ -58,7 +61,7 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const features = SettingsStore.getFeatureSettingNames();
|
||||
const [labs, betas] = features.reduce((arr, f) => {
|
||||
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
|
|
@ -233,7 +233,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||
const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar();
|
||||
let alwaysShowMenuBar = true;
|
||||
if (alwaysShowMenuBarSupported) {
|
||||
alwaysShowMenuBar = !await platform.getAutoHideMenuBarEnabled();
|
||||
alwaysShowMenuBar = !(await platform.getAutoHideMenuBarEnabled());
|
||||
}
|
||||
|
||||
const minimizeToTraySupported = await platform.supportsMinimizeToTray();
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
|
@ -26,34 +25,40 @@ import * as FormattingUtils from "../../../../../utils/FormattingUtils";
|
|||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Analytics from "../../../../../Analytics";
|
||||
import Modal from "../../../../../Modal";
|
||||
import * as sdk from "../../../../..";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { privateShouldBeEncrypted } from "../../../../../createRoom";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import SecureBackupPanel from "../../SecureBackupPanel";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||
import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||
import CountlyAnalytics from "../../../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import DevicesPanel from "../../DevicesPanel";
|
||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||
import CrossSigningPanel from "../../CrossSigningPanel";
|
||||
import EventIndexPanel from "../../EventIndexPanel";
|
||||
import InlineSpinner from "../../../elements/InlineSpinner";
|
||||
|
||||
export class IgnoredUser extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
onUnignored: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool.isRequired,
|
||||
};
|
||||
interface IIgnoredUserProps {
|
||||
userId: string;
|
||||
onUnignored: (userId: string) => void;
|
||||
inProgress: boolean;
|
||||
}
|
||||
|
||||
_onUnignoreClicked = (e) => {
|
||||
export class IgnoredUser extends React.Component<IIgnoredUserProps> {
|
||||
private onUnignoreClicked = (): void => {
|
||||
this.props.onUnignored(this.props.userId);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
|
||||
return (
|
||||
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
|
||||
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}>
|
||||
<AccessibleButton onClick={this.onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}>
|
||||
{ _t('Unignore') }
|
||||
</AccessibleButton>
|
||||
<span id={id}>{ this.props.userId }</span>
|
||||
|
@ -62,17 +67,26 @@ export class IgnoredUser extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
|
||||
export default class SecurityUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
closeSettingsFn: () => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IState {
|
||||
ignoredUserIds: string[];
|
||||
waitingUnignored: string[];
|
||||
managingInvites: boolean;
|
||||
invitedRoomAmt: number;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
|
||||
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Get number of rooms we're invited to
|
||||
const invitedRooms = this._getInvitedRooms();
|
||||
const invitedRooms = this.getInvitedRooms();
|
||||
|
||||
this.state = {
|
||||
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
|
||||
|
@ -80,59 +94,57 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
managingInvites: false,
|
||||
invitedRoomAmt: invitedRooms.length,
|
||||
};
|
||||
|
||||
this._onAction = this._onAction.bind(this);
|
||||
}
|
||||
|
||||
_onAction({ action }) {
|
||||
private onAction = ({ action }: ActionPayload)=> {
|
||||
if (action === "ignore_state_changed") {
|
||||
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
|
||||
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_updateBlacklistDevicesFlag = (checked) => {
|
||||
private updateBlacklistDevicesFlag = (checked): void => {
|
||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
||||
};
|
||||
|
||||
_updateAnalytics = (checked) => {
|
||||
private updateAnalytics = (checked: boolean): void => {
|
||||
checked ? Analytics.enable() : Analytics.disable();
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
|
||||
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
|
||||
};
|
||||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
||||
_onImportE2eKeysClicked = () => {
|
||||
private onImportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
||||
_onGoToUserProfileClick = () => {
|
||||
private onGoToUserProfileClick = (): void => {
|
||||
dis.dispatch({
|
||||
action: 'view_user_info',
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
});
|
||||
this.props.closeSettingsFn();
|
||||
}
|
||||
};
|
||||
|
||||
_onUserUnignored = async (userId) => {
|
||||
private onUserUnignored = async (userId: string): Promise<void> => {
|
||||
const { ignoredUserIds, waitingUnignored } = this.state;
|
||||
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
|
||||
|
||||
|
@ -144,24 +156,23 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_getInvitedRooms = () => {
|
||||
private getInvitedRooms = (): Room[] => {
|
||||
return MatrixClientPeg.get().getRooms().filter((r) => {
|
||||
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
|
||||
});
|
||||
};
|
||||
|
||||
_manageInvites = async (accept) => {
|
||||
private manageInvites = async (accept: boolean): Promise<void> => {
|
||||
this.setState({
|
||||
managingInvites: true,
|
||||
});
|
||||
|
||||
// Compile array of invitation room ids
|
||||
const invitedRoomIds = this._getInvitedRooms().map((room) => {
|
||||
const invitedRoomIds = this.getInvitedRooms().map((room) => {
|
||||
return room.roomId;
|
||||
});
|
||||
|
||||
// Execute all acceptances/rejections sequentially
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli);
|
||||
for (let i = 0; i < invitedRoomIds.length; i++) {
|
||||
|
@ -170,7 +181,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
// Accept/reject invite
|
||||
await action(roomId).then(() => {
|
||||
// No error, update invited rooms button
|
||||
this.setState({ invitedRoomAmt: self.state.invitedRoomAmt - 1 });
|
||||
this.setState({ invitedRoomAmt: this.state.invitedRoomAmt - 1 });
|
||||
}, async (e) => {
|
||||
// Action failure
|
||||
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||
|
@ -192,17 +203,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onAcceptAllInvitesClicked = (ev) => {
|
||||
this._manageInvites(true);
|
||||
private onAcceptAllInvitesClicked = (): void => {
|
||||
this.manageInvites(true);
|
||||
};
|
||||
|
||||
_onRejectAllInvitesClicked = (ev) => {
|
||||
this._manageInvites(false);
|
||||
private onRejectAllInvitesClicked = (): void => {
|
||||
this.manageInvites(false);
|
||||
};
|
||||
|
||||
_renderCurrentDeviceInfo() {
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
|
||||
private renderCurrentDeviceInfo(): JSX.Element {
|
||||
const client = MatrixClientPeg.get();
|
||||
const deviceId = client.deviceId;
|
||||
let identityKey = client.getDeviceEd25519Key();
|
||||
|
@ -216,10 +225,10 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
if (client.isCryptoEnabled()) {
|
||||
importExportButtons = (
|
||||
<div className='mx_SecurityUserSettingsTab_importExportButtons'>
|
||||
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
|
||||
<AccessibleButton kind='primary' onClick={this.onExportE2eKeysClicked}>
|
||||
{ _t("Export E2E room keys") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
|
||||
<AccessibleButton kind='primary' onClick={this.onImportE2eKeysClicked}>
|
||||
{ _t("Import E2E room keys") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -231,7 +240,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
noSendUnverifiedSetting = <SettingsFlag
|
||||
name='blacklistUnverifiedDevices'
|
||||
level={SettingLevel.DEVICE}
|
||||
onChange={this._updateBlacklistDevicesFlag}
|
||||
onChange={this.updateBlacklistDevicesFlag}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -254,7 +263,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderIgnoredUsers() {
|
||||
private renderIgnoredUsers(): JSX.Element {
|
||||
const { waitingUnignored, ignoredUserIds } = this.state;
|
||||
|
||||
const userIds = !ignoredUserIds?.length
|
||||
|
@ -263,7 +272,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
return (
|
||||
<IgnoredUser
|
||||
userId={u}
|
||||
onUnignored={this._onUserUnignored}
|
||||
onUnignored={this.onUserUnignored}
|
||||
key={u}
|
||||
inProgress={waitingUnignored.includes(u)}
|
||||
/>
|
||||
|
@ -280,15 +289,14 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderManageInvites() {
|
||||
private renderManageInvites(): JSX.Element {
|
||||
if (this.state.invitedRoomAmt === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const invitedRooms = this._getInvitedRooms();
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
const onClickAccept = this._onAcceptAllInvitesClicked.bind(this, invitedRooms);
|
||||
const onClickReject = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
|
||||
const invitedRooms = this.getInvitedRooms();
|
||||
const onClickAccept = this.onAcceptAllInvitesClicked.bind(this, invitedRooms);
|
||||
const onClickReject = this.onRejectAllInvitesClicked.bind(this, invitedRooms);
|
||||
return (
|
||||
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
|
||||
|
@ -303,11 +311,8 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
|
||||
|
||||
const secureBackup = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
|
@ -329,7 +334,6 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
// it's useful to have for testing the feature. If there's no interest
|
||||
// in having advanced details here once all flows are implemented, we
|
||||
// can remove this.
|
||||
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
|
||||
const crossSigning = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
|
||||
|
@ -365,16 +369,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
{ _t("Learn more about how we use analytics.") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
|
||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this.updateAnalytics} />
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
|
||||
let advancedSection;
|
||||
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
|
||||
const ignoreUsersPanel = this._renderIgnoredUsers();
|
||||
const invitesPanel = this._renderManageInvites();
|
||||
const ignoreUsersPanel = this.renderIgnoredUsers();
|
||||
const invitesPanel = this.renderManageInvites();
|
||||
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
|
||||
// only show the section if there's something to show
|
||||
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
|
||||
|
@ -399,7 +402,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
"Manage the names of and sign out of your sessions below or " +
|
||||
"<a>verify them in your User Profile</a>.", {},
|
||||
{
|
||||
a: sub => <AccessibleButton kind="link" onClick={this._onGoToUserProfileClick}>
|
||||
a: sub => <AccessibleButton kind="link" onClick={this.onGoToUserProfileClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
|
@ -415,7 +418,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
{ secureBackup }
|
||||
{ eventIndex }
|
||||
{ crossSigning }
|
||||
{ this._renderCurrentDeviceInfo() }
|
||||
{ this.renderCurrentDeviceInfo() }
|
||||
</div>
|
||||
{ privacySection }
|
||||
{ advancedSection }
|
|
@ -28,6 +28,8 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
|
|||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import ErrorDialog from '../../../dialogs/ErrorDialog';
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
|
||||
// Note we're looking for a device with deviceId 'default' but adding a device
|
||||
// with deviceId == the empty string: this is because Chrome gives us a device
|
||||
|
@ -101,7 +103,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
|
|||
}
|
||||
}
|
||||
if (error) {
|
||||
console.log("Failed to list userMedia devices", error);
|
||||
logger.log("Failed to list userMedia devices", error);
|
||||
const brand = SdkConfig.get().brand;
|
||||
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||
title: _t('No media permissions'),
|
||||
|
|
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