From 6ebe6a34885c1d8eee91cdc286d60114c3c08261 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Feb 2022 17:47:26 +0000 Subject: [PATCH 1/8] Small AppTile refactor to add missing listener --- src/components/views/elements/AppTile.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index de5cd67301e..5dada74c416 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -128,10 +128,7 @@ export default class AppTile extends React.Component { this.persistKey = getPersistKey(this.props.app.id); try { this.sgWidget = new StopGapWidget(this.props); - this.sgWidget.on("preparing", this.onWidgetPreparing); - this.sgWidget.on("ready", this.onWidgetReady); - // emits when the capabilites have been setup or changed - this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified); + this.setupSgListeners(); } catch (e) { logger.log("Failed to construct widget", e); this.sgWidget = null; @@ -269,12 +266,25 @@ export default class AppTile extends React.Component { OwnProfileStore.instance.removeListener(UPDATE_EVENT, this.onUserReady); } + private setupSgListeners() { + this.sgWidget.on("preparing", this.onWidgetPreparing); + this.sgWidget.on("ready", this.onWidgetReady); + // emits when the capabilites have been setup or changed + this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified); + } + + private stopSgListeners() { + if (!this.sgWidget) return; + this.sgWidget.off("preparing", this.onWidgetPreparing); + this.sgWidget.off("ready", this.onWidgetReady); + this.sgWidget.off("capabilitiesNotified", this.onWidgetCapabilitiesNotified); + } + private resetWidget(newProps: IProps): void { this.sgWidget?.stopMessaging(); try { this.sgWidget = new StopGapWidget(newProps); - this.sgWidget.on("preparing", this.onWidgetPreparing); - this.sgWidget.on("ready", this.onWidgetReady); + this.setupSgListeners(); this.startWidget(); } catch (e) { logger.error("Failed to construct widget", e); From 997081bfdeddd51e8b91461c6ff401cad94ab322 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Feb 2022 17:48:13 +0000 Subject: [PATCH 2/8] Refactor AppTile to use MatrixClientContext --- src/components/views/elements/AppTile.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 5dada74c416..67ad2806c62 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -18,14 +18,13 @@ limitations under the License. */ import url from 'url'; -import React, { createRef } from 'react'; +import React, { ContextType, createRef } from 'react'; import classNames from 'classnames'; import { MatrixCapabilities } from "matrix-widget-api"; import { Room } from "matrix-js-sdk/src/models/room"; import { logger } from "matrix-js-sdk/src/logger"; import { EventSubscription } from 'fbemitter'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; import AccessibleButton from './AccessibleButton'; import { _t } from '../../../languageHandler'; import AppPermission from './AppPermission'; @@ -49,12 +48,13 @@ import { OwnProfileStore } from '../../../stores/OwnProfileStore'; import { UPDATE_EVENT } from '../../../stores/AsyncStore'; import RoomViewStore from '../../../stores/RoomViewStore'; import WidgetUtils from '../../../utils/WidgetUtils'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps { app: IApp; // If room is not specified then it is an account level widget // which bypasses permission prompts as it was added explicitly by that user - room: Room; + room?: Room; threadId?: string | null; // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer. // This should be set to true when there is only one widget in the app drawer, otherwise it should be false. @@ -102,6 +102,9 @@ interface IState { @replaceableComponent("views.elements.AppTile") export default class AppTile extends React.Component { + public static contextType = MatrixClientContext; + context: ContextType; + public static defaultProps: Partial = { waitForIframeLoad: true, showMenubar: true, @@ -161,10 +164,9 @@ export default class AppTile extends React.Component { }; private onWidgetLayoutChange = () => { - const room = MatrixClientPeg.get().getRoom(this.props.room.roomId); - const app = this.props.app; - const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(app.id); - const isVisibleOnScreen = WidgetLayoutStore.instance.isVisibleOnScreen(room, app.id); + const room = this.context.getRoom(this.props.room.roomId); + const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); + const isVisibleOnScreen = WidgetLayoutStore.instance.isVisibleOnScreen(room, this.props.app.id); if (!isVisibleOnScreen && !isActiveWidget) { ActiveWidgetStore.instance.destroyPersistentWidget(app.id); PersistedElement.destroyElement(this.persistKey); @@ -517,7 +519,7 @@ export default class AppTile extends React.Component { ); } else if (!this.state.hasPermissionToLoad) { // only possible for room widgets, can assert this.props.room here - const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); + const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId); appTileBody = (
Date: Fri, 4 Feb 2022 17:49:26 +0000 Subject: [PATCH 3/8] Refactor PersistentApp --- .../views/elements/PersistentApp.tsx | 66 +++++++------------ 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 8c207cd5186..2961d7df01f 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -15,73 +15,51 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import { Room } from "matrix-js-sdk/src/models/room"; +import React, { ContextType } from 'react'; -import RoomViewStore from '../../../stores/RoomViewStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import WidgetUtils from '../../../utils/WidgetUtils'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AppTile from "./AppTile"; +import { IApp } from '../../../stores/WidgetStore'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps { persistentWidgetId: string; pointerEvents?: string; } -interface IState { - roomId: string; -} - @replaceableComponent("views.elements.PersistentApp") -export default class PersistentApp extends React.Component { - constructor(props: IProps) { - super(props); +export default class PersistentApp extends React.Component { + public static contextType = MatrixClientContext; + context: ContextType; - this.state = { - roomId: RoomViewStore.getRoomId(), - }; - } - - public componentDidMount(): void { - MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); - } + private get app(): IApp { + const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.props.persistentWidgetId); + const persistentWidgetInRoom = this.context.getRoom(persistentWidgetInRoomId); - public componentWillUnmount(): void { - MatrixClientPeg.get().off("Room.myMembership", this.onMyMembership); + // get the widget data + const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { + return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId(); + }); + return WidgetUtils.makeAppConfig( + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), + persistentWidgetInRoomId, appEvent.getId(), + ); } - private onMyMembership = async (room: Room, membership: string): Promise => { - const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.props.persistentWidgetId); - if (membership !== "join") { - // we're not in the room anymore - delete - if (room.roomId === persistentWidgetInRoomId) { - ActiveWidgetStore.instance.destroyPersistentWidget(this.props.persistentWidgetId); - } - } - }; - public render(): JSX.Element { - const wId = this.props.persistentWidgetId; - if (wId) { - const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(wId); - const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); + const app = this.app; + if (app) { + const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.props.persistentWidgetId); + const persistentWidgetInRoom = this.context.getRoom(persistentWidgetInRoomId); - // get the widget data - const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { - return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId(); - }); - const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), - persistentWidgetInRoomId, appEvent.getId(), - ); return Date: Fri, 4 Feb 2022 17:49:45 +0000 Subject: [PATCH 4/8] Show PIP regardless of being in room to prevent invisible iframes recording you --- src/components/views/voip/PipView.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/voip/PipView.tsx b/src/components/views/voip/PipView.tsx index 395763b157f..ebe4649f9f7 100644 --- a/src/components/views/voip/PipView.tsx +++ b/src/components/views/voip/PipView.tsx @@ -236,7 +236,6 @@ export default class PipView extends React.Component { // Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId public updateShowWidgetInPip(persistentWidgetId = this.state.persistentWidgetId) { - let userIsPartOfTheRoom = false; let fromAnotherRoom = false; let notVisible = false; if (persistentWidgetId) { @@ -248,16 +247,13 @@ export default class PipView extends React.Component { if (persistentWidgetInRoom) { const wls = WidgetLayoutStore.instance; notVisible = !wls.isVisibleOnScreen(persistentWidgetInRoom, persistentWidgetId); - userIsPartOfTheRoom = persistentWidgetInRoom.getMyMembership() == "join"; fromAnotherRoom = this.state.viewedRoomId !== persistentWidgetInRoomId; } } // The widget should only be shown as a persistent app (in a floating pip container) if it is not visible on screen // either, because we are viewing a different room OR because it is in none of the possible containers of the room view. - const showWidgetInPip = - (fromAnotherRoom && userIsPartOfTheRoom) || - (notVisible && userIsPartOfTheRoom); + const showWidgetInPip = fromAnotherRoom || notVisible; this.setState({ showWidgetInPip, persistentWidgetId }); } From fe89777b2dfc8eba99aef3b711c99616087e7f78 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Feb 2022 17:50:28 +0000 Subject: [PATCH 5/8] Fix StopGapWidget typing and prevent multiple destroys --- src/stores/widgets/StopGapWidget.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 5d1f1412182..3d09be835fe 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -64,9 +64,8 @@ import { arrayFastClone } from "../../utils/arrays"; interface IAppTileProps { // Note: these are only the props we care about - app: IWidget; - room: Room; + room?: Room; // without a room it is a user widget userId: string; creatorUserId: string; waitForIframeLoad: boolean; @@ -423,6 +422,7 @@ export class StopGapWidget extends EventEmitter { if (!this.started) return; WidgetMessagingStore.instance.stopMessaging(this.mockWidget); ActiveWidgetStore.instance.delRoomId(this.mockWidget.id); + this.messaging = null; if (MatrixClientPeg.get()) { MatrixClientPeg.get().off('event', this.onEvent); From d2c8f794854c75b006b4a9235c8ebec5e595112f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Feb 2022 18:14:44 +0000 Subject: [PATCH 6/8] Properly handle persistent widgets when room is left --- src/components/views/elements/AppTile.tsx | 114 +++++++++++++++------- 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 67ad2806c62..57302a95139 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -49,6 +49,7 @@ import { UPDATE_EVENT } from '../../../stores/AsyncStore'; import RoomViewStore from '../../../stores/RoomViewStore'; import WidgetUtils from '../../../utils/WidgetUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ActionPayload } from "../../../dispatcher/payloads"; interface IProps { app: IApp; @@ -168,21 +169,37 @@ export default class AppTile extends React.Component { const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); const isVisibleOnScreen = WidgetLayoutStore.instance.isVisibleOnScreen(room, this.props.app.id); if (!isVisibleOnScreen && !isActiveWidget) { - ActiveWidgetStore.instance.destroyPersistentWidget(app.id); - PersistedElement.destroyElement(this.persistKey); - this.sgWidget?.stopMessaging(); + this.endWidgetActions(); } }; private onRoomViewStoreUpdate = () => { if (this.props.room.roomId == RoomViewStore.getRoomId()) return; - const app = this.props.app; - const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(app.id); + const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); // Stop the widget if it's not the active (persistent) widget and it's not a user widget if (!isActiveWidget && !this.props.userWidget) { - ActiveWidgetStore.instance.destroyPersistentWidget(app.id); - PersistedElement.destroyElement(this.persistKey); - this.sgWidget?.stopMessaging(); + this.endWidgetActions(); + } + }; + + private onUserLeftRoom() { + const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); + if (isActiveWidget) { + // We just left the room that the active widget was from. If this was a Jitsi then reload to end call. + // Otherwise if we are not actively looking at the room then destroy this widget entirely. + if (WidgetType.JITSI.matches(this.props.app.type)) { + this.reload(); + } else if (RoomViewStore.getRoomId() !== this.props.room.roomId) { + this.endWidgetActions(); + } else { + ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); + } + } + } + + private onMyMembership = (room: Room, membership: string): void => { + if (membership === "leave" && room.roomId === this.props.room.roomId) { + this.onUserLeftRoom(); } }; @@ -246,6 +263,7 @@ export default class AppTile extends React.Component { if (this.props.room) { const emitEvent = WidgetLayoutStore.emissionForRoom(this.props.room); WidgetLayoutStore.instance.on(emitEvent, this.onWidgetLayoutChange); + this.context.on("Room.myMembership", this.onMyMembership); } this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); @@ -261,6 +279,7 @@ export default class AppTile extends React.Component { if (this.props.room) { const emitEvent = WidgetLayoutStore.emissionForRoom(this.props.room); WidgetLayoutStore.instance.off(emitEvent, this.onWidgetLayoutChange); + this.context.off("Room.myMembership", this.onMyMembership); } this.roomStoreToken?.remove(); @@ -284,6 +303,8 @@ export default class AppTile extends React.Component { private resetWidget(newProps: IProps): void { this.sgWidget?.stopMessaging(); + this.stopSgListeners(); + try { this.sgWidget = new StopGapWidget(newProps); this.setupSgListeners(); @@ -300,14 +321,18 @@ export default class AppTile extends React.Component { }); } + private startMessaging() { + try { + this.sgWidget?.startMessaging(this.iframe); + } catch (e) { + logger.error("Failed to start widget", e); + } + } + private iframeRefChange = (ref: HTMLIFrameElement): void => { this.iframe = ref; if (ref) { - try { - this.sgWidget?.startMessaging(ref); - } catch (e) { - logger.error("Failed to start widget", e); - } + this.startMessaging(); } else { this.resetWidget(this.props); } @@ -376,24 +401,31 @@ export default class AppTile extends React.Component { }); }; - private onAction = (payload): void => { - if (payload.widgetId === this.props.app.id) { - switch (payload.action) { - case 'm.sticker': - if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { - dis.dispatch({ - action: 'post_sticker_message', - data: { - ...payload.data, - threadId: this.props.threadId, - }, - }); - dis.dispatch({ action: 'stickerpicker_close' }); - } else { - logger.warn('Ignoring sticker message. Invalid capability'); - } - break; - } + private onAction = (payload: ActionPayload): void => { + switch (payload.action) { + case 'm.sticker': + if (payload.widgetId === this.props.app.id && + this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending) + ) { + dis.dispatch({ + action: 'post_sticker_message', + data: { + ...payload.data, + threadId: this.props.threadId, + }, + }); + dis.dispatch({ action: 'stickerpicker_close' }); + } else { + logger.warn('Ignoring sticker message. Invalid capability'); + } + break; + + case "after_leave_room": + if (payload.room_id === this.props.room?.roomId) { + // call this before we get it echoed down /sync, so it doesn't hang around as long and look jarring + this.onUserLeftRoom(); + } + break; } }; @@ -448,17 +480,25 @@ export default class AppTile extends React.Component { ); } + private reload() { + this.endWidgetActions().then(() => { + // reset messaging + this.resetWidget(this.props); + this.startMessaging(); + + if (this.iframe) { + // Reload iframe + this.iframe.src = this.sgWidget.embedUrl; + } + }); + } + // TODO replace with full screen interactions 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(() => { - if (this.iframe) { - // Reload iframe - this.iframe.src = this.sgWidget.embedUrl; - } - }); + this.reload(); } // Using Object.assign workaround as the following opens in a new window instead of a new tab. // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); From f3d6ee376f9672b9693509be8ff6a89bdd2f4894 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Feb 2022 18:21:56 +0000 Subject: [PATCH 7/8] Update logic --- src/components/views/elements/AppTile.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 57302a95139..3147fe7b3f9 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -185,13 +185,15 @@ export default class AppTile extends React.Component { private onUserLeftRoom() { const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); if (isActiveWidget) { - // We just left the room that the active widget was from. If this was a Jitsi then reload to end call. - // Otherwise if we are not actively looking at the room then destroy this widget entirely. - if (WidgetType.JITSI.matches(this.props.app.type)) { - this.reload(); - } else if (RoomViewStore.getRoomId() !== this.props.room.roomId) { + // We just left the room that the active widget was from. + if (RoomViewStore.getRoomId() !== this.props.room.roomId) { + // If we are not actively looking at the room then destroy this widget entirely. this.endWidgetActions(); + } else if (WidgetType.JITSI.matches(this.props.app.type)) { + // If this was a Jitsi then reload to end call. + this.reload(); } else { + // Otherwise just cancel its persistence. ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); } } From 5cde0810c0e75060344c71dbd150750b5047f5ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Feb 2022 14:32:55 +0000 Subject: [PATCH 8/8] get rid of redundant room lookup --- src/components/views/elements/AppTile.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 3147fe7b3f9..ae66bcab00b 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -165,9 +165,8 @@ export default class AppTile extends React.Component { }; private onWidgetLayoutChange = () => { - const room = this.context.getRoom(this.props.room.roomId); const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); - const isVisibleOnScreen = WidgetLayoutStore.instance.isVisibleOnScreen(room, this.props.app.id); + const isVisibleOnScreen = WidgetLayoutStore.instance.isVisibleOnScreen(this.props.room, this.props.app.id); if (!isVisibleOnScreen && !isActiveWidget) { this.endWidgetActions(); }