From 88cadd52a8d178ead74abd1ec9f8b6af6f4377a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 19:54:43 +0200 Subject: [PATCH 01/20] Convert VerificationQRCode to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...ificationQRCode.js => VerificationQRCode.tsx} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename src/components/views/elements/crypto/{VerificationQRCode.js => VerificationQRCode.tsx} (79%) diff --git a/src/components/views/elements/crypto/VerificationQRCode.js b/src/components/views/elements/crypto/VerificationQRCode.tsx similarity index 79% rename from src/components/views/elements/crypto/VerificationQRCode.js rename to src/components/views/elements/crypto/VerificationQRCode.tsx index 76cfb82d353..be9ede59b1d 100644 --- a/src/components/views/elements/crypto/VerificationQRCode.js +++ b/src/components/views/elements/crypto/VerificationQRCode.tsx @@ -15,20 +15,20 @@ limitations under the License. */ import React from "react"; -import PropTypes from "prop-types"; import { replaceableComponent } from "../../../../utils/replaceableComponent"; import QRCode from "../QRCode"; +import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode"; -@replaceableComponent("views.elements.crypto.VerificationQRCode") -export default class VerificationQRCode extends React.PureComponent { - static propTypes = { - qrCodeData: PropTypes.object.isRequired, - }; +interface IProps { + qrCodeData: QRCodeData; +} - render() { +@replaceableComponent("views.elements.crypto.VerificationQRCode") +export default class VerificationQRCode extends React.PureComponent { + public render(): JSX.Element { return ( ); From 971d375a5c9fb664159870ef1907a2206acf94bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 19:57:22 +0200 Subject: [PATCH 02/20] Convert AppWarning to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../elements/{AppWarning.js => AppWarning.tsx} | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) rename src/components/views/elements/{AppWarning.js => AppWarning.tsx} (60%) diff --git a/src/components/views/elements/AppWarning.js b/src/components/views/elements/AppWarning.tsx similarity index 60% rename from src/components/views/elements/AppWarning.js rename to src/components/views/elements/AppWarning.tsx index 517242dab5a..bac486d4b80 100644 --- a/src/components/views/elements/AppWarning.js +++ b/src/components/views/elements/AppWarning.tsx @@ -1,24 +1,20 @@ -import React from 'react'; // eslint-disable-line no-unused-vars -import PropTypes from 'prop-types'; +import React from 'react'; -const AppWarning = (props) => { +interface IProps { + errorMsg?: string; +} + +const AppWarning: React.FC = (props) => { return (
- { props.errorMsg } + { props.errorMsg || "Error" }
); }; -AppWarning.propTypes = { - errorMsg: PropTypes.string, -}; -AppWarning.defaultProps = { - errorMsg: 'Error', -}; - export default AppWarning; From ea623e79bb79e61ca2305e198299cbb3cdba9f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 20:11:58 +0200 Subject: [PATCH 03/20] Convert DirectorySearchBox to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...orySearchBox.js => DirectorySearchBox.tsx} | 81 +++++++++---------- 1 file changed, 37 insertions(+), 44 deletions(-) rename src/components/views/elements/{DirectorySearchBox.js => DirectorySearchBox.tsx} (63%) diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.tsx similarity index 63% rename from src/components/views/elements/DirectorySearchBox.js rename to src/components/views/elements/DirectorySearchBox.tsx index 11b1ed2cd2c..07407af6229 100644 --- a/src/components/views/elements/DirectorySearchBox.js +++ b/src/components/views/elements/DirectorySearchBox.tsx @@ -14,71 +14,73 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; +import React, { ChangeEvent, createRef } from 'react'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AccessibleButton from "./AccessibleButton"; + +interface IProps { + className?: string; + onChange?: (value: string) => void; + onClear?: () => void; + onJoinClick?: (value: string) => void; + placeholder?: string; + showJoinButton?: boolean; + initialText?: string; +} + +interface IState { + value: string; +} @replaceableComponent("views.elements.DirectorySearchBox") -export default class DirectorySearchBox extends React.Component { - constructor(props) { - super(props); - this._collectInput = this._collectInput.bind(this); - this._onClearClick = this._onClearClick.bind(this); - this._onChange = this._onChange.bind(this); - this._onKeyUp = this._onKeyUp.bind(this); - this._onJoinButtonClick = this._onJoinButtonClick.bind(this); +export default class DirectorySearchBox extends React.Component { + private input = createRef(); - this.input = null; + constructor(props: IProps) { + super(props); this.state = { value: this.props.initialText || '', }; } - _collectInput(e) { - this.input = e; - } - - _onClearClick() { + private onClearClick = (): void => { this.setState({ value: '' }); - if (this.input) { - this.input.focus(); + if (this.input.current) { + this.input.current.focus(); if (this.props.onClear) { this.props.onClear(); } } - } + }; - _onChange(ev) { - if (!this.input) return; + private onChange = (ev: ChangeEvent): void => { + if (!this.input.current) return; this.setState({ value: ev.target.value }); if (this.props.onChange) { this.props.onChange(ev.target.value); } - } + }; - _onKeyUp(ev) { + private onKeyUp = (ev: React.KeyboardEvent): void => { if (ev.key == 'Enter' && this.props.showJoinButton) { if (this.props.onJoinClick) { this.props.onJoinClick(this.state.value); } } - } + }; - _onJoinButtonClick() { + private onJoinButtonClick = (): void => { if (this.props.onJoinClick) { this.props.onJoinClick(this.state.value); } - } - - render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + }; + public render(): JSX.Element { const searchboxClasses = { mx_DirectorySearchBox: true, }; @@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component { let joinButton; if (this.props.showJoinButton) { joinButton = { _t("Join") }; } @@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component { name="dirsearch" value={this.state.value} className="mx_textinput_icon mx_textinput_search" - ref={this._collectInput} - onChange={this._onChange} - onKeyUp={this._onKeyUp} + ref={this.input} + onChange={this.onChange} + onKeyUp={this.onKeyUp} placeholder={this.props.placeholder} autoFocus /> { joinButton } - + ; } } -DirectorySearchBox.propTypes = { - className: PropTypes.string, - onChange: PropTypes.func, - onClear: PropTypes.func, - onJoinClick: PropTypes.func, - placeholder: PropTypes.string, - showJoinButton: PropTypes.bool, - initialText: PropTypes.string, -}; From 17d2998ec1855611d5eed661482d47709ad59c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 20:22:09 +0200 Subject: [PATCH 04/20] Convert EditableTextContainer to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...Container.js => EditableTextContainer.tsx} | 121 +++++++++--------- 1 file changed, 60 insertions(+), 61 deletions(-) rename src/components/views/elements/{EditableTextContainer.js => EditableTextContainer.tsx} (65%) diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.tsx similarity index 65% rename from src/components/views/elements/EditableTextContainer.js rename to src/components/views/elements/EditableTextContainer.tsx index 5778446355e..9610b188fbb 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.tsx @@ -15,9 +15,34 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Spinner from "./Spinner"; +import EditableText from "./EditableText"; + +interface IProps { + /* callback to retrieve the initial value. */ + getInitialValue?: () => Promise; + + /* initial value; used if getInitialValue is not given */ + initialValue?: string; + + /* placeholder text to use when the value is empty (and not being + * edited) */ + placeholder?: string; + + /* callback to update the value. Called with a single argument: the new + * value. */ + onSubmit?: (value: string) => Promise<{} | void>; + + /* should the input submit when focus is lost? */ + blurToSubmit?: boolean; +} + +interface IState { + busy: boolean; + errorString: string; + value: string; +} /** * A component which wraps an EditableText, with a spinner while updates take @@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; * taken from the 'initialValue' property. */ @replaceableComponent("views.elements.EditableTextContainer") -export default class EditableTextContainer extends React.Component { - constructor(props) { +export default class EditableTextContainer extends React.Component { + private _unmounted = false; + public static defaultProps: Partial = { + initialValue: "", + placeholder: "", + blurToSubmit: false, + onSubmit: () => { return Promise.resolve(); }, + }; + + constructor(props: IProps) { super(props); - this._unmounted = false; this.state = { busy: false, errorString: null, value: props.initialValue, }; - this._onValueChanged = this._onValueChanged.bind(this); } - componentDidMount() { - if (this.props.getInitialValue === undefined) { - // use whatever was given in the initialValue property. - return; - } + public async componentDidMount(): Promise { + // use whatever was given in the initialValue property. + if (this.props.getInitialValue === undefined) return; this.setState({ busy: true }); - - this.props.getInitialValue().then( - (result) => { - if (this._unmounted) { return; } - this.setState({ - busy: false, - value: result, - }); - }, - (error) => { - if (this._unmounted) { return; } - this.setState({ - errorString: error.toString(), - busy: false, - }); - }, - ); + try { + const initialValue = await this.props.getInitialValue(); + if (this._unmounted) return; + this.setState({ + busy: false, + value: initialValue, + }); + } catch (error) { + if (this._unmounted) return; + this.setState({ + errorString: error.toString(), + busy: false, + }); + } } - componentWillUnmount() { + public componentWillUnmount(): void { this._unmounted = true; } - _onValueChanged(value, shouldSubmit) { + private onValueChanged = (value: string, shouldSubmit: boolean): void => { if (!shouldSubmit) { return; } @@ -100,24 +126,22 @@ export default class EditableTextContainer extends React.Component { }); }, ); - } + }; - render() { + public render(): JSX.Element { if (this.state.busy) { - const Loader = sdk.getComponent("elements.Spinner"); return ( - + ); } else if (this.state.errorString) { return (
{ this.state.errorString }
); } else { - const EditableText = sdk.getComponent('elements.EditableText'); return ( ); @@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component { } } -EditableTextContainer.propTypes = { - /* callback to retrieve the initial value. */ - getInitialValue: PropTypes.func, - - /* initial value; used if getInitialValue is not given */ - initialValue: PropTypes.string, - - /* placeholder text to use when the value is empty (and not being - * edited) */ - placeholder: PropTypes.string, - - /* callback to update the value. Called with a single argument: the new - * value. */ - onSubmit: PropTypes.func, - - /* should the input submit when focus is lost? */ - blurToSubmit: PropTypes.bool, -}; - -EditableTextContainer.defaultProps = { - initialValue: "", - placeholder: "", - blurToSubmit: false, - onSubmit: function(v) {return Promise.resolve(); }, -}; From 1ee606c693fb3caa9ff8c00aba8f34f45c5cf491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 20:35:30 +0200 Subject: [PATCH 05/20] Convert LanguageDropdown to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...nguageDropdown.js => LanguageDropdown.tsx} | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) rename src/components/views/elements/{LanguageDropdown.js => LanguageDropdown.tsx} (84%) diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.tsx similarity index 84% rename from src/components/views/elements/LanguageDropdown.js rename to src/components/views/elements/LanguageDropdown.tsx index 3f17a78629e..c6c52ee4e82 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -16,13 +16,13 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import * as languageHandler from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Spinner from "./Spinner"; +import Dropdown from "./Dropdown"; function languageMatchesSearchQuery(query, language) { if (language.label.toUpperCase().includes(query.toUpperCase())) return true; @@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) { return false; } +interface IProps { + className?: string; + onOptionChange: (language: string) => void; + value?: string; + disabled?: boolean; +} + +interface IState { + searchQuery: string; + langs: string[]; +} + @replaceableComponent("views.elements.LanguageDropdown") -export default class LanguageDropdown extends React.Component { - constructor(props) { +export default class LanguageDropdown extends React.Component { + constructor(props: IProps) { super(props); - this._onSearchChange = this._onSearchChange.bind(this); this.state = { searchQuery: '', @@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component { }; } - componentDidMount() { + public componentDidMount(): void { languageHandler.getAllLanguagesFromJson().then((langs) => { langs.sort(function(a, b) { if (a.label < b.label) return -1; @@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component { } } - _onSearchChange(search) { + private onSearchChange = (search: string): void => { this.setState({ searchQuery: search, }); - } + }; - render() { + public render(): JSX.Element { if (this.state.langs === null) { - const Spinner = sdk.getComponent('elements.Spinner'); return ; } - const Dropdown = sdk.getComponent('elements.Dropdown'); - let displayedLanguages; if (this.state.searchQuery) { displayedLanguages = this.state.langs.filter((lang) => { @@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component { id="mx_LanguageDropdown" className={this.props.className} onOptionChange={this.props.onOptionChange} - onSearchChange={this._onSearchChange} + onSearchChange={this.onSearchChange} searchEnabled={true} value={value} label={_t("Language Dropdown")} @@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component { } } -LanguageDropdown.propTypes = { - className: PropTypes.string, - onOptionChange: PropTypes.func.isRequired, - value: PropTypes.string, -}; From 9e2bc28c06f121429ebe2253dd5d4936bc1edce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 20:44:26 +0200 Subject: [PATCH 06/20] Convert LazyRenderList TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../{LazyRenderList.js => LazyRenderList.tsx} | 89 ++++++++++--------- 1 file changed, 49 insertions(+), 40 deletions(-) rename src/components/views/elements/{LazyRenderList.js => LazyRenderList.tsx} (79%) diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.tsx similarity index 79% rename from src/components/views/elements/LazyRenderList.js rename to src/components/views/elements/LazyRenderList.tsx index 070d9bcc8da..65d578b56fa 100644 --- a/src/components/views/elements/LazyRenderList.js +++ b/src/components/views/elements/LazyRenderList.tsx @@ -15,17 +15,16 @@ limitations under the License. */ import React from "react"; -import PropTypes from 'prop-types'; import { replaceableComponent } from "../../../utils/replaceableComponent"; class ItemRange { - constructor(topCount, renderCount, bottomCount) { - this.topCount = topCount; - this.renderCount = renderCount; - this.bottomCount = bottomCount; - } + constructor( + public topCount: number, + public renderCount: number, + public bottomCount: number, + ) { } - contains(range) { + public contains(range: ItemRange): boolean { // don't contain empty ranges // as it will prevent clearing the list // once it is scrolled far enough out of view @@ -36,7 +35,7 @@ class ItemRange { (range.topCount + range.renderCount) <= (this.topCount + this.renderCount); } - expand(amount) { + public expand(amount: number): ItemRange { // don't expand ranges that won't render anything if (this.renderCount === 0) { return this; @@ -51,20 +50,55 @@ class ItemRange { ); } - totalSize() { + public totalSize(): number { return this.topCount + this.renderCount + this.bottomCount; } } +interface IProps { + // height in pixels of the component returned by `renderItem` + itemHeight: number; + // function to turn an element of `items` into a react component + renderItem: (item: T) => JSX.Element; + // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s) + scrollTop: number; + // the height of the viewport this content is scrolled in + height: number; + // all items for the list. These should not be react components, see `renderItem`. + items?: T[]; + // the amount of items to scroll before causing a rerender, + // should typically be less than `overflowItems` unless applying + // margins in the parent component when using multiple LazyRenderList in one viewport. + // use 0 to only rerender when items will come into view. + overflowMargin?: number; + // the amount of items to add at the top and bottom to render, + // so not every scroll of causes a rerender. + overflowItems?: number; + + element?: string; + className?: string; +} + +interface IState { + renderRange: ItemRange; +} + @replaceableComponent("views.elements.LazyRenderList") -export default class LazyRenderList extends React.Component { - constructor(props) { +export default class LazyRenderList extends React.Component, IState> { + public static defaultProps: Partial> = { + overflowItems: 20, + overflowMargin: 5, + }; + + constructor(props: IProps) { super(props); - this.state = {}; + this.state = { + renderRange: null, + }; } - static getDerivedStateFromProps(props, state) { + public static getDerivedStateFromProps(props: IProps, state: IState): Partial { const range = LazyRenderList.getVisibleRangeFromProps(props); const intersectRange = range.expand(props.overflowMargin); const renderRange = range.expand(props.overflowItems); @@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component { return null; } - static getVisibleRangeFromProps(props) { + private static getVisibleRangeFromProps(props: IProps): ItemRange { const { items, itemHeight, scrollTop, height } = props; const length = items ? items.length : 0; const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length); @@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component { return new ItemRange(topCount, renderCount, bottomCount); } - render() { + public render(): JSX.Element { const { itemHeight, items, renderItem } = this.props; const { renderRange } = this.state; const { topCount, renderCount, bottomCount } = renderRange; @@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component { } } -LazyRenderList.defaultProps = { - overflowItems: 20, - overflowMargin: 5, -}; - -LazyRenderList.propTypes = { - // height in pixels of the component returned by `renderItem` - itemHeight: PropTypes.number.isRequired, - // function to turn an element of `items` into a react component - renderItem: PropTypes.func.isRequired, - // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s) - scrollTop: PropTypes.number.isRequired, - // the height of the viewport this content is scrolled in - height: PropTypes.number.isRequired, - // all items for the list. These should not be react components, see `renderItem`. - items: PropTypes.array, - // the amount of items to scroll before causing a rerender, - // should typically be less than `overflowItems` unless applying - // margins in the parent component when using multiple LazyRenderList in one viewport. - // use 0 to only rerender when items will come into view. - overflowMargin: PropTypes.number, - // the amount of items to add at the top and bottom to render, - // so not every scroll of causes a rerender. - overflowItems: PropTypes.number, -}; From 96d1519ac5b2c3b1ab92233a00b17c4a0f2d9c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 20:47:49 +0200 Subject: [PATCH 07/20] Convert Spoiler to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../elements/{Spoiler.js => Spoiler.tsx} | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) rename src/components/views/elements/{Spoiler.js => Spoiler.tsx} (82%) diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.tsx similarity index 82% rename from src/components/views/elements/Spoiler.js rename to src/components/views/elements/Spoiler.tsx index 802c6cf8412..4779a7d90e1 100644 --- a/src/components/views/elements/Spoiler.js +++ b/src/components/views/elements/Spoiler.tsx @@ -17,25 +17,34 @@ import React from 'react'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + reason?: string; + contentHtml: string; +} + +interface IState { + visible: boolean; +} + @replaceableComponent("views.elements.Spoiler") -export default class Spoiler extends React.Component { - constructor(props) { +export default class Spoiler extends React.Component { + constructor(props: IProps) { super(props); this.state = { visible: false, }; } - toggleVisible(e) { + private toggleVisible = (e: React.MouseEvent): void => { if (!this.state.visible) { // we are un-blurring, we don't want this click to propagate to potential child pills e.preventDefault(); e.stopPropagation(); } this.setState({ visible: !this.state.visible }); - } + }; - render() { + public render(): JSX.Element { const reason = this.props.reason ? ( { "(" + this.props.reason + ")" } ) : null; @@ -43,7 +52,7 @@ export default class Spoiler extends React.Component { // as such, we pass the this.props.contentHtml instead and then set the raw // HTML content. This is secure as the contents have already been parsed previously return ( - + { reason }   From 8bf5d97b9e70812714331774271f1777b4a617e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 15 Sep 2021 21:06:06 +0200 Subject: [PATCH 08/20] Convert TextWithTooltip to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...TextWithTooltip.js => TextWithTooltip.tsx} | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) rename src/components/views/elements/{TextWithTooltip.js => TextWithTooltip.tsx} (71%) diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.tsx similarity index 71% rename from src/components/views/elements/TextWithTooltip.js rename to src/components/views/elements/TextWithTooltip.tsx index 288d33f71b1..b7c24771588 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -15,42 +15,44 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Tooltip from "./Tooltip"; -@replaceableComponent("views.elements.TextWithTooltip") -export default class TextWithTooltip extends React.Component { - static propTypes = { - class: PropTypes.string, - tooltipClass: PropTypes.string, - tooltip: PropTypes.node.isRequired, - tooltipProps: PropTypes.object, - }; +interface IProps { + class?: string; + tooltipClass?: string; + tooltip: React.ReactNode; + tooltipProps?: {}; + onClick?: (ev?: React.MouseEvent) => void; +} - constructor() { - super(); +interface IState { + hover: boolean; +} + +@replaceableComponent("views.elements.TextWithTooltip") +export default class TextWithTooltip extends React.Component { + constructor(props: IProps) { + super(props); this.state = { hover: false, }; } - onMouseOver = () => { + private onMouseOver = (): void => { this.setState({ hover: true }); }; - onMouseLeave = () => { + private onMouseLeave = (): void => { this.setState({ hover: false }); }; - render() { - const Tooltip = sdk.getComponent("elements.Tooltip"); - + public render(): JSX.Element { const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; return ( - + { children } { this.state.hover && Date: Wed, 15 Sep 2021 21:09:23 +0200 Subject: [PATCH 09/20] Convert SyntaxHighlight to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...SyntaxHighlight.js => SyntaxHighlight.tsx} | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) rename src/components/views/elements/{SyntaxHighlight.js => SyntaxHighlight.tsx} (73%) diff --git a/src/components/views/elements/SyntaxHighlight.js b/src/components/views/elements/SyntaxHighlight.tsx similarity index 73% rename from src/components/views/elements/SyntaxHighlight.js rename to src/components/views/elements/SyntaxHighlight.tsx index 2c29f7c989b..cd65cddfba0 100644 --- a/src/components/views/elements/SyntaxHighlight.js +++ b/src/components/views/elements/SyntaxHighlight.tsx @@ -15,40 +15,40 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { highlightBlock } from 'highlight.js'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + className?: string; + children?: React.ReactNode; +} + @replaceableComponent("views.elements.SyntaxHighlight") -export default class SyntaxHighlight extends React.Component { - static propTypes = { - className: PropTypes.string, - children: PropTypes.node, - }; +export default class SyntaxHighlight extends React.Component { + private el: HTMLPreElement = null; - constructor(props) { + constructor(props: IProps) { super(props); - - this._ref = this._ref.bind(this); } // componentDidUpdate used here for reusability - componentDidUpdate() { - if (this._el) highlightBlock(this._el); + public componentDidUpdate(): void { + if (this.el) highlightBlock(this.el); } // call componentDidUpdate because _ref is fired on initial render // which does not fire componentDidUpdate - _ref(el) { - this._el = el; + private ref = (el: HTMLPreElement): void => { + this.el = el; this.componentDidUpdate(); - } + }; - render() { + public render(): JSX.Element { const { className, children } = this.props; - return
+        return 
             { children }
         
; } } + From 819a2b4416905d4cf28934d69a1de8ca6d695c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 16 Sep 2021 17:55:51 +0200 Subject: [PATCH 10/20] Convert PowerSelector to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../{PowerSelector.js => PowerSelector.tsx} | 102 ++++++++++-------- 1 file changed, 57 insertions(+), 45 deletions(-) rename src/components/views/elements/{PowerSelector.js => PowerSelector.tsx} (61%) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.tsx similarity index 61% rename from src/components/views/elements/PowerSelector.js rename to src/components/views/elements/PowerSelector.tsx index 42386ca5c11..f6e24f2bbb4 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.tsx @@ -15,40 +15,52 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as Roles from '../../../Roles'; import { _t } from '../../../languageHandler'; import Field from "./Field"; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.elements.PowerSelector") -export default class PowerSelector extends React.Component { - static propTypes = { - value: PropTypes.number.isRequired, - // The maximum value that can be set with the power selector - maxValue: PropTypes.number.isRequired, +const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM"; - // Default user power level for the room - usersDefault: PropTypes.number.isRequired, +interface IProps { + value: number; + // The maximum value that can be set with the power selector + maxValue: number; - // should the user be able to change the value? false by default. - disabled: PropTypes.bool, - onChange: PropTypes.func, + // Default user power level for the room + usersDefault: number; - // Optional key to pass as the second argument to `onChange` - powerLevelKey: PropTypes.string, + // should the user be able to change the value? false by default. + disabled?: boolean; + onChange?: (value: string, powerLevelKey: string) => void; - // The name to annotate the selector with - label: PropTypes.string, - } + // Optional key to pass as the second argument to `onChange` + powerLevelKey?: string; + + // The name to annotate the selector with + label?: string; +} + +interface IState { + levelRoleMap: {}; + // List of power levels to show in the drop-down + options: number[]; - static defaultProps = { + customValue: number; + selectValue: number | string; + custom?: boolean; + customLevel?: number; +} + +@replaceableComponent("views.elements.PowerSelector") +export default class PowerSelector extends React.Component { + public static defaultProps: Partial = { maxValue: Infinity, usersDefault: 0, }; - constructor(props) { + constructor(props: IProps) { super(props); this.state = { @@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase - UNSAFE_componentWillMount() { - this._initStateFromProps(this.props); + // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention + public UNSAFE_componentWillMount(): void { + this.initStateFromProps(this.props); } - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps(newProps) { - this._initStateFromProps(newProps); + // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention + public UNSAFE_componentWillReceiveProps(newProps: IProps): void { + this.initStateFromProps(newProps); } - _initStateFromProps(newProps) { + private initStateFromProps(newProps: IProps): void { // This needs to be done now because levelRoleMap has translated strings const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); const options = Object.keys(levelRoleMap).filter(level => { return ( level === undefined || - level <= newProps.maxValue || - level == newProps.value + parseInt(level) <= newProps.maxValue || + parseInt(level) == newProps.value ); - }); + }).map(level => parseInt(level)); const isCustom = levelRoleMap[newProps.value] === undefined; @@ -90,32 +102,32 @@ export default class PowerSelector extends React.Component { options, custom: isCustom, customLevel: newProps.value, - selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value, + selectValue: isCustom ? CUSTOM_VALUE : newProps.value, }); } - onSelectChange = event => { - const isCustom = event.target.value === "SELECT_VALUE_CUSTOM"; + private onSelectChange = (event: React.ChangeEvent): void => { + const isCustom = event.target.value === CUSTOM_VALUE; if (isCustom) { this.setState({ custom: true }); } else { this.props.onChange(event.target.value, this.props.powerLevelKey); - this.setState({ selectValue: event.target.value }); + this.setState({ selectValue: parseInt(event.target.value) }); } }; - onCustomChange = event => { - this.setState({ customValue: event.target.value }); + private onCustomChange = (event: React.ChangeEvent): void => { + this.setState({ customValue: parseInt(event.target.value) }); }; - onCustomBlur = event => { + private onCustomBlur = (event: React.FocusEvent): void => { event.preventDefault(); event.stopPropagation(); - this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey); + this.props.onChange(String(this.state.customValue), this.props.powerLevelKey); }; - onCustomKeyDown = event => { + private onCustomKeyDown = (event: React.KeyboardEvent): void => { if (event.key === Key.ENTER) { event.preventDefault(); event.stopPropagation(); @@ -125,11 +137,11 @@ export default class PowerSelector extends React.Component { // raising a dialog which causes a blur which causes a dialog which causes a blur and // so on. By not causing the onChange to be called here, we avoid the loop because we // handle the onBlur safely. - event.target.blur(); + (event.target as HTMLInputElement).blur(); } }; - render() { + public render(): JSX.Element { let picker; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; if (this.state.custom) { @@ -147,14 +159,14 @@ export default class PowerSelector extends React.Component { ); } else { // Each level must have a definition in this.state.levelRoleMap - let options = this.state.options.map((level) => { + const options = this.state.options.map((level) => { return { - value: level, + value: String(level), text: Roles.textualPowerLevel(level, this.props.usersDefault), }; }); - options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") }); - options = options.map((op) => { + options.push({ value: CUSTOM_VALUE, text: _t("Custom level") }); + const optionsElements = options.map((op) => { return ; }); @@ -166,7 +178,7 @@ export default class PowerSelector extends React.Component { value={String(this.state.selectValue)} disabled={this.props.disabled} > - { options } + { optionsElements } ); } From a8d3bb76efb8f5437a6a79f7b3bae78a513b9381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 16 Sep 2021 18:46:18 +0200 Subject: [PATCH 11/20] Convert PersistentApp to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../{PersistentApp.js => PersistentApp.tsx} | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) rename src/components/views/elements/{PersistentApp.js => PersistentApp.tsx} (72%) diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.tsx similarity index 72% rename from src/components/views/elements/PersistentApp.js rename to src/components/views/elements/PersistentApp.tsx index 763ab634873..1f911659e2f 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.tsx @@ -19,57 +19,70 @@ import React from 'react'; import RoomViewStore from '../../../stores/RoomViewStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import WidgetUtils from '../../../utils/WidgetUtils'; -import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { EventSubscription } from 'fbemitter'; +import AppTile from "./AppTile"; +import { Room } from "matrix-js-sdk/src/models/room"; + +interface IState { + roomId: string; + persistentWidgetId: string; +} @replaceableComponent("views.elements.PersistentApp") -export default class PersistentApp extends React.Component { - state = { - roomId: RoomViewStore.getRoomId(), - persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), - }; +export default class PersistentApp extends React.Component<{}, IState> { + private roomStoreToken: EventSubscription; + + constructor() { + super({}); + + this.state = { + roomId: RoomViewStore.getRoomId(), + persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), + }; + } - componentDidMount() { - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); - ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate); - MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership); + public componentDidMount(): void { + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); + ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate); + MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); } - componentWillUnmount() { - if (this._roomStoreToken) { - this._roomStoreToken.remove(); + public componentWillUnmount(): void { + if (this.roomStoreToken) { + this.roomStoreToken.remove(); } - ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate); + ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate); if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership); + MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); } } - _onRoomViewStoreUpdate = payload => { + private onRoomViewStoreUpdate = (): void => { if (RoomViewStore.getRoomId() === this.state.roomId) return; this.setState({ roomId: RoomViewStore.getRoomId(), }); }; - _onActiveWidgetStoreUpdate = () => { + private onActiveWidgetStoreUpdate = (): void => { this.setState({ persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), }); }; - _onMyMembership = async (room, membership) => { + private onMyMembership = async (room: Room, membership: string): Promise => { const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); if (membership !== "join") { // we're not in the room anymore - delete - if (room.roomId === persistentWidgetInRoomId) { + if (room .roomId === persistentWidgetInRoomId) { ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); } } }; - render() { + public render(): JSX.Element { if (this.state.persistentWidgetId) { const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); @@ -89,7 +102,6 @@ export default class PersistentApp extends React.Component { appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, appEvent.getId(), ); - const AppTile = sdk.getComponent('elements.AppTile'); return Date: Thu, 16 Sep 2021 18:57:32 +0200 Subject: [PATCH 12/20] Convert PersistedElement to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...rsistedElement.js => PersistedElement.tsx} | 101 +++++++++--------- 1 file changed, 50 insertions(+), 51 deletions(-) rename src/components/views/elements/{PersistedElement.js => PersistedElement.tsx} (69%) diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.tsx similarity index 69% rename from src/components/views/elements/PersistedElement.js rename to src/components/views/elements/PersistedElement.tsx index 03aa9e0d6de..8dda530097c 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.tsx @@ -16,25 +16,26 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; import { throttle } from "lodash"; -import ResizeObserver from 'resize-observer-polyfill'; import dis from '../../../dispatcher/dispatcher'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ActionPayload } from "../../../dispatcher/payloads"; + +export const getPersistKey = (appId: string) => 'widget_' + appId; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and // pass in a custom control as the actual body. -function getContainer(containerId) { - return document.getElementById(containerId); +function getContainer(containerId: string): HTMLDivElement { + return document.getElementById(containerId) as HTMLDivElement; } -function getOrCreateContainer(containerId) { +function getOrCreateContainer(containerId: string): HTMLDivElement { let container = getContainer(containerId); if (!container) { @@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) { return container; } -/* +interface IProps { + // Unique identifier for this PersistedElement instance + // Any PersistedElements with the same persistKey will use + // the same DOM container. + persistKey: string; + + // z-index for the element. Defaults to 9. + zIndex?: number; + + style?: React.StyleHTMLAttributes; +} + +/** * Class of component that renders its children in a separate ReactDOM virtual tree * in a container element appended to document.body. * @@ -58,53 +71,40 @@ function getOrCreateContainer(containerId) { * bounding rect as the parent of PE. */ @replaceableComponent("views.elements.PersistedElement") -export default class PersistedElement extends React.Component { - static propTypes = { - // Unique identifier for this PersistedElement instance - // Any PersistedElements with the same persistKey will use - // the same DOM container. - persistKey: PropTypes.string.isRequired, - - // z-index for the element. Defaults to 9. - zIndex: PropTypes.number, - }; +export default class PersistedElement extends React.Component { + private resizeObserver: ResizeObserver; + private dispatcherRef: string; + private childContainer: HTMLDivElement; + private child: HTMLDivElement; - constructor() { - super(); - this.collectChildContainer = this.collectChildContainer.bind(this); - this.collectChild = this.collectChild.bind(this); - this._repositionChild = this._repositionChild.bind(this); - this._onAction = this._onAction.bind(this); + constructor(props: IProps) { + super(props); - this.resizeObserver = new ResizeObserver(this._repositionChild); + this.resizeObserver = new ResizeObserver(this.repositionChild); // Annoyingly, a resize observer is insufficient, since we also care // about when the element moves on the screen without changing its // dimensions. Doesn't look like there's a ResizeObserver equivalent // for this, so we bodge it by listening for document resize and // the timeline_resize action. - window.addEventListener('resize', this._repositionChild); - this._dispatcherRef = dis.register(this._onAction); + window.addEventListener('resize', this.repositionChild); + this.dispatcherRef = dis.register(this.onAction); } /** * Removes the DOM elements created when a PersistedElement with the given * persistKey was mounted. The DOM elements will be re-added if another - * PeristedElement is mounted in the future. + * PersistedElement is mounted in the future. * * @param {string} persistKey Key used to uniquely identify this PersistedElement */ - static destroyElement(persistKey) { + public static destroyElement(persistKey: string): void { const container = getContainer('mx_persistedElement_' + persistKey); if (container) { container.remove(); } } - static isMounted(persistKey) { - return Boolean(getContainer('mx_persistedElement_' + persistKey)); - } - - collectChildContainer(ref) { + private collectChildContainer = (ref: HTMLDivElement): void => { if (this.childContainer) { this.resizeObserver.unobserve(this.childContainer); } @@ -112,48 +112,48 @@ export default class PersistedElement extends React.Component { if (ref) { this.resizeObserver.observe(ref); } - } + }; - collectChild(ref) { + private collectChild = (ref: HTMLDivElement): void => { this.child = ref; this.updateChild(); - } + }; - componentDidMount() { + public componentDidMount(): void { this.updateChild(); this.renderApp(); } - componentDidUpdate() { + public componentDidUpdate(): void { this.updateChild(); this.renderApp(); } - componentWillUnmount() { + public componentWillUnmount(): void { this.updateChildVisibility(this.child, false); this.resizeObserver.disconnect(); - window.removeEventListener('resize', this._repositionChild); - dis.unregister(this._dispatcherRef); + window.removeEventListener('resize', this.repositionChild); + dis.unregister(this.dispatcherRef); } - _onAction(payload) { + private onAction = (payload: ActionPayload): void => { if (payload.action === 'timeline_resize') { - this._repositionChild(); + this.repositionChild(); } else if (payload.action === 'logout') { PersistedElement.destroyElement(this.props.persistKey); } - } + }; - _repositionChild() { + private repositionChild = (): void => { this.updateChildPosition(this.child, this.childContainer); - } + }; - updateChild() { + private updateChild(): void { this.updateChildPosition(this.child, this.childContainer); this.updateChildVisibility(this.child, true); } - renderApp() { + private renderApp(): void { const content =
{ this.props.children } @@ -163,12 +163,12 @@ export default class PersistedElement extends React.Component { ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey)); } - updateChildVisibility(child, visible) { + private updateChildVisibility(child: HTMLDivElement, visible: boolean): void { if (!child) return; child.style.display = visible ? 'block' : 'none'; } - updateChildPosition = throttle((child, parent) => { + private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => { if (!child || !parent) return; const parentRect = parent.getBoundingClientRect(); @@ -182,9 +182,8 @@ export default class PersistedElement extends React.Component { }); }, 100, { trailing: true, leading: true }); - render() { + public render(): JSX.Element { return
; } } -export const getPersistKey = (appId) => 'widget_' + appId; From af853e1d86aa71fe78ed24a2b2ca780453df4b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 16 Sep 2021 19:16:36 +0200 Subject: [PATCH 13/20] Convert DialogButtons to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../eventindex/ManageEventIndexDialog.tsx | 5 +- .../views/dialogs/CryptoStoreTooNewDialog.tsx | 5 +- .../{DialogButtons.js => DialogButtons.tsx} | 71 ++++++++++--------- 3 files changed, 40 insertions(+), 41 deletions(-) rename src/components/views/elements/{DialogButtons.js => DialogButtons.tsx} (64%) diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 2748fda35ac..ac7875b920a 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel"; import Field from '../../../../components/views/elements/Field'; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; +import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; -interface IProps { - onFinished: (confirmed: boolean) => void; -} +interface IProps extends IDialogProps {} interface IState { eventIndexSize: number; diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index d03b668cd93..3bb78233eac 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -23,10 +23,9 @@ import Modal from '../../../Modal'; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; import QuestionDialog from "./QuestionDialog"; +import { IDialogProps } from "./IDialogProps"; -interface IProps { - onFinished: (success: boolean) => void; -} +interface IProps extends IDialogProps {} const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { const brand = SdkConfig.get().brand; diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.tsx similarity index 64% rename from src/components/views/elements/DialogButtons.js rename to src/components/views/elements/DialogButtons.tsx index 9dd4a84b9a6..0dff64c0b4b 100644 --- a/src/components/views/elements/DialogButtons.js +++ b/src/components/views/elements/DialogButtons.tsx @@ -17,60 +17,61 @@ limitations under the License. */ import React from "react"; -import PropTypes from "prop-types"; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -/** - * Basic container for buttons in modal dialogs. - */ -@replaceableComponent("views.elements.DialogButtons") -export default class DialogButtons extends React.Component { - static propTypes = { - // The primary button which is styled differently and has default focus. - primaryButton: PropTypes.node.isRequired, +interface IProps { + // The primary button which is styled differently and has default focus. + primaryButton: React.ReactNode; - // A node to insert into the cancel button instead of default "Cancel" - cancelButton: PropTypes.node, + // A node to insert into the cancel button instead of default "Cancel" + cancelButton?: React.ReactNode; - // If true, make the primary button a form submit button (input type="submit") - primaryIsSubmit: PropTypes.bool, + // If true, make the primary button a form submit button (input type="submit") + primaryIsSubmit?: boolean; - // onClick handler for the primary button. - onPrimaryButtonClick: PropTypes.func, + // onClick handler for the primary button. + onPrimaryButtonClick?: (ev: React.MouseEvent) => void; - // should there be a cancel button? default: true - hasCancel: PropTypes.bool, + // should there be a cancel button? default: true + hasCancel?: boolean; - // The class of the cancel button, only used if a cancel button is - // enabled - cancelButtonClass: PropTypes.node, + // The class of the cancel button, only used if a cancel button is + // enabled + cancelButtonClass?: string; - // onClick handler for the cancel button. - onCancel: PropTypes.func, + // onClick handler for the cancel button. + onCancel?: (...args: any[]) => void; - focus: PropTypes.bool, + focus?: boolean; - // disables the primary and cancel buttons - disabled: PropTypes.bool, + // disables the primary and cancel buttons + disabled?: boolean; - // disables only the primary button - primaryDisabled: PropTypes.bool, + // disables only the primary button + primaryDisabled?: boolean; - // something to stick next to the buttons, optionally - additive: PropTypes.element, - }; + // something to stick next to the buttons, optionally + additive?: React.ReactNode; - static defaultProps = { + primaryButtonClass?: string; +} + +/** + * Basic container for buttons in modal dialogs. + */ +@replaceableComponent("views.elements.DialogButtons") +export default class DialogButtons extends React.Component { + public static defaultProps: Partial = { hasCancel: true, disabled: false, }; - _onCancelClick = () => { - this.props.onCancel(); + private onCancelClick = (event: React.MouseEvent): void => { + this.props.onCancel(event); }; - render() { + public render(): JSX.Element { let primaryButtonClassName = "mx_Dialog_primary"; if (this.props.primaryButtonClass) { primaryButtonClassName += " " + this.props.primaryButtonClass; @@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component { // important: the default type is 'submit' and this button comes before the // primary in the DOM so will get form submissions unless we make it not a submit. type="button" - onClick={this._onCancelClick} + onClick={this.onCancelClick} className={this.props.cancelButtonClass} disabled={this.props.disabled} > From 03ce568a5de18b177232f228417fc82fb738846c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 16 Sep 2021 19:33:17 +0200 Subject: [PATCH 14/20] Convert EditableText to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../{EditableText.js => EditableText.tsx} | 143 +++++++++--------- 1 file changed, 68 insertions(+), 75 deletions(-) rename src/components/views/elements/{EditableText.js => EditableText.tsx} (62%) diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.tsx similarity index 62% rename from src/components/views/elements/EditableText.js rename to src/components/views/elements/EditableText.tsx index 6dbc8b87713..b3ff8ee245a 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.tsx @@ -16,33 +16,42 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.elements.EditableText") -export default class EditableText extends React.Component { - static propTypes = { - onValueChanged: PropTypes.func, - initialValue: PropTypes.string, - label: PropTypes.string, - placeholder: PropTypes.string, - className: PropTypes.string, - labelClassName: PropTypes.string, - placeholderClassName: PropTypes.string, - // Overrides blurToSubmit if true - blurToCancel: PropTypes.bool, - // Will cause onValueChanged(value, true) to fire on blur - blurToSubmit: PropTypes.bool, - editable: PropTypes.bool, - }; +enum Phases { + Display = "display", + Edit = "edit", +} - static Phases = { - Display: "display", - Edit: "edit", - }; +interface IProps { + onValueChanged?: (value: string, shouldSubmit: boolean) => void; + initialValue?: string; + label?: string; + placeholder?: string; + className?: string; + labelClassName?: string; + placeholderClassName?: string; + // Overrides blurToSubmit if true + blurToCancel?: boolean; + // Will cause onValueChanged(value, true) to fire on blur + blurToSubmit?: boolean; + editable?: boolean; +} - static defaultProps = { +interface IState { + phase: Phases; +} + +@replaceableComponent("views.elements.EditableText") +export default class EditableText extends React.Component { + // we track value as an JS object field rather than in React state + // as React doesn't play nice with contentEditable. + public value = ''; + private placeholder = false; + private editableDiv = createRef(); + + public static defaultProps: Partial = { onValueChanged() {}, initialValue: '', label: '', @@ -53,81 +62,61 @@ export default class EditableText extends React.Component { blurToSubmit: false, }; - constructor(props) { + constructor(props: IProps) { super(props); - // we track value as an JS object field rather than in React state - // as React doesn't play nice with contentEditable. - this.value = ''; - this.placeholder = false; - - this._editable_div = createRef(); + this.state = { + phase: Phases.Display, + }; } - state = { - phase: EditableText.Phases.Display, - }; - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps(nextProps) { + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase + public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { if (nextProps.initialValue !== this.props.initialValue) { this.value = nextProps.initialValue; - if (this._editable_div.current) { + if (this.editableDiv.current) { this.showPlaceholder(!this.value); } } } - componentDidMount() { + public componentDidMount(): void { this.value = this.props.initialValue; - if (this._editable_div.current) { + if (this.editableDiv.current) { this.showPlaceholder(!this.value); } } - showPlaceholder = show => { + private showPlaceholder = (show: boolean): void => { if (show) { - this._editable_div.current.textContent = this.props.placeholder; - this._editable_div.current.setAttribute("class", this.props.className + this.editableDiv.current.textContent = this.props.placeholder; + this.editableDiv.current.setAttribute("class", this.props.className + " " + this.props.placeholderClassName); this.placeholder = true; this.value = ''; } else { - this._editable_div.current.textContent = this.value; - this._editable_div.current.setAttribute("class", this.props.className); + this.editableDiv.current.textContent = this.value; + this.editableDiv.current.setAttribute("class", this.props.className); this.placeholder = false; } }; - getValue = () => this.value; - - setValue = value => { - this.value = value; - this.showPlaceholder(!this.value); - }; - - edit = () => { - this.setState({ - phase: EditableText.Phases.Edit, - }); - }; - - cancelEdit = () => { + private cancelEdit = (): void => { this.setState({ - phase: EditableText.Phases.Display, + phase: Phases.Display, }); this.value = this.props.initialValue; this.showPlaceholder(!this.value); this.onValueChanged(false); - this._editable_div.current.blur(); + this.editableDiv.current.blur(); }; - onValueChanged = shouldSubmit => { + private onValueChanged = (shouldSubmit: boolean): void => { this.props.onValueChanged(this.value, shouldSubmit); }; - onKeyDown = ev => { + private onKeyDown = (ev: React.KeyboardEvent): void => { // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); if (this.placeholder) { @@ -142,13 +131,13 @@ export default class EditableText extends React.Component { // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); }; - onKeyUp = ev => { + private onKeyUp = (ev: React.KeyboardEvent): void => { // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); - if (!ev.target.textContent) { + if (!(ev.target as HTMLDivElement).textContent) { this.showPlaceholder(true); } else if (!this.placeholder) { - this.value = ev.target.textContent; + this.value = (ev.target as HTMLDivElement).textContent; } if (ev.key === Key.ENTER) { @@ -160,22 +149,22 @@ export default class EditableText extends React.Component { // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); }; - onClickDiv = ev => { + private onClickDiv = (): void => { if (!this.props.editable) return; this.setState({ - phase: EditableText.Phases.Edit, + phase: Phases.Edit, }); }; - onFocus = ev => { + private onFocus = (ev: React.FocusEvent): void => { //ev.target.setSelectionRange(0, ev.target.textContent.length); const node = ev.target.childNodes[0]; if (node) { const range = document.createRange(); range.setStart(node, 0); - range.setEnd(node, node.length); + range.setEnd(node, ev.target.childNodes.length); const sel = window.getSelection(); sel.removeAllRanges(); @@ -183,11 +172,15 @@ export default class EditableText extends React.Component { } }; - onFinish = (ev, shouldSubmit) => { + private onFinish = ( + ev: React.KeyboardEvent | React.FocusEvent, + shouldSubmit?: boolean, + ): void => { + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; - const submit = (ev.key === Key.ENTER) || shouldSubmit; + const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit; this.setState({ - phase: EditableText.Phases.Display, + phase: Phases.Display, }, () => { if (this.value !== this.props.initialValue) { self.onValueChanged(submit); @@ -195,7 +188,7 @@ export default class EditableText extends React.Component { }); }; - onBlur = ev => { + private onBlur = (ev: React.FocusEvent): void => { const sel = window.getSelection(); sel.removeAllRanges(); @@ -208,11 +201,11 @@ export default class EditableText extends React.Component { this.showPlaceholder(!this.value); }; - render() { + public render(): JSX.Element { const { className, editable, initialValue, label, labelClassName } = this.props; let editableEl; - if (!editable || (this.state.phase === EditableText.Phases.Display && + if (!editable || (this.state.phase === Phases.Display && (label || labelClassName) && !this.value) ) { // show the label @@ -222,7 +215,7 @@ export default class EditableText extends React.Component { } else { // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together editableEl =
Date: Thu, 16 Sep 2021 20:05:57 +0200 Subject: [PATCH 15/20] Convert AppTile to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/LeftPanelWidget.tsx | 1 - .../elements/{AppTile.js => AppTile.tsx} | 273 +++++++++--------- .../views/right_panel/WidgetCard.tsx | 1 - src/components/views/rooms/Stickerpicker.tsx | 19 +- src/stores/widgets/StopGapWidget.ts | 2 +- 5 files changed, 149 insertions(+), 147 deletions(-) rename src/components/views/elements/{AppTile.js => AppTile.tsx} (73%) diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx index 331e4283552..6b91acb5f80 100644 --- a/src/components/structures/LeftPanelWidget.tsx +++ b/src/components/structures/LeftPanelWidget.tsx @@ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => { void; + // Optional onDeleteClickHandler (overrides default behaviour) + onDeleteClick?: () => void; + // Optionally hide the tile title + showTitle?: boolean; + // Optionally handle minimise button pointer events (default false) + handleMinimisePointerEvents?: boolean; + // Optionally hide the popout widget icon + showPopout?: boolean; + // Is this an instance of a user widget + userWidget: boolean; + // sets the pointer-events property on the iframe + pointerEvents?: string; + widgetPageTitle?: string; +} + +interface IState { + initialising: boolean; // True while we are mangling the widget URL + // True while the iframe content is loading + loading: boolean; + // Assume that widget has permission to load if we are the user who + // added it to the room, or if explicitly granted by the user + hasPermissionToLoad: boolean; + error: Error; + menuDisplayed: boolean; + widgetPageTitle: string; +} @replaceableComponent("views.elements.AppTile") -export default class AppTile extends React.Component { - constructor(props) { +export default class AppTile extends React.Component { + displayName = 'AppTile'; + public static defaultProps: Partial = { + waitForIframeLoad: true, + showMenubar: true, + showTitle: true, + showPopout: true, + handleMinimisePointerEvents: false, + userWidget: false, + miniMode: false, + }; + + private contextMenuButton = createRef(); + private iframe: HTMLIFrameElement; // ref to the iframe (callback style) + private allowedWidgetsWatchRef: string; + private persistKey: string; + private sgWidget: StopGapWidget; + private dispatcherRef: string; + + constructor(props: IProps) { super(props); // The key used for PersistedElement - this._persistKey = getPersistKey(this.props.app.id); + this.persistKey = getPersistKey(this.props.app.id); try { - this._sgWidget = new StopGapWidget(this.props); - this._sgWidget.on("preparing", this._onWidgetPrepared); - this._sgWidget.on("ready", this._onWidgetReady); + this.sgWidget = new StopGapWidget(this.props); + this.sgWidget.on("preparing", this.onWidgetPrepared); + this.sgWidget.on("ready", this.onWidgetReady); } catch (e) { console.log("Failed to construct widget", e); - this._sgWidget = null; + this.sgWidget = null; } - this.iframe = null; // ref to the iframe (callback style) - this.state = this._getNewState(props); - this._contextMenuButton = createRef(); + this.state = this.getNewState(props); - this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); + this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); } // This is a function to make the impact of calling SettingsStore slightly less - hasPermissionToLoad = (props) => { - if (this._usingLocalWidget()) return true; + private hasPermissionToLoad = (props: IProps): boolean => { + if (this.usingLocalWidget()) return true; if (!props.room) return true; // user widgets always have permissions const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); @@ -81,34 +143,34 @@ export default class AppTile extends React.Component { * @param {Object} newProps The new properties of the component * @return {Object} Updated component state to be set with setState */ - _getNewState(newProps) { + private getNewState(newProps: IProps): IState { return { initialising: true, // True while we are mangling the widget URL // True while the iframe content is loading - loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), + loading: this.props.waitForIframeLoad, // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user hasPermissionToLoad: this.hasPermissionToLoad(newProps), error: null, - widgetPageTitle: newProps.widgetPageTitle, menuDisplayed: false, + widgetPageTitle: this.props.widgetPageTitle, }; } - onAllowedWidgetsChange = () => { + private onAllowedWidgetsChange = (): void => { const hasPermissionToLoad = this.hasPermissionToLoad(this.props); if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { // Force the widget to be non-persistent (able to be deleted/forgotten) ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); - PersistedElement.destroyElement(this._persistKey); - if (this._sgWidget) this._sgWidget.stop(); + PersistedElement.destroyElement(this.persistKey); + if (this.sgWidget) this.sgWidget.stop(); } this.setState({ hasPermissionToLoad }); }; - isMixedContent() { + private isMixedContent(): boolean { const parentContentProtocol = window.location.protocol; const u = url.parse(this.props.app.url); const childContentProtocol = u.protocol; @@ -120,77 +182,72 @@ export default class AppTile extends React.Component { return false; } - componentDidMount() { + public componentDidMount(): void { // Only fetch IM token on mount if we're showing and have permission to load - if (this._sgWidget && this.state.hasPermissionToLoad) { - this._startWidget(); + if (this.sgWidget && this.state.hasPermissionToLoad) { + this.startWidget(); } // Widget action listeners - this.dispatcherRef = dis.register(this._onAction); + this.dispatcherRef = dis.register(this.onAction); } - componentWillUnmount() { + public componentWillUnmount(): void { // Widget action listeners if (this.dispatcherRef) dis.unregister(this.dispatcherRef); // if it's not remaining on screen, get rid of the PersistedElement container if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); - PersistedElement.destroyElement(this._persistKey); + PersistedElement.destroyElement(this.persistKey); } - if (this._sgWidget) { - this._sgWidget.stop(); + if (this.sgWidget) { + this.sgWidget.stop(); } - SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef); + SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef); } - _resetWidget(newProps) { - if (this._sgWidget) { - this._sgWidget.stop(); + private resetWidget(newProps: IProps): void { + if (this.sgWidget) { + this.sgWidget.stop(); } try { - this._sgWidget = new StopGapWidget(newProps); - this._sgWidget.on("preparing", this._onWidgetPrepared); - this._sgWidget.on("ready", this._onWidgetReady); - this._startWidget(); + this.sgWidget = new StopGapWidget(newProps); + this.sgWidget.on("preparing", this.onWidgetPrepared); + this.sgWidget.on("ready", this.onWidgetReady); + this.startWidget(); } catch (e) { console.log("Failed to construct widget", e); - this._sgWidget = null; + this.sgWidget = null; } } - _startWidget() { - this._sgWidget.prepare().then(() => { + private startWidget(): void { + this.sgWidget.prepare().then(() => { this.setState({ initialising: false }); }); } - _iframeRefChange = (ref) => { + private iframeRefChange = (ref: HTMLIFrameElement): void => { this.iframe = ref; if (ref) { - if (this._sgWidget) this._sgWidget.start(ref); + if (this.sgWidget) this.sgWidget.start(ref); } else { - this._resetWidget(this.props); + this.resetWidget(this.props); } }; // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase + // eslint-disable-next-line @typescript-eslint/naming-convention + public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase if (nextProps.app.url !== this.props.app.url) { - this._getNewState(nextProps); + this.getNewState(nextProps); if (this.state.hasPermissionToLoad) { - this._resetWidget(nextProps); + this.resetWidget(nextProps); } } - - if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) { - this.setState({ - widgetPageTitle: nextProps.widgetPageTitle, - }); - } } /** @@ -198,7 +255,7 @@ export default class AppTile extends React.Component { * @private * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed. */ - async _endWidgetActions() { // widget migration dev note: async to maintain signature + private async endWidgetActions(): Promise { // widget migration dev note: async to maintain signature // HACK: This is a really dirty way to ensure that Jitsi cleans up // its hold on the webcam. Without this, the widget holds a media // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351 @@ -217,27 +274,27 @@ export default class AppTile extends React.Component { } // Delete the widget from the persisted store for good measure. - PersistedElement.destroyElement(this._persistKey); + PersistedElement.destroyElement(this.persistKey); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); - if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true }); + if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true }); } - _onWidgetPrepared = () => { + private onWidgetPrepared = (): void => { this.setState({ loading: false }); }; - _onWidgetReady = () => { + private onWidgetReady = (): void => { if (WidgetType.JITSI.matches(this.props.app.type)) { - this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {}); + this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {}); } }; - _onAction = payload => { + private onAction = (payload): void => { if (payload.widgetId === this.props.app.id) { switch (payload.action) { case 'm.sticker': - if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { + if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { dis.dispatch({ action: 'post_sticker_message', data: payload.data }); dis.dispatch({ action: 'stickerpicker_close' }); } else { @@ -248,7 +305,7 @@ export default class AppTile extends React.Component { } }; - _grantWidgetPermission = () => { + private grantWidgetPermission = (): void => { const roomId = this.props.room.roomId; console.info("Granting permission for widget to load: " + this.props.app.eventId); const current = SettingsStore.getValue("allowedWidgets", roomId); @@ -258,14 +315,14 @@ export default class AppTile extends React.Component { this.setState({ hasPermissionToLoad: true }); // Fetch a token for the integration manager, now that we're allowed to - this._startWidget(); + this.startWidget(); }).catch(err => { console.error(err); // We don't really need to do anything about this - the user will just hit the button again. }); }; - formatAppTileName() { + private formatAppTileName(): string { let appTileName = "No name"; if (this.props.app.name && this.props.app.name.trim()) { appTileName = this.props.app.name.trim(); @@ -278,11 +335,11 @@ export default class AppTile extends React.Component { * actual widget URL * @returns {bool} true If using a local version of the widget */ - _usingLocalWidget() { + private usingLocalWidget(): boolean { return WidgetType.JITSI.matches(this.props.app.type); } - _getTileTitle() { + private getTileTitle(): JSX.Element { const name = this.formatAppTileName(); const titleSpacer =  - ; let title = ''; @@ -300,32 +357,32 @@ export default class AppTile extends React.Component { } // TODO replace with full screen interactions - _onPopoutWidgetClick = () => { + private onPopoutWidgetClick = (): void => { // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop). if (WidgetType.JITSI.matches(this.props.app.type)) { - this._endWidgetActions().then(() => { + this.endWidgetActions().then(() => { if (this.iframe) { // Reload iframe - this.iframe.src = this._sgWidget.embedUrl; + this.iframe.src = this.sgWidget.embedUrl; } }); } // Using Object.assign workaround as the following opens in a new window instead of a new tab. // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), - { target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click(); + { target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click(); }; - _onContextMenuClick = () => { + private onContextMenuClick = (): void => { this.setState({ menuDisplayed: true }); }; - _closeContextMenu = () => { + private closeContextMenu = (): void => { this.setState({ menuDisplayed: false }); }; - render() { + public render(): JSX.Element { let appTileBody; // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin @@ -351,7 +408,7 @@ export default class AppTile extends React.Component {
); - if (this._sgWidget === null) { + if (this.sgWidget === null) { appTileBody = (
@@ -365,9 +422,9 @@ export default class AppTile extends React.Component {
); @@ -390,8 +447,8 @@ export default class AppTile extends React.Component { { this.state.loading && loadingElement }