From e1445f71bd9da37f3dd61ea8f895fa8ab159f64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 16 Sep 2021 20:05:57 +0200 Subject: [PATCH] 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 331e428355..6b91acb5f8 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 }