diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 391bcb0dd6..aa47d3063f 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -30,6 +30,8 @@ const HEARTBEAT_INTERVAL = 5_000; // ms const SESSION_UPDATE_INTERVAL = 60; // seconds const MAX_PENDING_EVENTS = 1000; +export type Rating = 1 | 2 | 3 | 4 | 5; + enum Orientation { Landscape = "landscape", Portrait = "portrait", @@ -451,7 +453,7 @@ export default class CountlyAnalytics { window.removeEventListener("scroll", this.onUserActivity); } - public reportFeedback(rating: 1 | 2 | 3 | 4 | 5, comment: string) { + public reportFeedback(rating: Rating, comment: string) { this.track("[CLY]_star_rating", { rating, comment }, null, {}, true); } diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.tsx similarity index 61% rename from src/components/views/dialogs/BaseDialog.js rename to src/components/views/dialogs/BaseDialog.tsx index 42b21ec743..0af494f53e 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -18,15 +18,54 @@ limitations under the License. import React from 'react'; import FocusLock from 'react-focus-lock'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Key } from '../../../Keyboard'; -import AccessibleButton from '../elements/AccessibleButton'; +import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { + // Whether the dialog should have a 'close' button that will + // cause the dialog to be cancelled. This should only be set + // to false if there is nothing the app can sensibly do if the + // dialog is cancelled, eg. "We can't restore your session and + // the app cannot work". Default: true. + hasCancel?: boolean; + + // called when a key is pressed + onKeyDown?: (e: KeyboardEvent | React.KeyboardEvent) => void; + + // CSS class to apply to dialog div + className?: string; + + // if true, dialog container is 60% of the viewport width. Otherwise, + // the container will have no fixed size, allowing its contents to + // determine its size. Default: true. + fixedWidth?: boolean; + + // Title for the dialog. + title?: JSX.Element | string; + + // Path to an icon to put in the header + headerImage?: string; + + // children should be the content of the dialog + children?: React.ReactNode; + + // Id of content element + // If provided, this is used to add a aria-describedby attribute + contentId?: string; + + // optional additional class for the title element (basically anything that can be passed to classnames) + titleClass?: string | string[]; + + headerButton?: JSX.Element; +} /* * Basic container for modal dialogs. @@ -35,54 +74,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; * dialog on escape. */ @replaceableComponent("views.dialogs.BaseDialog") -export default class BaseDialog extends React.Component { - static propTypes = { - // onFinished callback to call when Escape is pressed - // Take a boolean which is true if the dialog was dismissed - // with a positive / confirm action or false if it was - // cancelled (BaseDialog itself only calls this with false). - onFinished: PropTypes.func.isRequired, +export default class BaseDialog extends React.Component { + private matrixClient: MatrixClient; - // Whether the dialog should have a 'close' button that will - // cause the dialog to be cancelled. This should only be set - // to false if there is nothing the app can sensibly do if the - // dialog is cancelled, eg. "We can't restore your session and - // the app cannot work". Default: true. - hasCancel: PropTypes.bool, - - // called when a key is pressed - onKeyDown: PropTypes.func, - - // CSS class to apply to dialog div - className: PropTypes.string, - - // if true, dialog container is 60% of the viewport width. Otherwise, - // the container will have no fixed size, allowing its contents to - // determine its size. Default: true. - fixedWidth: PropTypes.bool, - - // Title for the dialog. - title: PropTypes.node.isRequired, - - // Path to an icon to put in the header - headerImage: PropTypes.string, - - // children should be the content of the dialog - children: PropTypes.node, - - // Id of content element - // If provided, this is used to add a aria-describedby attribute - contentId: PropTypes.string, - - // optional additional class for the title element (basically anything that can be passed to classnames) - titleClass: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - PropTypes.arrayOf(PropTypes.string), - ]), - }; - - static defaultProps = { + public static defaultProps = { hasCancel: true, fixedWidth: true, }; @@ -90,10 +85,10 @@ export default class BaseDialog extends React.Component { constructor(props) { super(props); - this._matrixClient = MatrixClientPeg.get(); + this.matrixClient = MatrixClientPeg.get(); } - _onKeyDown = (e) => { + private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => { if (this.props.onKeyDown) { this.props.onKeyDown(e); } @@ -104,15 +99,15 @@ export default class BaseDialog extends React.Component { } }; - _onCancelClick = (e) => { + private onCancelClick = (e: ButtonEvent): void => { this.props.onFinished(false); }; - render() { + public render(): JSX.Element { let cancelButton; if (this.props.hasCancel) { cancelButton = ( - + ); } @@ -122,11 +117,11 @@ export default class BaseDialog extends React.Component { } return ( - + { - const [rating, setRating] = useState(""); - const [comment, setComment] = useState(""); +interface IProps extends IDialogProps {} - const onDebugLogsLinkClick = () => { +const FeedbackDialog: React.FC = (props: IProps) => { + const [rating, setRating] = useState(); + const [comment, setComment] = useState(""); + + const onDebugLogsLinkClick = (): void => { props.onFinished(); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; const hasFeedback = CountlyAnalytics.instance.canEnable(); - const onFinished = (sendFeedback) => { + const onFinished = (sendFeedback: boolean): void => { if (hasFeedback && sendFeedback) { - CountlyAnalytics.instance.reportFeedback(parseInt(rating, 10), comment); + CountlyAnalytics.instance.reportFeedback(rating, comment); Modal.createTrackedDialog('Feedback sent', '', InfoDialog, { title: _t('Feedback sent'), description: _t('Thank you!'), @@ -65,8 +68,8 @@ export default (props) => { setRating(parseInt(r, 10) as Rating)} definitions={[ { value: "1", label: "😠" }, { value: "2", label: "😞" }, @@ -138,7 +141,9 @@ export default (props) => { { countlyFeedbackSection } } button={hasFeedback ? _t("Send feedback") : _t("Go back")} - buttonDisabled={hasFeedback && rating === ""} + buttonDisabled={hasFeedback && !rating} onFinished={onFinished} />); }; + +export default FeedbackDialog; diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.tsx similarity index 66% rename from src/components/views/dialogs/IncomingSasDialog.js rename to src/components/views/dialogs/IncomingSasDialog.tsx index a5863518bc..da766f495c 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.tsx @@ -15,12 +15,20 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import VerificationComplete from "../verification/VerificationComplete"; +import VerificationCancelled from "../verification/VerificationCancelled"; +import BaseAvatar from "../avatars/BaseAvatar"; +import Spinner from "../elements/Spinner"; +import VerificationShowSas from "../verification/VerificationShowSas"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import { IDialogProps } from "./IDialogProps"; +import { IGeneratedSas, ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; +import { VerificationBase } from "matrix-js-sdk/src/crypto/verification/Base"; import { logger } from "matrix-js-sdk/src/logger"; @@ -30,41 +38,56 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2; const PHASE_VERIFIED = 3; const PHASE_CANCELLED = 4; -@replaceableComponent("views.dialogs.IncomingSasDialog") -export default class IncomingSasDialog extends React.Component { - static propTypes = { - verifier: PropTypes.object.isRequired, - }; +interface IProps extends IDialogProps { + verifier: VerificationBase; // TODO types +} - constructor(props) { +interface IState { + phase: number; + sasVerified: boolean; + opponentProfile: { + // eslint-disable-next-line camelcase + avatar_url?: string; + displayname?: string; + }; + opponentProfileError: Error; + sas: IGeneratedSas; +} + +@replaceableComponent("views.dialogs.IncomingSasDialog") +export default class IncomingSasDialog extends React.Component { + private showSasEvent: ISasEvent; + + constructor(props: IProps) { super(props); let phase = PHASE_START; - if (this.props.verifier.cancelled) { + if (this.props.verifier.hasBeenCancelled) { logger.log("Verifier was cancelled in the background."); phase = PHASE_CANCELLED; } - this._showSasEvent = null; + this.showSasEvent = null; this.state = { phase: phase, sasVerified: false, opponentProfile: null, opponentProfileError: null, + sas: null, }; - this.props.verifier.on('show_sas', this._onVerifierShowSas); - this.props.verifier.on('cancel', this._onVerifierCancel); - this._fetchOpponentProfile(); + this.props.verifier.on('show_sas', this.onVerifierShowSas); + this.props.verifier.on('cancel', this.onVerifierCancel); + this.fetchOpponentProfile(); } - componentWillUnmount() { + public componentWillUnmount(): void { if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) { - this.props.verifier.cancel('User cancel'); + this.props.verifier.cancel(new Error('User cancel')); } - this.props.verifier.removeListener('show_sas', this._onVerifierShowSas); + this.props.verifier.removeListener('show_sas', this.onVerifierShowSas); } - async _fetchOpponentProfile() { + private async fetchOpponentProfile(): Promise { try { const prof = await MatrixClientPeg.get().getProfileInfo( 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); - } + }; - _onCancelClick = () => { + private onCancelClick = (): void => { this.props.onFinished(this.state.phase === PHASE_VERIFIED); - } + }; - _onContinueClick = () => { + private onContinueClick = (): void => { this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM }); this.props.verifier.verify().then(() => { this.setState({ phase: PHASE_VERIFIED }); }).catch((e) => { logger.log("Verification failed", e); }); - } + }; - _onVerifierShowSas = (e) => { - this._showSasEvent = e; + private onVerifierShowSas = (e: ISasEvent): void => { + this.showSasEvent = e; this.setState({ phase: PHASE_SHOW_SAS, sas: e.sas, }); - } + }; - _onVerifierCancel = (e) => { + private onVerifierCancel = (): void => { this.setState({ phase: PHASE_CANCELLED, }); - } + }; - _onSasMatchesClick = () => { - this._showSasEvent.confirm(); + private onSasMatchesClick = (): void => { + this.showSasEvent.confirm(); this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM, }); - } + }; - _onVerifiedDoneClick = () => { + private onVerifiedDoneClick = (): void => { this.props.onFinished(true); - } - - _renderPhaseStart() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Spinner = sdk.getComponent("views.elements.Spinner"); - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + }; + private renderPhaseStart(): JSX.Element { const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId(); let profile; @@ -192,27 +211,24 @@ export default class IncomingSasDialog extends React.Component { ); } - _renderPhaseShowSas() { - const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); + private renderPhaseShowSas(): JSX.Element { return ; } - _renderPhaseWaitForPartnerToConfirm() { - const Spinner = sdk.getComponent("views.elements.Spinner"); - + private renderPhaseWaitForPartnerToConfirm(): JSX.Element { return (
@@ -221,41 +237,38 @@ export default class IncomingSasDialog extends React.Component { ); } - _renderPhaseVerified() { - const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); - return ; + private renderPhaseVerified(): JSX.Element { + return ; } - _renderPhaseCancelled() { - const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); - return ; + private renderPhaseCancelled(): JSX.Element { + return ; } - render() { + public render(): JSX.Element { let body; switch (this.state.phase) { case PHASE_START: - body = this._renderPhaseStart(); + body = this.renderPhaseStart(); break; case PHASE_SHOW_SAS: - body = this._renderPhaseShowSas(); + body = this.renderPhaseShowSas(); break; case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM: - body = this._renderPhaseWaitForPartnerToConfirm(); + body = this.renderPhaseWaitForPartnerToConfirm(); break; case PHASE_VERIFIED: - body = this._renderPhaseVerified(); + body = this.renderPhaseVerified(); break; case PHASE_CANCELLED: - body = this._renderPhaseCancelled(); + body = this.renderPhaseCancelled(); break; } - const BaseDialog = sdk.getComponent("dialogs.BaseDialog"); return ( { body } diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.tsx similarity index 76% rename from src/components/views/dialogs/IntegrationsDisabledDialog.js rename to src/components/views/dialogs/IntegrationsDisabledDialog.tsx index 6a5b2f08f9..7da4bb84b9 100644 --- a/src/components/views/dialogs/IntegrationsDisabledDialog.js +++ b/src/components/views/dialogs/IntegrationsDisabledDialog.tsx @@ -15,32 +15,28 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from "../../../languageHandler"; -import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps {} @replaceableComponent("views.dialogs.IntegrationsDisabledDialog") -export default class IntegrationsDisabledDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - - _onAcknowledgeClick = () => { +export default class IntegrationsDisabledDialog extends React.Component { + private onAcknowledgeClick = (): void => { this.props.onFinished(); }; - _onOpenSettingsClick = () => { + private onOpenSettingsClick = (): void => { this.props.onFinished(); dis.fire(Action.ViewUserSettings); }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - + public render(): JSX.Element { return ( ); diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx similarity index 88% rename from src/components/views/dialogs/IntegrationsImpossibleDialog.js rename to src/components/views/dialogs/IntegrationsImpossibleDialog.tsx index 6cfb96a1b4..52e3a2fbb8 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx @@ -15,23 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; import * as sdk from "../../../index"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps {} @replaceableComponent("views.dialogs.IntegrationsImpossibleDialog") -export default class IntegrationsImpossibleDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - - _onAcknowledgeClick = () => { +export default class IntegrationsImpossibleDialog extends React.Component { + private onAcknowledgeClick = (): void => { this.props.onFinished(); }; - render() { + public render(): JSX.Element { const brand = SdkConfig.get().brand; const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -54,7 +52,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.tsx similarity index 63% rename from src/components/views/dialogs/InteractiveAuthDialog.js rename to src/components/views/dialogs/InteractiveAuthDialog.tsx index e5f4887f06..2ea97f91c3 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -17,69 +17,88 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; -import { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth"; +import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth"; import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import BaseDialog from "./BaseDialog"; +import { IAuthData } from "matrix-js-sdk/src/interactive-auth"; +import { IDialogProps } from "./IDialogProps"; + +interface IDialogAesthetics { + [x: string]: { + [x: number]: { + title: string; + body: string; + continueText: string; + continueKind: string; + }; + }; +} + +interface IProps extends IDialogProps { + // matrix client to use for UI auth requests + matrixClient: MatrixClient; + + // response from initial request. If not supplied, will do a request on + // mount. + authData?: IAuthData; + + // callback + makeRequest: (auth: IAuthData) => Promise; + + // Optional title and body to show when not showing a particular stage + title?: string; + body?: string; + + // Optional title and body pairs for particular stages and phases within + // those stages. Object structure/example is: + // { + // "org.example.stage_type": { + // 1: { + // "body": "This is a body for phase 1" of org.example.stage_type, + // "title": "Title for phase 1 of org.example.stage_type" + // }, + // 2: { + // "body": "This is a body for phase 2 of org.example.stage_type", + // "title": "Title for phase 2 of org.example.stage_type" + // "continueText": "Confirm identity with Example Auth", + // "continueKind": "danger" + // } + // } + // } + // + // Default is defined in _getDefaultDialogAesthetics() + aestheticsForStagePhases?: IDialogAesthetics; +} + +interface IState { + authError: Error; + + // See _onUpdateStagePhase() + uiaStage: number | string; + uiaStagePhase: number | string; +} @replaceableComponent("views.dialogs.InteractiveAuthDialog") -export default class InteractiveAuthDialog extends React.Component { - static propTypes = { - // matrix client to use for UI auth requests - matrixClient: PropTypes.object.isRequired, +export default class InteractiveAuthDialog extends React.Component { + constructor(props: IProps) { + super(props); - // response from initial request. If not supplied, will do a request on - // mount. - authData: PropTypes.shape({ - flows: PropTypes.array, - params: PropTypes.object, - session: PropTypes.string, - }), + this.state = { + authError: null, - // callback - makeRequest: PropTypes.func.isRequired, + // See _onUpdateStagePhase() + uiaStage: null, + uiaStagePhase: null, + }; + } - onFinished: PropTypes.func.isRequired, - - // Optional title and body to show when not showing a particular stage - title: PropTypes.string, - body: PropTypes.string, - - // Optional title and body pairs for particular stages and phases within - // those stages. Object structure/example is: - // { - // "org.example.stage_type": { - // 1: { - // "body": "This is a body for phase 1" of org.example.stage_type, - // "title": "Title for phase 1 of org.example.stage_type" - // }, - // 2: { - // "body": "This is a body for phase 2 of org.example.stage_type", - // "title": "Title for phase 2 of org.example.stage_type" - // "continueText": "Confirm identity with Example Auth", - // "continueKind": "danger" - // } - // } - // } - // - // Default is defined in _getDefaultDialogAesthetics() - aestheticsForStagePhases: PropTypes.object, - }; - - state = { - authError: null, - - // See _onUpdateStagePhase() - uiaStage: null, - uiaStagePhase: null, - }; - - _getDefaultDialogAesthetics() { + private getDefaultDialogAesthetics(): IDialogAesthetics { const ssoAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), @@ -101,7 +120,7 @@ export default class InteractiveAuthDialog extends React.Component { }; } - _onAuthFinished = (success, result) => { + private onAuthFinished = (success: boolean, result: Error): void => { if (success) { this.props.onFinished(true, result); } else { @@ -115,19 +134,16 @@ export default class InteractiveAuthDialog extends React.Component { } }; - _onUpdateStagePhase = (newStage, newPhase) => { + private onUpdateStagePhase = (newStage: string | number, newPhase: string | number): void => { // We copy the stage and stage phase params into state for title selection in render() this.setState({ uiaStage: newStage, uiaStagePhase: newPhase }); }; - _onDismissClick = () => { + private onDismissClick = (): void => { this.props.onFinished(false); }; - render() { - const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - + public render(): JSX.Element { // Let's pick a title, body, and other params text that we'll show to the user. The order // is most specific first, so stagePhase > our props > defaults. @@ -135,7 +151,7 @@ export default class InteractiveAuthDialog extends React.Component { let body = this.state.authError ? null : this.props.body; let continueText = null; let continueKind = null; - const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics(); + const dialogAesthetics = this.props.aestheticsForStagePhases || this.getDefaultDialogAesthetics(); if (!this.state.authError && dialogAesthetics) { if (dialogAesthetics[this.state.uiaStage]) { const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase]; @@ -152,9 +168,9 @@ export default class InteractiveAuthDialog extends React.Component {
{ this.state.authError.message || this.state.authError.toString() }

- { _t("Dismiss") } @@ -165,12 +181,11 @@ export default class InteractiveAuthDialog extends React.Component {
{ body } diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js b/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx similarity index 87% rename from src/components/views/dialogs/KeySignatureUploadFailedDialog.js rename to src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx index 6b36c19977..28e4d1ca80 100644 --- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js +++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx @@ -15,20 +15,29 @@ limitations under the License. */ import React, { useState, useCallback, useRef } from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import Spinner from "../elements/Spinner"; +import { IDialogProps } from "./IDialogProps"; -export default function KeySignatureUploadFailedDialog({ +interface IProps extends IDialogProps { + failures: Record>; + source: string; + continuation: () => void; +} + +const KeySignatureUploadFailedDialog: React.FC = ({ failures, source, continuation, onFinished, -}) { +}) => { const RETRIES = 2; - const BaseDialog = sdk.getComponent('dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Spinner = sdk.getComponent('elements.Spinner'); const [retry, setRetry] = useState(RETRIES); const [cancelled, setCancelled] = useState(false); const [retrying, setRetrying] = useState(false); @@ -107,4 +116,6 @@ export default function KeySignatureUploadFailedDialog({ { body } ); -} +}; + +export default KeySignatureUploadFailedDialog; diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.js b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx similarity index 89% rename from src/components/views/dialogs/LazyLoadingDisabledDialog.js rename to src/components/views/dialogs/LazyLoadingDisabledDialog.tsx index e43cb28a22..ec30123436 100644 --- a/src/components/views/dialogs/LazyLoadingDisabledDialog.js +++ b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx @@ -19,8 +19,13 @@ import React from 'react'; import QuestionDialog from './QuestionDialog'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; +import { IDialogProps } from "./IDialogProps"; -export default (props) => { +interface IProps extends IDialogProps { + host: string; +} + +const LazyLoadingDisabledDialog: React.FC = (props) => { const brand = SdkConfig.get().brand; const description1 = _t( "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " + @@ -49,3 +54,5 @@ export default (props) => { onFinished={props.onFinished} />); }; + +export default LazyLoadingDisabledDialog; diff --git a/src/components/views/dialogs/LazyLoadingResyncDialog.js b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx similarity index 87% rename from src/components/views/dialogs/LazyLoadingResyncDialog.js rename to src/components/views/dialogs/LazyLoadingResyncDialog.tsx index a5db15ebbe..e6a505511c 100644 --- a/src/components/views/dialogs/LazyLoadingResyncDialog.js +++ b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx @@ -19,8 +19,11 @@ import React from 'react'; import QuestionDialog from './QuestionDialog'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; +import { IDialogProps } from "./IDialogProps"; -export default (props) => { +interface IProps extends IDialogProps {} + +const LazyLoadingResyncDialog: React.FC = (props) => { const brand = SdkConfig.get().brand; const description = _t( @@ -38,3 +41,5 @@ export default (props) => { onFinished={props.onFinished} />); }; + +export default LazyLoadingResyncDialog; diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx similarity index 84% rename from src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js rename to src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx index 4387108fac..88419d26b8 100644 --- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js +++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx @@ -19,37 +19,31 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import QuestionDialog from "./QuestionDialog"; +import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { + userId: string; + device: DeviceInfo; +} @replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog") -export default class ManualDeviceKeyVerificationDialog extends React.Component { - static propTypes = { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - }; - - _onCancelClick = () => { - this.props.onFinished(false); - } - - _onLegacyFinished = (confirm) => { +export default class ManualDeviceKeyVerificationDialog extends React.Component { + private onLegacyFinished = (confirm: boolean): void => { if (confirm) { MatrixClientPeg.get().setDeviceVerified( this.props.userId, this.props.device.deviceId, true, ); } this.props.onFinished(confirm); - } - - render() { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + }; + public render(): JSX.Element { let text; if (MatrixClientPeg.get().getUserId() === this.props.userId) { text = _t("Confirm by comparing the following with the User Settings in your other session:"); @@ -81,7 +75,7 @@ export default class ManualDeviceKeyVerificationDialog extends React.Component { title={_t("Verify session")} description={body} button={_t("Verify session")} - onFinished={this._onLegacyFinished} + onFinished={this.onLegacyFinished} /> ); } diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.tsx similarity index 81% rename from src/components/views/dialogs/MessageEditHistoryDialog.js rename to src/components/views/dialogs/MessageEditHistoryDialog.tsx index 6fce8aecd4..7753eba199 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx @@ -15,21 +15,39 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; -import * as sdk from "../../../index"; import { wantsDateSeparator } from '../../../DateUtils'; import SettingsStore from '../../../settings/SettingsStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import BaseDialog from "./BaseDialog"; +import ScrollPanel from "../../structures/ScrollPanel"; +import Spinner from "../elements/Spinner"; +import EditHistoryMessage from "../messages/EditHistoryMessage"; +import DateSeparator from "../messages/DateSeparator"; +import { IDialogProps } from "./IDialogProps"; +import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; +import { defer } from "matrix-js-sdk/src/utils"; + +interface IProps extends IDialogProps { + mxEvent: MatrixEvent; +} + +interface IState { + originalEvent: MatrixEvent; + error: { + errcode: string; + }; + events: MatrixEvent[]; + nextBatch: string; + isLoading: boolean; + isTwelveHour: boolean; +} @replaceableComponent("views.dialogs.MessageEditHistoryDialog") -export default class MessageEditHistoryDialog extends React.PureComponent { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - }; - - constructor(props) { +export default class MessageEditHistoryDialog extends React.PureComponent { + constructor(props: IProps) { super(props); this.state = { originalEvent: null, @@ -41,7 +59,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { }; } - loadMoreEdits = async (backwards) => { + private loadMoreEdits = async (backwards?: boolean): Promise => { if (backwards || (!this.state.nextBatch && !this.state.isLoading)) { // bail out on backwards as we only paginate in one direction return false; @@ -50,13 +68,13 @@ export default class MessageEditHistoryDialog extends React.PureComponent { const roomId = this.props.mxEvent.getRoomId(); const eventId = this.props.mxEvent.getId(); const client = MatrixClientPeg.get(); + + const { resolve, reject, promise } = defer(); let result; - let resolve; - let reject; - const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;}); + try { result = await client.relations( - roomId, eventId, "m.replace", "m.room.message", opts); + roomId, eventId, RelationType.Replace, EventType.RoomMessage, opts); } catch (error) { // log if the server returned an error if (error.errcode) { @@ -67,7 +85,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { } const newEvents = result.events; - this._locallyRedactEventsIfNeeded(newEvents); + this.locallyRedactEventsIfNeeded(newEvents); this.setState({ originalEvent: this.state.originalEvent || result.originalEvent, events: this.state.events.concat(newEvents), @@ -78,9 +96,9 @@ export default class MessageEditHistoryDialog extends React.PureComponent { resolve(hasMoreResults); }); return promise; - } + }; - _locallyRedactEventsIfNeeded(newEvents) { + private locallyRedactEventsIfNeeded(newEvents: MatrixEvent[]): void { const roomId = this.props.mxEvent.getRoomId(); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); @@ -95,13 +113,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent { } } - componentDidMount() { + public componentDidMount(): void { this.loadMoreEdits(); } - _renderEdits() { - const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage'); - const DateSeparator = sdk.getComponent('messages.DateSeparator'); + private renderEdits(): JSX.Element[] { const nodes = []; let lastEvent; let allEvents = this.state.events; @@ -128,7 +144,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { return nodes; } - render() { + public render(): JSX.Element { let content; if (this.state.error) { const { error } = this.state; @@ -149,20 +165,17 @@ export default class MessageEditHistoryDialog extends React.PureComponent {

); } } else if (this.state.isLoading) { - const Spinner = sdk.getComponent("elements.Spinner"); content = ; } else { - const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); content = ( -
    { this._renderEdits() }
+
    { this.renderEdits() }
); } - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { + public static defaultProps: Partial = { title: "", description: "", extraButtons: null, @@ -48,17 +49,19 @@ export default class QuestionDialog extends React.Component { quitOnly: false, }; - onOk = () => { + private onOk = (): void => { this.props.onFinished(true); }; - onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); }; - render() { + public render(): JSX.Element { + // Converting these to imports breaks wrench tests const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + let primaryButtonClass = ""; if (this.props.danger) { primaryButtonClass = "danger"; diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx similarity index 80% rename from src/components/views/dialogs/SessionRestoreErrorDialog.js rename to src/components/views/dialogs/SessionRestoreErrorDialog.tsx index eeeadbbfe5..b36dbf548e 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx @@ -17,27 +17,27 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import QuestionDialog from "./QuestionDialog"; +import BugReportDialog from "./BugReportDialog"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { + error: string; +} @replaceableComponent("views.dialogs.SessionRestoreErrorDialog") -export default class SessionRestoreErrorDialog extends React.Component { - static propTypes = { - error: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, - }; - - _sendBugReport = () => { - const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); +export default class SessionRestoreErrorDialog extends React.Component { + private sendBugReport = (): void => { Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {}); }; - _onClearStorageClick = () => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + private onClearStorageClick = (): void => { Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, { title: _t("Sign out"), description: @@ -48,19 +48,17 @@ export default class SessionRestoreErrorDialog extends React.Component { }); }; - _onRefreshClick = () => { + private onRefreshClick = (): void => { // Is this likely to help? Probably not, but giving only one button // that clears your storage seems awful. - window.location.reload(true); + window.location.reload(); }; - render() { + public render(): JSX.Element { const brand = SdkConfig.get().brand; - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const clearStorageButton = ( - ); @@ -68,7 +66,7 @@ export default class SessionRestoreErrorDialog extends React.Component { let dialogButtons; if (SdkConfig.get().bug_report_endpoint_url) { dialogButtons = @@ -76,7 +74,7 @@ export default class SessionRestoreErrorDialog extends React.Component { ; } else { dialogButtons = diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.tsx similarity index 81% rename from src/components/views/dialogs/SetEmailDialog.js rename to src/components/views/dialogs/SetEmailDialog.tsx index 3dad3821fb..a8b8207f7d 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.tsx @@ -16,13 +16,26 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import * as Email from '../../../email'; import AddThreepid from '../../../AddThreepid'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Spinner from "../elements/Spinner"; +import ErrorDialog from "./ErrorDialog"; +import QuestionDialog from "./QuestionDialog"; +import BaseDialog from "./BaseDialog"; +import EditableText from "../elements/EditableText"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { + title: string; +} + +interface IState { + emailAddress: string; + emailBusy: boolean; +} /* * Prompt the user to set an email address. @@ -30,26 +43,25 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; * On success, `onFinished(true)` is called. */ @replaceableComponent("views.dialogs.SetEmailDialog") -export default class SetEmailDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +export default class SetEmailDialog extends React.Component { + private addThreepid: AddThreepid; - state = { - emailAddress: '', - emailBusy: false, - }; + constructor(props: IProps) { + super(props); - onEmailAddressChanged = value => { + this.state = { + emailAddress: '', + emailBusy: false, + }; + } + + private onEmailAddressChanged = (value: string): void => { this.setState({ emailAddress: value, }); }; - onSubmit = () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - + private onSubmit = (): void => { const emailAddress = this.state.emailAddress; if (!Email.looksValid(emailAddress)) { Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, { @@ -58,8 +70,8 @@ export default class SetEmailDialog extends React.Component { }); return; } - this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).then(() => { + this.addThreepid = new AddThreepid(); + this.addThreepid.addEmailAddress(emailAddress).then(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -80,11 +92,11 @@ export default class SetEmailDialog extends React.Component { this.setState({ emailBusy: true }); }; - onCancelled = () => { + private onCancelled = (): void => { this.props.onFinished(false); }; - onEmailDialogFinished = ok => { + private onEmailDialogFinished = (ok: boolean): void => { if (ok) { this.verifyEmailAddress(); } else { @@ -92,13 +104,12 @@ export default class SetEmailDialog extends React.Component { } }; - verifyEmailAddress() { - this._addThreepid.checkEmailLinkClicked().then(() => { + private verifyEmailAddress(): void { + this.addThreepid.checkEmailLinkClicked().then(() => { this.props.onFinished(true); }, (err) => { this.setState({ emailBusy: false }); if (err.errcode == 'M_THREEPID_AUTH_FAILED') { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const message = _t("Unable to verify email address.") + " " + _t("Please check your email and click on the link it contains. Once this is done, click continue."); Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, { @@ -108,7 +119,6 @@ export default class SetEmailDialog extends React.Component { onFinished: this.onEmailDialogFinished, }); } else { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to verify email address: " + err); Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { title: _t("Unable to verify email address."), @@ -118,15 +128,10 @@ export default class SetEmailDialog extends React.Component { }); } - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const Spinner = sdk.getComponent('elements.Spinner'); - const EditableText = sdk.getComponent('elements.EditableText'); - + public render(): JSX.Element { const emailInput = this.state.emailBusy ? : { - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); +interface IProps extends IDialogProps {} +const SlashCommandHelpDialog: React.FC = ({ onFinished }) => { const categories = {}; Commands.forEach(cmd => { if (!cmd.isEnabled()) return; @@ -62,3 +63,5 @@ export default ({ onFinished }) => { hasCloseButton={true} onFinished={onFinished} />; }; + +export default SlashCommandHelpDialog; diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.tsx similarity index 79% rename from src/components/views/dialogs/StorageEvictedDialog.js rename to src/components/views/dialogs/StorageEvictedDialog.tsx index 507ee09e75..bdbbf815e6 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.js +++ b/src/components/views/dialogs/StorageEvictedDialog.tsx @@ -15,40 +15,36 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import BugReportDialog from "./BugReportDialog"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { } @replaceableComponent("views.dialogs.StorageEvictedDialog") -export default class StorageEvictedDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - - _sendBugReport = ev => { +export default class StorageEvictedDialog extends React.Component { + private sendBugReport = (ev: React.MouseEvent): void => { ev.preventDefault(); - const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); Modal.createTrackedDialog('Storage evicted', 'Send Bug Report Dialog', BugReportDialog, {}); }; - _onSignOutClick = () => { + private onSignOutClick = (): void => { this.props.onFinished(true); }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - + public render(): JSX.Element { let logRequest; if (SdkConfig.get().bug_report_endpoint_url) { logRequest = _t( "To help us prevent this in future, please send us logs.", {}, { - a: text => { text }, + a: text => { text }, }, ); } @@ -73,7 +69,7 @@ export default class StorageEvictedDialog extends React.Component { ) } { logRequest }

diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx similarity index 78% rename from src/components/views/dialogs/TabbedIntegrationManagerDialog.js rename to src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx index 8723d4a453..0f87b5c18d 100644 --- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js +++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx @@ -15,42 +15,47 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { Room } from "matrix-js-sdk/src/models/room"; -import * as sdk from '../../../index'; import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms"; import classNames from 'classnames'; import * as ScalarMessaging from "../../../ScalarMessaging"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance"; +import ScalarAuthClient from "../../../ScalarAuthClient"; +import AccessibleButton from "../elements/AccessibleButton"; +import IntegrationManager from "../settings/IntegrationManager"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { + /** + * Optional room where the integration manager should be open to + */ + room?: Room; + + /** + * Optional screen to open on the integration manager + */ + screen?: string; + + /** + * Optional integration ID to open in the integration manager + */ + integrationId?: string; +} + +interface IState { + managers: IntegrationManagerInstance[]; + busy: boolean; + currentIndex: number; + currentConnected: boolean; + currentLoading: boolean; + currentScalarClient: ScalarAuthClient; +} @replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog") -export default class TabbedIntegrationManagerDialog extends React.Component { - static propTypes = { - /** - * Called with: - * * success {bool} True if the user accepted any douments, false if cancelled - * * agreedUrls {string[]} List of agreed URLs - */ - onFinished: PropTypes.func.isRequired, - - /** - * Optional room where the integration manager should be open to - */ - room: PropTypes.instanceOf(Room), - - /** - * Optional screen to open on the integration manager - */ - screen: PropTypes.string, - - /** - * Optional integration ID to open in the integration manager - */ - integrationId: PropTypes.string, - }; - - constructor(props) { +export default class TabbedIntegrationManagerDialog extends React.Component { + constructor(props: IProps) { super(props); this.state = { @@ -63,11 +68,11 @@ export default class TabbedIntegrationManagerDialog extends React.Component { }; } - componentDidMount() { + public componentDidMount(): void { this.openManager(0, true); } - openManager = async (i, force = false) => { + private openManager = async (i: number, force = false): Promise => { if (i === this.state.currentIndex && !force) return; const manager = this.state.managers[i]; @@ -120,8 +125,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { } }; - _renderTabs() { - const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + private renderTabs(): JSX.Element[] { return this.state.managers.map((m, i) => { const classes = classNames({ 'mx_TabbedIntegrationManagerDialog_tab': true, @@ -140,8 +144,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { }); } - _renderTab() { - const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); + public renderTab(): JSX.Element { let uiUrl = null; if (this.state.currentScalarClient) { uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom( @@ -151,7 +154,6 @@ export default class TabbedIntegrationManagerDialog extends React.Component { ); } return ; } - render() { + public render(): JSX.Element { return (
- { this._renderTabs() } + { this.renderTabs() }
- { this._renderTab() } + { this.renderTab() }
); diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.tsx similarity index 67% rename from src/components/views/dialogs/TextInputDialog.js rename to src/components/views/dialogs/TextInputDialog.tsx index 3d37c89424..7a5887f053 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.tsx @@ -14,33 +14,39 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; +import React, { ChangeEvent, createRef } from 'react'; import Field from "../elements/Field"; import { _t, _td } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IFieldState, IValidationResult } from "../elements/Validation"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { + title?: string; + description?: React.ReactNode; + value?: string; + placeholder?: string; + button?: string; + busyMessage?: string; // pass _td string + focus?: boolean; + hasCancel?: boolean; + validator?: (fieldState: IFieldState) => IValidationResult; // result of withValidation + fixedWidth?: boolean; +} + +interface IState { + value: string; + busy: boolean; + valid: boolean; +} @replaceableComponent("views.dialogs.TextInputDialog") -export default class TextInputDialog extends React.Component { - static propTypes = { - title: PropTypes.string, - description: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.string, - ]), - value: PropTypes.string, - placeholder: PropTypes.string, - button: PropTypes.string, - busyMessage: PropTypes.string, // pass _td string - focus: PropTypes.bool, - onFinished: PropTypes.func.isRequired, - hasCancel: PropTypes.bool, - validator: PropTypes.func, // result of withValidation - fixedWidth: PropTypes.bool, - }; +export default class TextInputDialog extends React.Component { + private field = createRef(); - static defaultProps = { + public static defaultProps = { title: "", value: "", description: "", @@ -49,11 +55,9 @@ export default class TextInputDialog extends React.Component { hasCancel: true, }; - constructor(props) { + constructor(props: IProps) { super(props); - this._field = createRef(); - this.state = { value: this.props.value, busy: false, @@ -61,23 +65,23 @@ export default class TextInputDialog extends React.Component { }; } - componentDidMount() { + public componentDidMount(): void { if (this.props.focus) { // Set the cursor at the end of the text input // this._field.current.value = this.props.value; - this._field.current.focus(); + this.field.current.focus(); } } - onOk = async ev => { + private onOk = async (ev: React.FormEvent): Promise => { ev.preventDefault(); if (this.props.validator) { this.setState({ busy: true }); - await this._field.current.validate({ allowEmpty: false }); + await this.field.current.validate({ allowEmpty: false }); - if (!this._field.current.state.valid) { - this._field.current.focus(); - this._field.current.validate({ allowEmpty: false, focused: true }); + if (!this.field.current.state.valid) { + this.field.current.focus(); + this.field.current.validate({ allowEmpty: false, focused: true }); this.setState({ busy: false }); return; } @@ -85,17 +89,17 @@ export default class TextInputDialog extends React.Component { this.props.onFinished(true, this.state.value); }; - onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); }; - onChange = ev => { + private onChange = (ev: ChangeEvent): void => { this.setState({ value: ev.target.value, }); }; - onValidate = async fieldState => { + private onValidate = async (fieldState: IFieldState): Promise => { const result = await this.props.validator(fieldState); this.setState({ valid: result.valid, @@ -103,9 +107,7 @@ export default class TextInputDialog extends React.Component { return result; }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + public render(): JSX.Element { return (
diff --git a/src/components/views/dialogs/UploadFailureDialog.js b/src/components/views/dialogs/UploadFailureDialog.tsx similarity index 80% rename from src/components/views/dialogs/UploadFailureDialog.js rename to src/components/views/dialogs/UploadFailureDialog.tsx index 224098f935..bb8d14e161 100644 --- a/src/components/views/dialogs/UploadFailureDialog.js +++ b/src/components/views/dialogs/UploadFailureDialog.tsx @@ -17,11 +17,18 @@ limitations under the License. import filesize from 'filesize'; import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import { IDialogProps } from "./IDialogProps"; + +interface IProps extends IDialogProps { + badFiles: File[]; + totalFiles: number; + contentMessages: ContentMessages; +} /* * Tells the user about files we know cannot be uploaded before we even try uploading @@ -29,26 +36,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; * the size of the file. */ @replaceableComponent("views.dialogs.UploadFailureDialog") -export default class UploadFailureDialog extends React.Component { - static propTypes = { - badFiles: PropTypes.arrayOf(PropTypes.object).isRequired, - totalFiles: PropTypes.number.isRequired, - contentMessages: PropTypes.instanceOf(ContentMessages).isRequired, - onFinished: PropTypes.func.isRequired, - } - - _onCancelClick = () => { +export default class UploadFailureDialog extends React.Component { + private onCancelClick = (): void => { this.props.onFinished(false); - } + }; - _onUploadClick = () => { + private onUploadClick = (): void => { this.props.onFinished(true); - } - - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + }; + public render(): JSX.Element { let message; let preview; let buttons; @@ -65,7 +62,7 @@ export default class UploadFailureDialog extends React.Component { ); buttons = ; } else if (this.props.totalFiles === this.props.badFiles.length) { @@ -80,7 +77,7 @@ export default class UploadFailureDialog extends React.Component { ); buttons = ; } else { @@ -96,17 +93,17 @@ export default class UploadFailureDialog extends React.Component { const howManyOthers = this.props.totalFiles - this.props.badFiles.length; buttons = ; } return ( diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx index a17fba2908..81861c7f4d 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx @@ -47,15 +47,15 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent { + private onAllow = (): void => { this.onPermissionSelection(true); }; - private onDeny = () => { + private onDeny = (): void => { this.onPermissionSelection(false); }; - private onPermissionSelection(allowed: boolean) { + private onPermissionSelection(allowed: boolean): void { if (this.state.rememberSelection) { logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`); @@ -68,11 +68,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent { + private onRememberSelectionChange = (newVal: boolean): void => { this.setState({ rememberSelection: newVal }); }; - public render() { + public render(): JSX.Element { return ( void; +} + +interface IState { + backupInfo: IKeyBackupInfo; + backupKeyStored: Record; + loading: boolean; + loadError: string; + restoreError: { + errcode: string; + }; + recoveryKey: string; + recoverInfo: IKeyBackupRestoreResult; + recoveryKeyValid: boolean; + forceRecoveryKey: boolean; + passPhrase: string; + restoreType: RestoreType; + progress: { + stage: ProgressState; + total?: number; + successes?: number; + failures?: number; + }; +} /* * Dialog for restoring e2e keys from a backup and the user's recovery key */ -export default class RestoreKeyBackupDialog extends React.PureComponent { - static propTypes = { - // if false, will close the dialog as soon as the restore completes succesfully - // default: true - showSummary: PropTypes.bool, - // If specified, gather the key from the user but then call the function with the backup - // key rather than actually (necessarily) restoring the backup. - keyCallback: PropTypes.func, - }; - +export default class RestoreKeyBackupDialog extends React.PureComponent { static defaultProps = { showSummary: true, }; @@ -60,58 +92,58 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { forceRecoveryKey: false, passPhrase: '', restoreType: null, - progress: { stage: "prefetch" }, + progress: { stage: ProgressState.PreFetch }, }; } - componentDidMount() { - this._loadBackupStatus(); + public componentDidMount(): void { + this.loadBackupStatus(); } - _onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); - } + }; - _onDone = () => { + private onDone = (): void => { this.props.onFinished(true); - } + }; - _onUseRecoveryKeyClick = () => { + private onUseRecoveryKeyClick = (): void => { this.setState({ forceRecoveryKey: true, }); - } + }; - _progressCallback = (data) => { + private progressCallback = (data): void => { this.setState({ progress: data, }); - } + }; - _onResetRecoveryClick = () => { + private onResetRecoveryClick = (): void => { this.props.onFinished(false); - accessSecretStorage(() => {}, /* forceReset = */ true); - } + accessSecretStorage(async () => {}, /* forceReset = */ true); + }; - _onRecoveryKeyChange = (e) => { + private onRecoveryKeyChange = (e): void => { this.setState({ recoveryKey: e.target.value, recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), }); - } + }; - _onPassPhraseNext = async () => { + private onPassPhraseNext = async (): Promise => { this.setState({ loading: true, restoreError: null, - restoreType: RESTORE_TYPE_PASSPHRASE, + restoreType: RestoreType.Passphrase, }); try { // We do still restore the key backup: we must ensure that the key backup key // is the right one and restoring it is currently the only way we can do this. const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword( this.state.passPhrase, undefined, undefined, this.state.backupInfo, - { progressCallback: this._progressCallback }, + { progressCallback: this.progressCallback }, ); if (this.props.keyCallback) { const key = await MatrixClientPeg.get().keyBackupKeyFromPassword( @@ -135,20 +167,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { restoreError: e, }); } - } + }; - _onRecoveryKeyNext = async () => { + private onRecoveryKeyNext = async (): Promise => { if (!this.state.recoveryKeyValid) return; this.setState({ loading: true, restoreError: null, - restoreType: RESTORE_TYPE_RECOVERYKEY, + restoreType: RestoreType.RecoveryKey, }); try { const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey( this.state.recoveryKey, undefined, undefined, this.state.backupInfo, - { progressCallback: this._progressCallback }, + { progressCallback: this.progressCallback }, ); if (this.props.keyCallback) { const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey); @@ -169,31 +201,30 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { restoreError: e, }); } - } + }; - _onPassPhraseChange = (e) => { + private onPassPhraseChange = (e): void => { this.setState({ passPhrase: e.target.value, }); - } + }; - async _restoreWithSecretStorage() { + private async restoreWithSecretStorage(): Promise { this.setState({ loading: true, restoreError: null, - restoreType: RESTORE_TYPE_SECRET_STORAGE, + restoreType: RestoreType.SecretStorage, }); try { // `accessSecretStorage` may prompt for storage access as needed. - const recoverInfo = await accessSecretStorage(async () => { - return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( + await accessSecretStorage(async () => { + await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( this.state.backupInfo, undefined, undefined, - { progressCallback: this._progressCallback }, + { progressCallback: this.progressCallback }, ); }); this.setState({ loading: false, - recoverInfo, }); } catch (e) { 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 { if (!backupInfo) return false; try { const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache( undefined, /* targetRoomId */ undefined, /* targetSessionId */ backupInfo, - { progressCallback: this._progressCallback }, + { progressCallback: this.progressCallback }, ); this.setState({ recoverInfo, @@ -223,7 +254,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { } } - async _loadBackupStatus() { + private async loadBackupStatus(): Promise { this.setState({ loading: true, loadError: null, @@ -238,7 +269,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { backupKeyStored, }); - const gotCache = await this._restoreWithCachedKey(backupInfo); + const gotCache = await this.restoreWithCachedKey(backupInfo); if (gotCache) { logger.log("RestoreKeyBackupDialog: found cached backup key"); 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 (backupKeyStored) { - return this._restoreWithSecretStorage(); + return this.restoreWithSecretStorage(); } 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 Spinner = sdk.getComponent("elements.Spinner"); @@ -281,12 +315,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { if (this.state.loading) { title = _t("Restoring keys from backup"); let details; - if (this.state.progress.stage === "fetch") { + if (this.state.progress.stage === ProgressState.Fetch) { details = _t("Fetching keys from server..."); - } else if (this.state.progress.stage === "load_keys") { + } else if (this.state.progress.stage === ProgressState.LoadKeys) { const { total, successes, failures } = this.state.progress; details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures }); - } else if (this.state.progress.stage === "prefetch") { + } else if (this.state.progress.stage === ProgressState.PreFetch) { details = _t("Fetching keys from server..."); } content =
@@ -298,7 +332,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { content = _t("Unable to load backup status"); } else if (this.state.restoreError) { if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) { - if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) { + if (this.state.restoreType === RestoreType.RecoveryKey) { title = _t("Security Key mismatch"); content =

{ _t( @@ -323,7 +357,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { title = _t("Error"); content = _t("No backup found!"); } else if (this.state.recoverInfo) { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); title = _t("Keys restored"); let failedToDecrypt; if (this.state.recoverInfo.total > this.state.recoverInfo.imported) { @@ -336,14 +369,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {

{ _t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported }) }

{ failedToDecrypt }
; } else if (backupHasPassphrase && !this.state.forceRecoveryKey) { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); title = _t("Enter Security Phrase"); content =

{ _t( @@ -359,16 +390,16 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {

@@ -381,14 +412,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { button1: s => { s } , button2: s => { s } , @@ -396,8 +427,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
; } else { title = _t("Enter Security Key"); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let keyStatus; if (this.state.recoveryKey.length === 0) { @@ -425,15 +454,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
{ keyStatus } @@ -445,7 +474,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { { button: s => { s } , diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 2f4e70c5dd..0323769536 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -20,6 +20,7 @@ import BaseDialog from '../BaseDialog'; import { _t } from '../../../../languageHandler'; import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../../utils/replaceableComponent"; +import { IDialogProps } from "../IDialogProps"; function iconFromPhase(phase: Phase) { if (phase === Phase.Done) { @@ -29,12 +30,9 @@ function iconFromPhase(phase: Phase) { } } -interface IProps { - onFinished: (success: boolean) => void; -} - +interface IProps extends IDialogProps {} interface IState { - icon: Phase; + icon: string; } @replaceableComponent("views.dialogs.security.SetupEncryptionDialog")