Merge pull request #6743 from SimonBrandner/task/dialogs-ts

This commit is contained in:
Germain 2021-09-22 10:43:19 +01:00 committed by GitHub
commit b13fdb698c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 628 additions and 542 deletions

View file

@ -30,6 +30,8 @@ const HEARTBEAT_INTERVAL = 5_000; // ms
const SESSION_UPDATE_INTERVAL = 60; // seconds const SESSION_UPDATE_INTERVAL = 60; // seconds
const MAX_PENDING_EVENTS = 1000; const MAX_PENDING_EVENTS = 1000;
export type Rating = 1 | 2 | 3 | 4 | 5;
enum Orientation { enum Orientation {
Landscape = "landscape", Landscape = "landscape",
Portrait = "portrait", Portrait = "portrait",
@ -451,7 +453,7 @@ export default class CountlyAnalytics {
window.removeEventListener("scroll", this.onUserActivity); window.removeEventListener("scroll", this.onUserActivity);
} }
public reportFeedback(rating: 1 | 2 | 3 | 4 | 5, comment: string) { public reportFeedback(rating: Rating, comment: string) {
this.track<IStarRatingEvent>("[CLY]_star_rating", { rating, comment }, null, {}, true); this.track<IStarRatingEvent>("[CLY]_star_rating", { rating, comment }, null, {}, true);
} }

View file

@ -18,15 +18,54 @@ limitations under the License.
import React from 'react'; import React from 'react';
import FocusLock from 'react-focus-lock'; import FocusLock from 'react-focus-lock';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Key } from '../../../Keyboard'; import { Key } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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. * Basic container for modal dialogs.
@ -35,54 +74,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* dialog on escape. * dialog on escape.
*/ */
@replaceableComponent("views.dialogs.BaseDialog") @replaceableComponent("views.dialogs.BaseDialog")
export default class BaseDialog extends React.Component { export default class BaseDialog extends React.Component<IProps> {
static propTypes = { private matrixClient: MatrixClient;
// 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,
// Whether the dialog should have a 'close' button that will public static defaultProps = {
// 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 = {
hasCancel: true, hasCancel: true,
fixedWidth: true, fixedWidth: true,
}; };
@ -90,10 +85,10 @@ export default class BaseDialog extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._matrixClient = MatrixClientPeg.get(); this.matrixClient = MatrixClientPeg.get();
} }
_onKeyDown = (e) => { private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
if (this.props.onKeyDown) { if (this.props.onKeyDown) {
this.props.onKeyDown(e); 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); this.props.onFinished(false);
}; };
render() { public render(): JSX.Element {
let cancelButton; let cancelButton;
if (this.props.hasCancel) { if (this.props.hasCancel) {
cancelButton = ( 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 ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this.matrixClient}>
<FocusLock <FocusLock
returnFocus={true} returnFocus={true}
lockProps={{ lockProps={{
onKeyDown: this._onKeyDown, onKeyDown: this.onKeyDown,
role: "dialog", role: "dialog",
["aria-labelledby"]: "mx_BaseDialog_title", ["aria-labelledby"]: "mx_BaseDialog_title",
// This should point to a node describing the dialog. // This should point to a node describing the dialog.

View file

@ -19,30 +19,33 @@ import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "../elements/Field"; import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics, { Rating } from "../../../CountlyAnalytics";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import BugReportDialog from "./BugReportDialog"; import BugReportDialog from "./BugReportDialog";
import InfoDialog from "./InfoDialog"; import InfoDialog from "./InfoDialog";
import StyledRadioGroup from "../elements/StyledRadioGroup"; import StyledRadioGroup from "../elements/StyledRadioGroup";
import { IDialogProps } from "./IDialogProps";
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" + const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc"; "?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose"; const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose";
export default (props) => { interface IProps extends IDialogProps {}
const [rating, setRating] = useState("");
const [comment, setComment] = useState("");
const onDebugLogsLinkClick = () => { const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
const [rating, setRating] = useState<Rating>();
const [comment, setComment] = useState<string>("");
const onDebugLogsLinkClick = (): void => {
props.onFinished(); props.onFinished();
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
}; };
const hasFeedback = CountlyAnalytics.instance.canEnable(); const hasFeedback = CountlyAnalytics.instance.canEnable();
const onFinished = (sendFeedback) => { const onFinished = (sendFeedback: boolean): void => {
if (hasFeedback && sendFeedback) { if (hasFeedback && sendFeedback) {
CountlyAnalytics.instance.reportFeedback(parseInt(rating, 10), comment); CountlyAnalytics.instance.reportFeedback(rating, comment);
Modal.createTrackedDialog('Feedback sent', '', InfoDialog, { Modal.createTrackedDialog('Feedback sent', '', InfoDialog, {
title: _t('Feedback sent'), title: _t('Feedback sent'),
description: _t('Thank you!'), description: _t('Thank you!'),
@ -65,8 +68,8 @@ export default (props) => {
<StyledRadioGroup <StyledRadioGroup
name="feedbackRating" name="feedbackRating"
value={rating} value={String(rating)}
onChange={setRating} onChange={(r) => setRating(parseInt(r, 10) as Rating)}
definitions={[ definitions={[
{ value: "1", label: "😠" }, { value: "1", label: "😠" },
{ value: "2", label: "😞" }, { value: "2", label: "😞" },
@ -138,7 +141,9 @@ export default (props) => {
{ countlyFeedbackSection } { countlyFeedbackSection }
</React.Fragment>} </React.Fragment>}
button={hasFeedback ? _t("Send feedback") : _t("Go back")} button={hasFeedback ? _t("Send feedback") : _t("Go back")}
buttonDisabled={hasFeedback && rating === ""} buttonDisabled={hasFeedback && !rating}
onFinished={onFinished} onFinished={onFinished}
/>); />);
}; };
export default FeedbackDialog;

View file

@ -15,12 +15,20 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; 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"; import { logger } from "matrix-js-sdk/src/logger";
@ -30,41 +38,56 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
const PHASE_VERIFIED = 3; const PHASE_VERIFIED = 3;
const PHASE_CANCELLED = 4; const PHASE_CANCELLED = 4;
@replaceableComponent("views.dialogs.IncomingSasDialog") interface IProps extends IDialogProps {
export default class IncomingSasDialog extends React.Component { verifier: VerificationBase; // TODO types
static propTypes = { }
verifier: PropTypes.object.isRequired,
};
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); super(props);
let phase = PHASE_START; let phase = PHASE_START;
if (this.props.verifier.cancelled) { if (this.props.verifier.hasBeenCancelled) {
logger.log("Verifier was cancelled in the background."); logger.log("Verifier was cancelled in the background.");
phase = PHASE_CANCELLED; phase = PHASE_CANCELLED;
} }
this._showSasEvent = null; this.showSasEvent = null;
this.state = { this.state = {
phase: phase, phase: phase,
sasVerified: false, sasVerified: false,
opponentProfile: null, opponentProfile: null,
opponentProfileError: null, opponentProfileError: null,
sas: null,
}; };
this.props.verifier.on('show_sas', this._onVerifierShowSas); this.props.verifier.on('show_sas', this.onVerifierShowSas);
this.props.verifier.on('cancel', this._onVerifierCancel); this.props.verifier.on('cancel', this.onVerifierCancel);
this._fetchOpponentProfile(); this.fetchOpponentProfile();
} }
componentWillUnmount() { public componentWillUnmount(): void {
if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) { 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 { try {
const prof = await MatrixClientPeg.get().getProfileInfo( const prof = await MatrixClientPeg.get().getProfileInfo(
this.props.verifier.userId, this.props.verifier.userId,
@ -79,53 +102,49 @@ export default class IncomingSasDialog extends React.Component {
} }
} }
_onFinished = () => { private onFinished = (): void => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED); this.props.onFinished(this.state.phase === PHASE_VERIFIED);
} };
_onCancelClick = () => { private onCancelClick = (): void => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED); this.props.onFinished(this.state.phase === PHASE_VERIFIED);
} };
_onContinueClick = () => { private onContinueClick = (): void => {
this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM }); this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM });
this.props.verifier.verify().then(() => { this.props.verifier.verify().then(() => {
this.setState({ phase: PHASE_VERIFIED }); this.setState({ phase: PHASE_VERIFIED });
}).catch((e) => { }).catch((e) => {
logger.log("Verification failed", e); logger.log("Verification failed", e);
}); });
} };
_onVerifierShowSas = (e) => { private onVerifierShowSas = (e: ISasEvent): void => {
this._showSasEvent = e; this.showSasEvent = e;
this.setState({ this.setState({
phase: PHASE_SHOW_SAS, phase: PHASE_SHOW_SAS,
sas: e.sas, sas: e.sas,
}); });
} };
_onVerifierCancel = (e) => { private onVerifierCancel = (): void => {
this.setState({ this.setState({
phase: PHASE_CANCELLED, phase: PHASE_CANCELLED,
}); });
} };
_onSasMatchesClick = () => { private onSasMatchesClick = (): void => {
this._showSasEvent.confirm(); this.showSasEvent.confirm();
this.setState({ this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM, phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
}); });
} };
_onVerifiedDoneClick = () => { private onVerifiedDoneClick = (): void => {
this.props.onFinished(true); 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(); const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
let profile; let profile;
@ -192,27 +211,24 @@ export default class IncomingSasDialog extends React.Component {
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
hasCancel={true} hasCancel={true}
onPrimaryButtonClick={this._onContinueClick} onPrimaryButtonClick={this.onContinueClick}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
/> />
</div> </div>
); );
} }
_renderPhaseShowSas() { private renderPhaseShowSas(): JSX.Element {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas return <VerificationShowSas
sas={this._showSasEvent.sas} sas={this.showSasEvent.sas}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
onDone={this._onSasMatchesClick} onDone={this.onSasMatchesClick}
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()} isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
inDialog={true} inDialog={true}
/>; />;
} }
_renderPhaseWaitForPartnerToConfirm() { private renderPhaseWaitForPartnerToConfirm(): JSX.Element {
const Spinner = sdk.getComponent("views.elements.Spinner");
return ( return (
<div> <div>
<Spinner /> <Spinner />
@ -221,41 +237,38 @@ export default class IncomingSasDialog extends React.Component {
); );
} }
_renderPhaseVerified() { private renderPhaseVerified(): JSX.Element {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); return <VerificationComplete onDone={this.onVerifiedDoneClick} />;
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
} }
_renderPhaseCancelled() { private renderPhaseCancelled(): JSX.Element {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); return <VerificationCancelled onDone={this.onCancelClick} />;
return <VerificationCancelled onDone={this._onCancelClick} />;
} }
render() { public render(): JSX.Element {
let body; let body;
switch (this.state.phase) { switch (this.state.phase) {
case PHASE_START: case PHASE_START:
body = this._renderPhaseStart(); body = this.renderPhaseStart();
break; break;
case PHASE_SHOW_SAS: case PHASE_SHOW_SAS:
body = this._renderPhaseShowSas(); body = this.renderPhaseShowSas();
break; break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM: case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderPhaseWaitForPartnerToConfirm(); body = this.renderPhaseWaitForPartnerToConfirm();
break; break;
case PHASE_VERIFIED: case PHASE_VERIFIED:
body = this._renderPhaseVerified(); body = this.renderPhaseVerified();
break; break;
case PHASE_CANCELLED: case PHASE_CANCELLED:
body = this._renderPhaseCancelled(); body = this.renderPhaseCancelled();
break; break;
} }
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return ( return (
<BaseDialog <BaseDialog
title={_t("Incoming Verification Request")} title={_t("Incoming Verification Request")}
onFinished={this._onFinished} onFinished={this.onFinished}
fixedWidth={false} fixedWidth={false}
> >
{ body } { body }

View file

@ -15,32 +15,28 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
export default class IntegrationsDisabledDialog extends React.Component { export default class IntegrationsDisabledDialog extends React.Component<IProps> {
static propTypes = { private onAcknowledgeClick = (): void => {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished(); this.props.onFinished();
}; };
_onOpenSettingsClick = () => { private onOpenSettingsClick = (): void => {
this.props.onFinished(); this.props.onFinished();
dis.fire(Action.ViewUserSettings); dis.fire(Action.ViewUserSettings);
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog <BaseDialog
className='mx_IntegrationsDisabledDialog' className='mx_IntegrationsDisabledDialog'
@ -53,9 +49,9 @@ export default class IntegrationsDisabledDialog extends React.Component {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Settings")} primaryButton={_t("Settings")}
onPrimaryButtonClick={this._onOpenSettingsClick} onPrimaryButtonClick={this.onOpenSettingsClick}
cancelButton={_t("OK")} cancelButton={_t("OK")}
onCancel={this._onAcknowledgeClick} onCancel={this.onAcknowledgeClick}
/> />
</BaseDialog> </BaseDialog>
); );

View file

@ -15,23 +15,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {}
@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog") @replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
export default class IntegrationsImpossibleDialog extends React.Component { export default class IntegrationsImpossibleDialog extends React.Component<IProps> {
static propTypes = { private onAcknowledgeClick = (): void => {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished(); this.props.onFinished();
}; };
render() { public render(): JSX.Element {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -54,7 +52,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("OK")} primaryButton={_t("OK")}
onPrimaryButtonClick={this._onAcknowledgeClick} onPrimaryButtonClick={this.onAcknowledgeClick}
hasCancel={false} hasCancel={false}
/> />
</BaseDialog> </BaseDialog>

View file

@ -17,69 +17,88 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton'; 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 { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.InteractiveAuthDialog")
export default class InteractiveAuthDialog extends React.Component { export default class InteractiveAuthDialog extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
// matrix client to use for UI auth requests super(props);
matrixClient: PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on this.state = {
// mount. authError: null,
authData: PropTypes.shape({
flows: PropTypes.array,
params: PropTypes.object,
session: PropTypes.string,
}),
// callback // See _onUpdateStagePhase()
makeRequest: PropTypes.func.isRequired, uiaStage: null,
uiaStagePhase: null,
};
}
onFinished: PropTypes.func.isRequired, private getDefaultDialogAesthetics(): IDialogAesthetics {
// 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() {
const ssoAesthetics = { const ssoAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: { [SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"), 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) { if (success) {
this.props.onFinished(true, result); this.props.onFinished(true, result);
} else { } 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() // We copy the stage and stage phase params into state for title selection in render()
this.setState({ uiaStage: newStage, uiaStagePhase: newPhase }); this.setState({ uiaStage: newStage, uiaStagePhase: newPhase });
}; };
_onDismissClick = () => { private onDismissClick = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
render() { public render(): JSX.Element {
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
// Let's pick a title, body, and other params text that we'll show to the user. The order // 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. // 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 body = this.state.authError ? null : this.props.body;
let continueText = null; let continueText = null;
let continueKind = null; let continueKind = null;
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics(); const dialogAesthetics = this.props.aestheticsForStagePhases || this.getDefaultDialogAesthetics();
if (!this.state.authError && dialogAesthetics) { if (!this.state.authError && dialogAesthetics) {
if (dialogAesthetics[this.state.uiaStage]) { if (dialogAesthetics[this.state.uiaStage]) {
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase]; 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 id='mx_Dialog_content'>
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div> <div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
<br /> <br />
<AccessibleButton onClick={this._onDismissClick} <AccessibleButton onClick={this.onDismissClick}
className="mx_GeneralButton" className="mx_GeneralButton"
autoFocus="true" autoFocus={true}
> >
{ _t("Dismiss") } { _t("Dismiss") }
</AccessibleButton> </AccessibleButton>
@ -165,12 +181,11 @@ export default class InteractiveAuthDialog extends React.Component {
<div id='mx_Dialog_content'> <div id='mx_Dialog_content'>
{ body } { body }
<InteractiveAuth <InteractiveAuth
ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient} matrixClient={this.props.matrixClient}
authData={this.props.authData} authData={this.props.authData}
makeRequest={this.props.makeRequest} makeRequest={this.props.makeRequest}
onAuthFinished={this._onAuthFinished} onAuthFinished={this.onAuthFinished}
onStagePhaseChange={this._onUpdateStagePhase} onStagePhaseChange={this.onUpdateStagePhase}
continueText={continueText} continueText={continueText}
continueKind={continueKind} continueKind={continueKind}
/> />

View file

@ -15,20 +15,29 @@ limitations under the License.
*/ */
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef } from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; 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, failures,
source, source,
continuation, continuation,
onFinished, onFinished,
}) { }) => {
const RETRIES = 2; 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 [retry, setRetry] = useState(RETRIES);
const [cancelled, setCancelled] = useState(false); const [cancelled, setCancelled] = useState(false);
const [retrying, setRetrying] = useState(false); const [retrying, setRetrying] = useState(false);
@ -107,4 +116,6 @@ export default function KeySignatureUploadFailedDialog({
{ body } { body }
</BaseDialog> </BaseDialog>
); );
} };
export default KeySignatureUploadFailedDialog;

View file

@ -19,8 +19,13 @@ import React from 'react';
import QuestionDialog from './QuestionDialog'; import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; 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 brand = SdkConfig.get().brand;
const description1 = _t( const description1 = _t(
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " + "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} onFinished={props.onFinished}
/>); />);
}; };
export default LazyLoadingDisabledDialog;

View file

@ -19,8 +19,11 @@ import React from 'react';
import QuestionDialog from './QuestionDialog'; import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; 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 brand = SdkConfig.get().brand;
const description = const description =
_t( _t(
@ -38,3 +41,5 @@ export default (props) => {
onFinished={props.onFinished} onFinished={props.onFinished}
/>); />);
}; };
export default LazyLoadingResyncDialog;

View file

@ -19,37 +19,31 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils'; import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
export default class ManualDeviceKeyVerificationDialog extends React.Component { export default class ManualDeviceKeyVerificationDialog extends React.Component<IProps> {
static propTypes = { private onLegacyFinished = (confirm: boolean): void => {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
_onCancelClick = () => {
this.props.onFinished(false);
}
_onLegacyFinished = (confirm) => {
if (confirm) { if (confirm) {
MatrixClientPeg.get().setDeviceVerified( MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true, this.props.userId, this.props.device.deviceId, true,
); );
} }
this.props.onFinished(confirm); this.props.onFinished(confirm);
} };
render() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
public render(): JSX.Element {
let text; let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) { if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("Confirm by comparing the following with the User Settings in your other session:"); 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")} title={_t("Verify session")}
description={body} description={body}
button={_t("Verify session")} button={_t("Verify session")}
onFinished={this._onLegacyFinished} onFinished={this.onLegacyFinished}
/> />
); );
} }

View file

@ -15,21 +15,39 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from "../../../index";
import { wantsDateSeparator } from '../../../DateUtils'; import { wantsDateSeparator } from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore'; import SettingsStore from '../../../settings/SettingsStore';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.MessageEditHistoryDialog")
export default class MessageEditHistoryDialog extends React.PureComponent { export default class MessageEditHistoryDialog extends React.PureComponent<IProps, IState> {
static propTypes = { constructor(props: IProps) {
mxEvent: PropTypes.object.isRequired,
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
originalEvent: null, 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)) { if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
// bail out on backwards as we only paginate in one direction // bail out on backwards as we only paginate in one direction
return false; return false;
@ -50,13 +68,13 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
const roomId = this.props.mxEvent.getRoomId(); const roomId = this.props.mxEvent.getRoomId();
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const { resolve, reject, promise } = defer<boolean>();
let result; let result;
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;});
try { try {
result = await client.relations( result = await client.relations(
roomId, eventId, "m.replace", "m.room.message", opts); roomId, eventId, RelationType.Replace, EventType.RoomMessage, opts);
} catch (error) { } catch (error) {
// log if the server returned an error // log if the server returned an error
if (error.errcode) { if (error.errcode) {
@ -67,7 +85,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
} }
const newEvents = result.events; const newEvents = result.events;
this._locallyRedactEventsIfNeeded(newEvents); this.locallyRedactEventsIfNeeded(newEvents);
this.setState({ this.setState({
originalEvent: this.state.originalEvent || result.originalEvent, originalEvent: this.state.originalEvent || result.originalEvent,
events: this.state.events.concat(newEvents), events: this.state.events.concat(newEvents),
@ -78,9 +96,9 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
resolve(hasMoreResults); resolve(hasMoreResults);
}); });
return promise; return promise;
} };
_locallyRedactEventsIfNeeded(newEvents) { private locallyRedactEventsIfNeeded(newEvents: MatrixEvent[]): void {
const roomId = this.props.mxEvent.getRoomId(); const roomId = this.props.mxEvent.getRoomId();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
@ -95,13 +113,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
} }
} }
componentDidMount() { public componentDidMount(): void {
this.loadMoreEdits(); this.loadMoreEdits();
} }
_renderEdits() { private renderEdits(): JSX.Element[] {
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const nodes = []; const nodes = [];
let lastEvent; let lastEvent;
let allEvents = this.state.events; let allEvents = this.state.events;
@ -128,7 +144,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
return nodes; return nodes;
} }
render() { public render(): JSX.Element {
let content; let content;
if (this.state.error) { if (this.state.error) {
const { error } = this.state; const { error } = this.state;
@ -149,20 +165,17 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
</p>); </p>);
} }
} else if (this.state.isLoading) { } else if (this.state.isLoading) {
const Spinner = sdk.getComponent("elements.Spinner");
content = <Spinner />; content = <Spinner />;
} else { } else {
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
content = (<ScrollPanel content = (<ScrollPanel
className="mx_MessageEditHistoryDialog_scrollPanel" className="mx_MessageEditHistoryDialog_scrollPanel"
onFillRequest={this.loadMoreEdits} onFillRequest={this.loadMoreEdits}
stickyBottom={false} stickyBottom={false}
startAtBottom={false} startAtBottom={false}
> >
<ul className="mx_MessageEditHistoryDialog_edits">{ this._renderEdits() }</ul> <ul className="mx_MessageEditHistoryDialog_edits">{ this.renderEdits() }</ul>
</ScrollPanel>); </ScrollPanel>);
} }
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog <BaseDialog
className='mx_MessageEditHistoryDialog' className='mx_MessageEditHistoryDialog'

View file

@ -16,29 +16,30 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from "classnames"; import classNames from "classnames";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
export default class QuestionDialog extends React.Component { interface IProps extends IDialogProps {
static propTypes = { title?: string;
title: PropTypes.string, description?: React.ReactNode;
description: PropTypes.node, extraButtons?: React.ReactNode;
extraButtons: PropTypes.node, button?: string;
button: PropTypes.string, buttonDisabled?: boolean;
buttonDisabled: PropTypes.bool, danger?: boolean;
danger: PropTypes.bool, focus?: boolean;
focus: PropTypes.bool, headerImage?: string;
onFinished: PropTypes.func.isRequired, quitOnly?: boolean; // quitOnly doesn't show the cancel button just the quit [x].
headerImage: PropTypes.string, fixedWidth?: boolean;
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x]. className?: string;
fixedWidth: PropTypes.bool, hasCancelButton?: boolean;
className: PropTypes.string, cancelButton?: React.ReactNode;
}; }
static defaultProps = { export default class QuestionDialog extends React.Component<IProps> {
public static defaultProps: Partial<IProps> = {
title: "", title: "",
description: "", description: "",
extraButtons: null, extraButtons: null,
@ -48,17 +49,19 @@ export default class QuestionDialog extends React.Component {
quitOnly: false, quitOnly: false,
}; };
onOk = () => { private onOk = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
}; };
onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
render() { public render(): JSX.Element {
// Converting these to imports breaks wrench tests
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let primaryButtonClass = ""; let primaryButtonClass = "";
if (this.props.danger) { if (this.props.danger) {
primaryButtonClass = "danger"; primaryButtonClass = "danger";

View file

@ -17,27 +17,27 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
export default class SessionRestoreErrorDialog extends React.Component { export default class SessionRestoreErrorDialog extends React.Component<IProps> {
static propTypes = { private sendBugReport = (): void => {
error: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
_sendBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {}); Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
}; };
_onClearStorageClick = () => { private onClearStorageClick = (): void => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, { Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
title: _t("Sign out"), title: _t("Sign out"),
description: 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 // Is this likely to help? Probably not, but giving only one button
// that clears your storage seems awful. // that clears your storage seems awful.
window.location.reload(true); window.location.reload();
}; };
render() { public render(): JSX.Element {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const clearStorageButton = ( const clearStorageButton = (
<button onClick={this._onClearStorageClick} className="danger"> <button onClick={this.onClearStorageClick} className="danger">
{ _t("Clear Storage and Sign Out") } { _t("Clear Storage and Sign Out") }
</button> </button>
); );
@ -68,7 +66,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
let dialogButtons; let dialogButtons;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
dialogButtons = <DialogButtons primaryButton={_t("Send Logs")} dialogButtons = <DialogButtons primaryButton={_t("Send Logs")}
onPrimaryButtonClick={this._sendBugReport} onPrimaryButtonClick={this.sendBugReport}
focus={true} focus={true}
hasCancel={false} hasCancel={false}
> >
@ -76,7 +74,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
</DialogButtons>; </DialogButtons>;
} else { } else {
dialogButtons = <DialogButtons primaryButton={_t("Refresh")} dialogButtons = <DialogButtons primaryButton={_t("Refresh")}
onPrimaryButtonClick={this._onRefreshClick} onPrimaryButtonClick={this.onRefreshClick}
focus={true} focus={true}
hasCancel={false} hasCancel={false}
> >

View file

@ -16,13 +16,26 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as Email from '../../../email'; import * as Email from '../../../email';
import AddThreepid from '../../../AddThreepid'; import AddThreepid from '../../../AddThreepid';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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. * Prompt the user to set an email address.
@ -30,26 +43,25 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* On success, `onFinished(true)` is called. * On success, `onFinished(true)` is called.
*/ */
@replaceableComponent("views.dialogs.SetEmailDialog") @replaceableComponent("views.dialogs.SetEmailDialog")
export default class SetEmailDialog extends React.Component { export default class SetEmailDialog extends React.Component<IProps, IState> {
static propTypes = { private addThreepid: AddThreepid;
onFinished: PropTypes.func.isRequired,
};
state = { constructor(props: IProps) {
emailAddress: '', super(props);
emailBusy: false,
};
onEmailAddressChanged = value => { this.state = {
emailAddress: '',
emailBusy: false,
};
}
private onEmailAddressChanged = (value: string): void => {
this.setState({ this.setState({
emailAddress: value, emailAddress: value,
}); });
}; };
onSubmit = () => { private onSubmit = (): void => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const emailAddress = this.state.emailAddress; const emailAddress = this.state.emailAddress;
if (!Email.looksValid(emailAddress)) { if (!Email.looksValid(emailAddress)) {
Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, { Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, {
@ -58,8 +70,8 @@ export default class SetEmailDialog extends React.Component {
}); });
return; return;
} }
this._addThreepid = new AddThreepid(); this.addThreepid = new AddThreepid();
this._addThreepid.addEmailAddress(emailAddress).then(() => { this.addThreepid.addEmailAddress(emailAddress).then(() => {
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
title: _t("Verification Pending"), title: _t("Verification Pending"),
description: _t( description: _t(
@ -80,11 +92,11 @@ export default class SetEmailDialog extends React.Component {
this.setState({ emailBusy: true }); this.setState({ emailBusy: true });
}; };
onCancelled = () => { private onCancelled = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
onEmailDialogFinished = ok => { private onEmailDialogFinished = (ok: boolean): void => {
if (ok) { if (ok) {
this.verifyEmailAddress(); this.verifyEmailAddress();
} else { } else {
@ -92,13 +104,12 @@ export default class SetEmailDialog extends React.Component {
} }
}; };
verifyEmailAddress() { private verifyEmailAddress(): void {
this._addThreepid.checkEmailLinkClicked().then(() => { this.addThreepid.checkEmailLinkClicked().then(() => {
this.props.onFinished(true); this.props.onFinished(true);
}, (err) => { }, (err) => {
this.setState({ emailBusy: false }); this.setState({ emailBusy: false });
if (err.errcode == 'M_THREEPID_AUTH_FAILED') { if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const message = _t("Unable to verify email address.") + " " + 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."); _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, { Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, {
@ -108,7 +119,6 @@ export default class SetEmailDialog extends React.Component {
onFinished: this.onEmailDialogFinished, onFinished: this.onEmailDialogFinished,
}); });
} else { } else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err); console.error("Unable to verify email address: " + err);
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
title: _t("Unable to verify email address."), title: _t("Unable to verify email address."),
@ -118,15 +128,10 @@ export default class SetEmailDialog extends React.Component {
}); });
} }
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const EditableText = sdk.getComponent('elements.EditableText');
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
initialValue={this.state.emailAddress} initialValue={this.state.emailAddress}
className="mx_SetEmailDialog_email_input" className="mx_SetEmailDialog_email_input"
autoFocus="true"
placeholder={_t("Email address")} placeholder={_t("Email address")}
placeholderClassName="mx_SetEmailDialog_email_input_placeholder" placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={false} blurToCancel={false}

View file

@ -17,11 +17,12 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { CommandCategories, Commands } from "../../../SlashCommands"; import { CommandCategories, Commands } from "../../../SlashCommands";
import * as sdk from "../../../index"; import { IDialogProps } from "./IDialogProps";
import InfoDialog from "./InfoDialog";
export default ({ onFinished }) => { interface IProps extends IDialogProps {}
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
const SlashCommandHelpDialog: React.FC<IProps> = ({ onFinished }) => {
const categories = {}; const categories = {};
Commands.forEach(cmd => { Commands.forEach(cmd => {
if (!cmd.isEnabled()) return; if (!cmd.isEnabled()) return;
@ -62,3 +63,5 @@ export default ({ onFinished }) => {
hasCloseButton={true} hasCloseButton={true}
onFinished={onFinished} />; onFinished={onFinished} />;
}; };
export default SlashCommandHelpDialog;

View file

@ -15,40 +15,36 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.StorageEvictedDialog")
export default class StorageEvictedDialog extends React.Component { export default class StorageEvictedDialog extends React.Component<IProps> {
static propTypes = { private sendBugReport = (ev: React.MouseEvent): void => {
onFinished: PropTypes.func.isRequired,
};
_sendBugReport = ev => {
ev.preventDefault(); ev.preventDefault();
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Storage evicted', 'Send Bug Report Dialog', BugReportDialog, {}); Modal.createTrackedDialog('Storage evicted', 'Send Bug Report Dialog', BugReportDialog, {});
}; };
_onSignOutClick = () => { private onSignOutClick = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let logRequest; let logRequest;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
logRequest = _t( logRequest = _t(
"To help us prevent this in future, please <a>send us logs</a>.", "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> ) } { logRequest }</p>
</div> </div>
<DialogButtons primaryButton={_t("Sign out")} <DialogButtons primaryButton={_t("Sign out")}
onPrimaryButtonClick={this._onSignOutClick} onPrimaryButtonClick={this.onSignOutClick}
focus={true} focus={true}
hasCancel={false} hasCancel={false}
/> />

View file

@ -15,42 +15,47 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import * as sdk from '../../../index';
import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms"; import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
import classNames from 'classnames'; import classNames from 'classnames';
import * as ScalarMessaging from "../../../ScalarMessaging"; import * as ScalarMessaging from "../../../ScalarMessaging";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
export default class TabbedIntegrationManagerDialog extends React.Component { export default class TabbedIntegrationManagerDialog extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
/**
* Optional room where the integration manager should be open to
*/
room: PropTypes.instanceOf(Room),
/**
* Optional screen to open on the integration manager
*/
screen: PropTypes.string,
/**
* Optional integration ID to open in the integration manager
*/
integrationId: PropTypes.string,
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -63,11 +68,11 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
}; };
} }
componentDidMount() { public componentDidMount(): void {
this.openManager(0, true); 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; if (i === this.state.currentIndex && !force) return;
const manager = this.state.managers[i]; const manager = this.state.managers[i];
@ -120,8 +125,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
} }
}; };
_renderTabs() { private renderTabs(): JSX.Element[] {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
return this.state.managers.map((m, i) => { return this.state.managers.map((m, i) => {
const classes = classNames({ const classes = classNames({
'mx_TabbedIntegrationManagerDialog_tab': true, 'mx_TabbedIntegrationManagerDialog_tab': true,
@ -140,8 +144,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
}); });
} }
_renderTab() { public renderTab(): JSX.Element {
const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
let uiUrl = null; let uiUrl = null;
if (this.state.currentScalarClient) { if (this.state.currentScalarClient) {
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom( uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
@ -151,7 +154,6 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
); );
} }
return <IntegrationManager return <IntegrationManager
configured={true}
loading={this.state.currentLoading} loading={this.state.currentLoading}
connected={this.state.currentConnected} connected={this.state.currentConnected}
url={uiUrl} url={uiUrl}
@ -159,14 +161,14 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
/>; />;
} }
render() { public render(): JSX.Element {
return ( return (
<div className='mx_TabbedIntegrationManagerDialog_container'> <div className='mx_TabbedIntegrationManagerDialog_container'>
<div className='mx_TabbedIntegrationManagerDialog_tabs'> <div className='mx_TabbedIntegrationManagerDialog_tabs'>
{ this._renderTabs() } { this.renderTabs() }
</div> </div>
<div className='mx_TabbedIntegrationManagerDialog_currentManager'> <div className='mx_TabbedIntegrationManagerDialog_currentManager'>
{ this._renderTab() } { this.renderTab() }
</div> </div>
</div> </div>
); );

View file

@ -14,33 +14,39 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { ChangeEvent, createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import Field from "../elements/Field"; import Field from "../elements/Field";
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.dialogs.TextInputDialog")
export default class TextInputDialog extends React.Component { export default class TextInputDialog extends React.Component<IProps, IState> {
static propTypes = { private field = createRef<Field>();
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,
};
static defaultProps = { public static defaultProps = {
title: "", title: "",
value: "", value: "",
description: "", description: "",
@ -49,11 +55,9 @@ export default class TextInputDialog extends React.Component {
hasCancel: true, hasCancel: true,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._field = createRef();
this.state = { this.state = {
value: this.props.value, value: this.props.value,
busy: false, busy: false,
@ -61,23 +65,23 @@ export default class TextInputDialog extends React.Component {
}; };
} }
componentDidMount() { public componentDidMount(): void {
if (this.props.focus) { if (this.props.focus) {
// Set the cursor at the end of the text input // Set the cursor at the end of the text input
// this._field.current.value = this.props.value; // 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(); ev.preventDefault();
if (this.props.validator) { if (this.props.validator) {
this.setState({ busy: true }); this.setState({ busy: true });
await this._field.current.validate({ allowEmpty: false }); await this.field.current.validate({ allowEmpty: false });
if (!this._field.current.state.valid) { if (!this.field.current.state.valid) {
this._field.current.focus(); this.field.current.focus();
this._field.current.validate({ allowEmpty: false, focused: true }); this.field.current.validate({ allowEmpty: false, focused: true });
this.setState({ busy: false }); this.setState({ busy: false });
return; return;
} }
@ -85,17 +89,17 @@ export default class TextInputDialog extends React.Component {
this.props.onFinished(true, this.state.value); this.props.onFinished(true, this.state.value);
}; };
onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
onChange = ev => { private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
value: ev.target.value, value: ev.target.value,
}); });
}; };
onValidate = async fieldState => { private onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.props.validator(fieldState); const result = await this.props.validator(fieldState);
this.setState({ this.setState({
valid: result.valid, valid: result.valid,
@ -103,9 +107,7 @@ export default class TextInputDialog extends React.Component {
return result; return result;
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog <BaseDialog
className="mx_TextInputDialog" className="mx_TextInputDialog"
@ -121,13 +123,12 @@ export default class TextInputDialog extends React.Component {
<div> <div>
<Field <Field
className="mx_TextInputDialog_input" className="mx_TextInputDialog_input"
ref={this._field} ref={this.field}
type="text" type="text"
label={this.props.placeholder} label={this.props.placeholder}
value={this.state.value} value={this.state.value}
onChange={this.onChange} onChange={this.onChange}
onValidate={this.props.validator ? this.onValidate : undefined} onValidate={this.props.validator ? this.onValidate : undefined}
size="64"
/> />
</div> </div>
</div> </div>

View file

@ -17,11 +17,18 @@ limitations under the License.
import filesize from 'filesize'; import filesize from 'filesize';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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 * 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. * the size of the file.
*/ */
@replaceableComponent("views.dialogs.UploadFailureDialog") @replaceableComponent("views.dialogs.UploadFailureDialog")
export default class UploadFailureDialog extends React.Component { export default class UploadFailureDialog extends React.Component<IProps> {
static propTypes = { private onCancelClick = (): void => {
badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
totalFiles: PropTypes.number.isRequired,
contentMessages: PropTypes.instanceOf(ContentMessages).isRequired,
onFinished: PropTypes.func.isRequired,
}
_onCancelClick = () => {
this.props.onFinished(false); this.props.onFinished(false);
} };
_onUploadClick = () => { private onUploadClick = (): void => {
this.props.onFinished(true); 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 message;
let preview; let preview;
let buttons; let buttons;
@ -65,7 +62,7 @@ export default class UploadFailureDialog extends React.Component {
); );
buttons = <DialogButtons primaryButton={_t('OK')} buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false} hasCancel={false}
onPrimaryButtonClick={this._onCancelClick} onPrimaryButtonClick={this.onCancelClick}
focus={true} focus={true}
/>; />;
} else if (this.props.totalFiles === this.props.badFiles.length) { } 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')} buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false} hasCancel={false}
onPrimaryButtonClick={this._onCancelClick} onPrimaryButtonClick={this.onCancelClick}
focus={true} focus={true}
/>; />;
} else { } else {
@ -96,17 +93,17 @@ export default class UploadFailureDialog extends React.Component {
const howManyOthers = this.props.totalFiles - this.props.badFiles.length; const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
buttons = <DialogButtons buttons = <DialogButtons
primaryButton={_t('Upload %(count)s other files', { count: howManyOthers })} primaryButton={_t('Upload %(count)s other files', { count: howManyOthers })}
onPrimaryButtonClick={this._onUploadClick} onPrimaryButtonClick={this.onUploadClick}
hasCancel={true} hasCancel={true}
cancelButton={_t("Cancel All")} cancelButton={_t("Cancel All")}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
focus={true} focus={true}
/>; />;
} }
return ( return (
<BaseDialog className='mx_UploadFailureDialog' <BaseDialog className='mx_UploadFailureDialog'
onFinished={this._onCancelClick} onFinished={this.onCancelClick}
title={_t("Upload Error")} title={_t("Upload Error")}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >

View file

@ -47,15 +47,15 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
}; };
} }
private onAllow = () => { private onAllow = (): void => {
this.onPermissionSelection(true); this.onPermissionSelection(true);
}; };
private onDeny = () => { private onDeny = (): void => {
this.onPermissionSelection(false); this.onPermissionSelection(false);
}; };
private onPermissionSelection(allowed: boolean) { private onPermissionSelection(allowed: boolean): void {
if (this.state.rememberSelection) { if (this.state.rememberSelection) {
logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`); logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
@ -68,11 +68,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
this.props.onFinished(allowed); this.props.onFinished(allowed);
} }
private onRememberSelectionChange = (newVal: boolean) => { private onRememberSelectionChange = (newVal: boolean): void => {
this.setState({ rememberSelection: newVal }); this.setState({ rememberSelection: newVal });
}; };
public render() { public render(): JSX.Element {
return ( return (
<BaseDialog <BaseDialog
className='mx_WidgetOpenIDPermissionsDialog' className='mx_WidgetOpenIDPermissionsDialog'

View file

@ -16,32 +16,64 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager'; 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"; import { logger } from "matrix-js-sdk/src/logger";
const RESTORE_TYPE_PASSPHRASE = 0; enum RestoreType {
const RESTORE_TYPE_RECOVERYKEY = 1; Passphrase = "passphrase",
const RESTORE_TYPE_SECRET_STORAGE = 2; 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 * Dialog for restoring e2e keys from a backup and the user's recovery key
*/ */
export default class RestoreKeyBackupDialog extends React.PureComponent { export default class RestoreKeyBackupDialog extends React.PureComponent<IProps, IState> {
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,
};
static defaultProps = { static defaultProps = {
showSummary: true, showSummary: true,
}; };
@ -60,58 +92,58 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
forceRecoveryKey: false, forceRecoveryKey: false,
passPhrase: '', passPhrase: '',
restoreType: null, restoreType: null,
progress: { stage: "prefetch" }, progress: { stage: ProgressState.PreFetch },
}; };
} }
componentDidMount() { public componentDidMount(): void {
this._loadBackupStatus(); this.loadBackupStatus();
} }
_onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
} };
_onDone = () => { private onDone = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
} };
_onUseRecoveryKeyClick = () => { private onUseRecoveryKeyClick = (): void => {
this.setState({ this.setState({
forceRecoveryKey: true, forceRecoveryKey: true,
}); });
} };
_progressCallback = (data) => { private progressCallback = (data): void => {
this.setState({ this.setState({
progress: data, progress: data,
}); });
} };
_onResetRecoveryClick = () => { private onResetRecoveryClick = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
accessSecretStorage(() => {}, /* forceReset = */ true); accessSecretStorage(async () => {}, /* forceReset = */ true);
} };
_onRecoveryKeyChange = (e) => { private onRecoveryKeyChange = (e): void => {
this.setState({ this.setState({
recoveryKey: e.target.value, recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
}); });
} };
_onPassPhraseNext = async () => { private onPassPhraseNext = async (): Promise<void> => {
this.setState({ this.setState({
loading: true, loading: true,
restoreError: null, restoreError: null,
restoreType: RESTORE_TYPE_PASSPHRASE, restoreType: RestoreType.Passphrase,
}); });
try { try {
// We do still restore the key backup: we must ensure that the key backup key // 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. // is the right one and restoring it is currently the only way we can do this.
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword( const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
this.state.passPhrase, undefined, undefined, this.state.backupInfo, this.state.passPhrase, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
if (this.props.keyCallback) { if (this.props.keyCallback) {
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword( const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
@ -135,20 +167,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
restoreError: e, restoreError: e,
}); });
} }
} };
_onRecoveryKeyNext = async () => { private onRecoveryKeyNext = async (): Promise<void> => {
if (!this.state.recoveryKeyValid) return; if (!this.state.recoveryKeyValid) return;
this.setState({ this.setState({
loading: true, loading: true,
restoreError: null, restoreError: null,
restoreType: RESTORE_TYPE_RECOVERYKEY, restoreType: RestoreType.RecoveryKey,
}); });
try { try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey( const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey, undefined, undefined, this.state.backupInfo, this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
if (this.props.keyCallback) { if (this.props.keyCallback) {
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey); const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
@ -169,31 +201,30 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
restoreError: e, restoreError: e,
}); });
} }
} };
_onPassPhraseChange = (e) => { private onPassPhraseChange = (e): void => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: e.target.value,
}); });
} };
async _restoreWithSecretStorage() { private async restoreWithSecretStorage(): Promise<void> {
this.setState({ this.setState({
loading: true, loading: true,
restoreError: null, restoreError: null,
restoreType: RESTORE_TYPE_SECRET_STORAGE, restoreType: RestoreType.SecretStorage,
}); });
try { try {
// `accessSecretStorage` may prompt for storage access as needed. // `accessSecretStorage` may prompt for storage access as needed.
const recoverInfo = await accessSecretStorage(async () => { await accessSecretStorage(async () => {
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo, undefined, undefined, this.state.backupInfo, undefined, undefined,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
}); });
this.setState({ this.setState({
loading: false, loading: false,
recoverInfo,
}); });
} catch (e) { } catch (e) {
logger.log("Error restoring backup", e); logger.log("Error restoring backup", e);
@ -204,14 +235,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} }
} }
async _restoreWithCachedKey(backupInfo) { private async restoreWithCachedKey(backupInfo): Promise<boolean> {
if (!backupInfo) return false; if (!backupInfo) return false;
try { try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache( const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache(
undefined, /* targetRoomId */ undefined, /* targetRoomId */
undefined, /* targetSessionId */ undefined, /* targetSessionId */
backupInfo, backupInfo,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
this.setState({ this.setState({
recoverInfo, recoverInfo,
@ -223,7 +254,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} }
} }
async _loadBackupStatus() { private async loadBackupStatus(): Promise<void> {
this.setState({ this.setState({
loading: true, loading: true,
loadError: null, loadError: null,
@ -238,7 +269,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
backupKeyStored, backupKeyStored,
}); });
const gotCache = await this._restoreWithCachedKey(backupInfo); const gotCache = await this.restoreWithCachedKey(backupInfo);
if (gotCache) { if (gotCache) {
logger.log("RestoreKeyBackupDialog: found cached backup key"); logger.log("RestoreKeyBackupDialog: found cached backup key");
this.setState({ this.setState({
@ -249,7 +280,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// If the backup key is stored, we can proceed directly to restore. // If the backup key is stored, we can proceed directly to restore.
if (backupKeyStored) { if (backupKeyStored) {
return this._restoreWithSecretStorage(); return this.restoreWithSecretStorage();
} }
this.setState({ this.setState({
@ -265,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 BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
@ -281,12 +315,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
if (this.state.loading) { if (this.state.loading) {
title = _t("Restoring keys from backup"); title = _t("Restoring keys from backup");
let details; let details;
if (this.state.progress.stage === "fetch") { if (this.state.progress.stage === ProgressState.Fetch) {
details = _t("Fetching keys from server..."); 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; const { total, successes, failures } = this.state.progress;
details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures }); 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..."); details = _t("Fetching keys from server...");
} }
content = <div> content = <div>
@ -298,7 +332,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content = _t("Unable to load backup status"); content = _t("Unable to load backup status");
} else if (this.state.restoreError) { } else if (this.state.restoreError) {
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) { 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"); title = _t("Security Key mismatch");
content = <div> content = <div>
<p>{ _t( <p>{ _t(
@ -323,7 +357,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Error"); title = _t("Error");
content = _t("No backup found!"); content = _t("No backup found!");
} else if (this.state.recoverInfo) { } else if (this.state.recoverInfo) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
title = _t("Keys restored"); title = _t("Keys restored");
let failedToDecrypt; let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) { if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
@ -336,14 +369,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<p>{ _t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported }) }</p> <p>{ _t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported }) }</p>
{ failedToDecrypt } { failedToDecrypt }
<DialogButtons primaryButton={_t('OK')} <DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone} onPrimaryButtonClick={this.onDone}
hasCancel={false} hasCancel={false}
focus={true} focus={true}
/> />
</div>; </div>;
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) { } else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter Security Phrase"); title = _t("Enter Security Phrase");
content = <div> content = <div>
<p>{ _t( <p>{ _t(
@ -359,16 +390,16 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<form className="mx_RestoreKeyBackupDialog_primaryContainer"> <form className="mx_RestoreKeyBackupDialog_primaryContainer">
<input type="password" <input type="password"
className="mx_RestoreKeyBackupDialog_passPhraseInput" className="mx_RestoreKeyBackupDialog_passPhraseInput"
onChange={this._onPassPhraseChange} onChange={this.onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}
autoFocus={true} autoFocus={true}
/> />
<DialogButtons <DialogButtons
primaryButton={_t('Next')} primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext} onPrimaryButtonClick={this.onPassPhraseNext}
primaryIsSubmit={true} primaryIsSubmit={true}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
/> />
</form> </form>
@ -381,14 +412,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
button1: s => <AccessibleButton button1: s => <AccessibleButton
className="mx_linkButton" className="mx_linkButton"
element="span" element="span"
onClick={this._onUseRecoveryKeyClick} onClick={this.onUseRecoveryKeyClick}
> >
{ s } { s }
</AccessibleButton>, </AccessibleButton>,
button2: s => <AccessibleButton button2: s => <AccessibleButton
className="mx_linkButton" className="mx_linkButton"
element="span" element="span"
onClick={this._onResetRecoveryClick} onClick={this.onResetRecoveryClick}
> >
{ s } { s }
</AccessibleButton>, </AccessibleButton>,
@ -396,8 +427,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
</div>; </div>;
} else { } else {
title = _t("Enter Security Key"); title = _t("Enter Security Key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus; let keyStatus;
if (this.state.recoveryKey.length === 0) { if (this.state.recoveryKey.length === 0) {
@ -425,15 +454,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<div className="mx_RestoreKeyBackupDialog_primaryContainer"> <div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput" <input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange} onChange={this.onRecoveryKeyChange}
value={this.state.recoveryKey} value={this.state.recoveryKey}
autoFocus={true} autoFocus={true}
/> />
{ keyStatus } { keyStatus }
<DialogButtons primaryButton={_t('Next')} <DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext} onPrimaryButtonClick={this.onRecoveryKeyNext}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={!this.state.recoveryKeyValid} primaryDisabled={!this.state.recoveryKeyValid}
/> />
@ -445,7 +474,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
{ {
button: s => <AccessibleButton className="mx_linkButton" button: s => <AccessibleButton className="mx_linkButton"
element="span" element="span"
onClick={this._onResetRecoveryClick} onClick={this.onResetRecoveryClick}
> >
{ s } { s }
</AccessibleButton>, </AccessibleButton>,

View file

@ -20,6 +20,7 @@ import BaseDialog from '../BaseDialog';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore'; import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { IDialogProps } from "../IDialogProps";
function iconFromPhase(phase: Phase) { function iconFromPhase(phase: Phase) {
if (phase === Phase.Done) { if (phase === Phase.Done) {
@ -29,12 +30,9 @@ function iconFromPhase(phase: Phase) {
} }
} }
interface IProps { interface IProps extends IDialogProps {}
onFinished: (success: boolean) => void;
}
interface IState { interface IState {
icon: Phase; icon: string;
} }
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") @replaceableComponent("views.dialogs.security.SetupEncryptionDialog")