Don't assume that widget IDs are unique (#8052)
* Don't assume that widget IDs are unique Signed-off-by: Robin Townsend <robin@robin.town> * Don't remove live tiles that don't exist Signed-off-by: Robin Townsend <robin@robin.town> * Add unit test for AppTile's live tile tracking Signed-off-by: Robin Townsend <robin@robin.town>pull/21833/head
							parent
							
								
									bc8fdac491
								
							
						
					
					
						commit
						744eeb53fe
					
				|  | @ -1123,7 +1123,7 @@ export default class CallHandler extends EventEmitter { | |||
| 
 | ||||
|         const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); | ||||
|         jitsiWidgets.forEach(w => { | ||||
|             const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id); | ||||
|             const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(w)); | ||||
|             if (!messaging) return; // more "should never happen" words
 | ||||
| 
 | ||||
|             messaging.transport.send(ElementWidgetActions.HangupCall, {}); | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({ | |||
|     const cli = useContext(MatrixClientContext); | ||||
|     const { room, roomId } = useContext(RoomContext); | ||||
| 
 | ||||
|     const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id); | ||||
|     const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app)); | ||||
|     const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId); | ||||
| 
 | ||||
|     let streamAudioStreamButton; | ||||
|  |  | |||
|  | @ -118,24 +118,27 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
|         showLayoutButtons: true, | ||||
|     }; | ||||
| 
 | ||||
|     // We track a count of all "live" `AppTile`s for a given widget ID.
 | ||||
|     // We track a count of all "live" `AppTile`s for a given widget UID.
 | ||||
|     // For this purpose, an `AppTile` is considered live from the time it is
 | ||||
|     // constructed until it is unmounted. This is used to aid logic around when
 | ||||
|     // to tear down the widget iframe. See `componentWillUnmount` for details.
 | ||||
|     private static liveTilesById: { [key: string]: number } = {}; | ||||
|     private static liveTilesByUid = new Map<string, number>(); | ||||
| 
 | ||||
|     public static addLiveTile(widgetId: string): void { | ||||
|         const refs = this.liveTilesById[widgetId] || 0; | ||||
|         this.liveTilesById[widgetId] = refs + 1; | ||||
|     public static addLiveTile(widgetId: string, roomId: string): void { | ||||
|         const uid = WidgetUtils.calcWidgetUid(widgetId, roomId); | ||||
|         const refs = this.liveTilesByUid.get(uid) ?? 0; | ||||
|         this.liveTilesByUid.set(uid, refs + 1); | ||||
|     } | ||||
| 
 | ||||
|     public static removeLiveTile(widgetId: string): void { | ||||
|         const refs = this.liveTilesById[widgetId] || 0; | ||||
|         this.liveTilesById[widgetId] = refs - 1; | ||||
|     public static removeLiveTile(widgetId: string, roomId: string): void { | ||||
|         const uid = WidgetUtils.calcWidgetUid(widgetId, roomId); | ||||
|         const refs = this.liveTilesByUid.get(uid); | ||||
|         if (refs) this.liveTilesByUid.set(uid, refs - 1); | ||||
|     } | ||||
| 
 | ||||
|     public static isLive(widgetId: string): boolean { | ||||
|         const refs = this.liveTilesById[widgetId] || 0; | ||||
|     public static isLive(widgetId: string, roomId: string): boolean { | ||||
|         const uid = WidgetUtils.calcWidgetUid(widgetId, roomId); | ||||
|         const refs = this.liveTilesByUid.get(uid) ?? 0; | ||||
|         return refs > 0; | ||||
|     } | ||||
| 
 | ||||
|  | @ -150,10 +153,10 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         AppTile.addLiveTile(this.props.app.id); | ||||
|         AppTile.addLiveTile(this.props.app.id, this.props.app.roomId); | ||||
| 
 | ||||
|         // The key used for PersistedElement
 | ||||
|         this.persistKey = getPersistKey(this.props.app.id); | ||||
|         this.persistKey = getPersistKey(WidgetUtils.getWidgetUid(this.props.app)); | ||||
|         try { | ||||
|             this.sgWidget = new StopGapWidget(this.props); | ||||
|             this.setupSgListeners(); | ||||
|  | @ -189,7 +192,9 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
|     }; | ||||
| 
 | ||||
|     private onUserLeftRoom() { | ||||
|         const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); | ||||
|         const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence( | ||||
|             this.props.app.id, this.props.app.roomId, | ||||
|         ); | ||||
|         if (isActiveWidget) { | ||||
|             // We just left the room that the active widget was from.
 | ||||
|             if (this.props.room && RoomViewStore.getRoomId() !== this.props.room.roomId) { | ||||
|  | @ -200,7 +205,7 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
|                 this.reload(); | ||||
|             } else { | ||||
|                 // Otherwise just cancel its persistence.
 | ||||
|                 ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); | ||||
|                 ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id, this.props.app.roomId); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -241,7 +246,7 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { | ||||
|             // Force the widget to be non-persistent (able to be deleted/forgotten)
 | ||||
|             ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); | ||||
|             ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id, this.props.app.roomId); | ||||
|             PersistedElement.destroyElement(this.persistKey); | ||||
|             this.sgWidget?.stopMessaging(); | ||||
|         } | ||||
|  | @ -291,14 +296,16 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
|         // container is constructed before the old one unmounts. By counting the
 | ||||
|         // mounted `AppTile`s for each widget, we know to only tear down the
 | ||||
|         // widget iframe when the last the `AppTile` unmounts.
 | ||||
|         AppTile.removeLiveTile(this.props.app.id); | ||||
|         AppTile.removeLiveTile(this.props.app.id, this.props.app.roomId); | ||||
| 
 | ||||
|         // We also support a separate "persistence" mode where a single widget
 | ||||
|         // can request to be "sticky" and follow you across rooms in a PIP
 | ||||
|         // container.
 | ||||
|         const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id); | ||||
|         const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence( | ||||
|             this.props.app.id, this.props.app.roomId, | ||||
|         ); | ||||
| 
 | ||||
|         if (!AppTile.isLive(this.props.app.id) && !isActiveWidget) { | ||||
|         if (!AppTile.isLive(this.props.app.id, this.props.app.roomId) && !isActiveWidget) { | ||||
|             this.endWidgetActions(); | ||||
|         } | ||||
| 
 | ||||
|  | @ -408,7 +415,7 @@ export default class AppTile extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         // Delete the widget from the persisted store for good measure.
 | ||||
|         PersistedElement.destroyElement(this.persistKey); | ||||
|         ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); | ||||
|         ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id, this.props.app.roomId); | ||||
| 
 | ||||
|         this.sgWidget?.stopMessaging({ forceDestroy: true }); | ||||
|     } | ||||
|  |  | |||
|  | @ -16,8 +16,8 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React, { ContextType } from 'react'; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| 
 | ||||
| import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; | ||||
| import WidgetUtils from '../../../utils/WidgetUtils'; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import AppTile from "./AppTile"; | ||||
|  | @ -26,6 +26,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; | |||
| 
 | ||||
| interface IProps { | ||||
|     persistentWidgetId: string; | ||||
|     persistentRoomId: string; | ||||
|     pointerEvents?: string; | ||||
| } | ||||
| 
 | ||||
|  | @ -33,32 +34,32 @@ interface IProps { | |||
| export default class PersistentApp extends React.Component<IProps> { | ||||
|     public static contextType = MatrixClientContext; | ||||
|     context: ContextType<typeof MatrixClientContext>; | ||||
|     private room: Room; | ||||
| 
 | ||||
|     constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) { | ||||
|         super(props, context); | ||||
|         this.room = context.getRoom(this.props.persistentRoomId); | ||||
|     } | ||||
| 
 | ||||
|     private get app(): IApp { | ||||
|         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 appEvent = WidgetUtils.getRoomWidgets(this.room).find(ev => | ||||
|             ev.getStateKey() === this.props.persistentWidgetId, | ||||
|         ); | ||||
|         return WidgetUtils.makeAppConfig( | ||||
|             appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), | ||||
|             persistentWidgetInRoomId, appEvent.getId(), | ||||
|             this.room.roomId, appEvent.getId(), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public render(): JSX.Element { | ||||
|         const app = this.app; | ||||
|         if (app) { | ||||
|             const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.props.persistentWidgetId); | ||||
|             const persistentWidgetInRoom = this.context.getRoom(persistentWidgetInRoomId); | ||||
| 
 | ||||
|             return <AppTile | ||||
|                 key={app.id} | ||||
|                 app={app} | ||||
|                 fullWidth={true} | ||||
|                 room={persistentWidgetInRoom} | ||||
|                 room={this.room} | ||||
|                 userId={this.context.credentials.userId} | ||||
|                 creatorUserId={app.creatorUserId} | ||||
|                 widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} | ||||
|  |  | |||
|  | @ -233,7 +233,9 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> { | |||
| 
 | ||||
|     private sendVisibilityToWidget(visible: boolean): void { | ||||
|         if (!this.state.stickerpickerWidget) return; | ||||
|         const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id); | ||||
|         const messaging = WidgetMessagingStore.instance.getMessagingForUid( | ||||
|             WidgetUtils.calcWidgetUid(this.state.stickerpickerWidget.id, null), | ||||
|         ); | ||||
|         if (messaging && visible !== this.prevSentVisibility) { | ||||
|             messaging.updateVisibility(visible).catch(err => { | ||||
|                 logger.error("Error updating widget visibility: ", err); | ||||
|  |  | |||
|  | @ -60,6 +60,7 @@ interface IState { | |||
| 
 | ||||
|     // widget candidate to be displayed in the pip view.
 | ||||
|     persistentWidgetId: string; | ||||
|     persistentRoomId: string; | ||||
|     showWidgetInPip: boolean; | ||||
| 
 | ||||
|     moving: boolean; | ||||
|  | @ -122,6 +123,7 @@ export default class PipView extends React.Component<IProps, IState> { | |||
|             primaryCall: primaryCall, | ||||
|             secondaryCall: secondaryCalls[0], | ||||
|             persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(), | ||||
|             persistentRoomId: ActiveWidgetStore.instance.getPersistentRoomId(), | ||||
|             showWidgetInPip: false, | ||||
|         }; | ||||
|     } | ||||
|  | @ -187,7 +189,10 @@ export default class PipView extends React.Component<IProps, IState> { | |||
|     }; | ||||
| 
 | ||||
|     private onActiveWidgetStoreUpdate = (): void => { | ||||
|         this.updateShowWidgetInPip(ActiveWidgetStore.instance.getPersistentWidgetId()); | ||||
|         this.updateShowWidgetInPip( | ||||
|             ActiveWidgetStore.instance.getPersistentWidgetId(), | ||||
|             ActiveWidgetStore.instance.getPersistentRoomId(), | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     private updateCalls = (): void => { | ||||
|  | @ -213,30 +218,27 @@ export default class PipView extends React.Component<IProps, IState> { | |||
| 
 | ||||
|     private onDoubleClick = (): void => { | ||||
|         const callRoomId = this.state.primaryCall?.roomId; | ||||
|         const widgetRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId); | ||||
|         if (!!(callRoomId ?? widgetRoomId)) { | ||||
|         if (callRoomId ?? this.state.persistentRoomId) { | ||||
|             dis.dispatch<ViewRoomPayload>({ | ||||
|                 action: Action.ViewRoom, | ||||
|                 room_id: callRoomId ?? widgetRoomId, | ||||
|                 room_id: callRoomId ?? this.state.persistentRoomId, | ||||
|                 metricsTrigger: "WebFloatingCallWindow", | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     // Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
 | ||||
|     public updateShowWidgetInPip(persistentWidgetId = this.state.persistentWidgetId) { | ||||
|     public updateShowWidgetInPip( | ||||
|         persistentWidgetId = this.state.persistentWidgetId, | ||||
|         persistentRoomId = this.state.persistentRoomId, | ||||
|     ) { | ||||
|         let fromAnotherRoom = false; | ||||
|         let notVisible = false; | ||||
|         if (persistentWidgetId) { | ||||
|             const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(persistentWidgetId); | ||||
|             const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); | ||||
| 
 | ||||
|             // Sanity check the room - the widget may have been destroyed between render cycles, and
 | ||||
|             // thus no room is associated anymore.
 | ||||
|             if (persistentWidgetInRoom) { | ||||
|                 notVisible = !AppTile.isLive(persistentWidgetId); | ||||
|                 fromAnotherRoom = this.state.viewedRoomId !== persistentWidgetInRoomId; | ||||
|             } | ||||
|         // Sanity check the room - the widget may have been destroyed between render cycles, and
 | ||||
|         // thus no room is associated anymore.
 | ||||
|         if (persistentWidgetId && MatrixClientPeg.get().getRoom(persistentRoomId)) { | ||||
|             notVisible = !AppTile.isLive(persistentWidgetId, persistentRoomId); | ||||
|             fromAnotherRoom = this.state.viewedRoomId !== persistentRoomId; | ||||
|         } | ||||
| 
 | ||||
|         // The widget should only be shown as a persistent app (in a floating
 | ||||
|  | @ -245,7 +247,7 @@ export default class PipView extends React.Component<IProps, IState> { | |||
|         // containers of the room view.
 | ||||
|         const showWidgetInPip = fromAnotherRoom || notVisible; | ||||
| 
 | ||||
|         this.setState({ showWidgetInPip, persistentWidgetId }); | ||||
|         this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); | ||||
|     } | ||||
| 
 | ||||
|     public render() { | ||||
|  | @ -269,8 +271,7 @@ export default class PipView extends React.Component<IProps, IState> { | |||
|                 mx_CallView_pip: pipMode, | ||||
|                 mx_CallView_large: !pipMode, | ||||
|             }); | ||||
|             const roomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId); | ||||
|             const roomForWidget = MatrixClientPeg.get().getRoom(roomId); | ||||
|             const roomForWidget = MatrixClientPeg.get().getRoom(this.state.persistentRoomId); | ||||
| 
 | ||||
|             pipContent = ({ onStartMoving, _onResize }) => | ||||
|                 <div className={pipViewClasses}> | ||||
|  | @ -281,6 +282,7 @@ export default class PipView extends React.Component<IProps, IState> { | |||
|                     /> | ||||
|                     <PersistentApp | ||||
|                         persistentWidgetId={this.state.persistentWidgetId} | ||||
|                         persistentRoomId={this.state.persistentRoomId} | ||||
|                         pointerEvents={this.state.moving ? 'none' : undefined} | ||||
|                     /> | ||||
|                 </div>; | ||||
|  |  | |||
|  | @ -16,8 +16,10 @@ limitations under the License. | |||
| 
 | ||||
| import EventEmitter from 'events'; | ||||
| import { MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; | ||||
| import { RoomState } from "matrix-js-sdk/src/models/room-state"; | ||||
| 
 | ||||
| import { MatrixClientPeg } from '../MatrixClientPeg'; | ||||
| import WidgetUtils from "../utils/WidgetUtils"; | ||||
| import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore"; | ||||
| 
 | ||||
| export enum ActiveWidgetStoreEvent { | ||||
|  | @ -33,8 +35,7 @@ export enum ActiveWidgetStoreEvent { | |||
| export default class ActiveWidgetStore extends EventEmitter { | ||||
|     private static internalInstance: ActiveWidgetStore; | ||||
|     private persistentWidgetId: string; | ||||
|     // What room ID each widget is associated with (if it's a room widget)
 | ||||
|     private roomIdByWidgetId = new Map<string, string>(); | ||||
|     private persistentRoomId: string; | ||||
| 
 | ||||
|     public static get instance(): ActiveWidgetStore { | ||||
|         if (!ActiveWidgetStore.internalInstance) { | ||||
|  | @ -48,64 +49,49 @@ export default class ActiveWidgetStore extends EventEmitter { | |||
|     } | ||||
| 
 | ||||
|     public stop(): void { | ||||
|         if (MatrixClientPeg.get()) { | ||||
|             MatrixClientPeg.get().removeListener(RoomStateEvent.Events, this.onRoomStateEvents); | ||||
|         } | ||||
|         this.roomIdByWidgetId.clear(); | ||||
|         MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); | ||||
|     } | ||||
| 
 | ||||
|     private onRoomStateEvents = (ev: MatrixEvent): void => { | ||||
|     private onRoomStateEvents = (ev: MatrixEvent, { roomId }: RoomState): void => { | ||||
|         // XXX: This listens for state events in order to remove the active widget.
 | ||||
|         // Everything else relies on views listening for events and calling setters
 | ||||
|         // on this class which is terrible. This store should just listen for events
 | ||||
|         // and keep itself up to date.
 | ||||
|         // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
 | ||||
|         if (ev.getType() !== 'im.vector.modular.widgets') return; | ||||
| 
 | ||||
|         if (ev.getStateKey() === this.persistentWidgetId) { | ||||
|             this.destroyPersistentWidget(this.persistentWidgetId); | ||||
|         if (ev.getType() === "im.vector.modular.widgets") { | ||||
|             this.destroyPersistentWidget(ev.getStateKey(), roomId); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     public destroyPersistentWidget(id: string): void { | ||||
|         if (id !== this.persistentWidgetId) return; | ||||
|         const toDeleteId = this.persistentWidgetId; | ||||
| 
 | ||||
|         WidgetMessagingStore.instance.stopMessagingById(id); | ||||
| 
 | ||||
|         this.setWidgetPersistence(toDeleteId, false); | ||||
|         this.delRoomId(toDeleteId); | ||||
|     public destroyPersistentWidget(widgetId: string, roomId: string): void { | ||||
|         if (!this.getWidgetPersistence(widgetId, roomId)) return; | ||||
|         WidgetMessagingStore.instance.stopMessagingByUid(WidgetUtils.calcWidgetUid(widgetId, roomId)); | ||||
|         this.setWidgetPersistence(widgetId, roomId, false); | ||||
|     } | ||||
| 
 | ||||
|     public setWidgetPersistence(widgetId: string, val: boolean): void { | ||||
|         if (this.persistentWidgetId === widgetId && !val) { | ||||
|     public setWidgetPersistence(widgetId: string, roomId: string, val: boolean): void { | ||||
|         const isPersisted = this.getWidgetPersistence(widgetId, roomId); | ||||
| 
 | ||||
|         if (isPersisted && !val) { | ||||
|             this.persistentWidgetId = null; | ||||
|         } else if (this.persistentWidgetId !== widgetId && val) { | ||||
|             this.persistentRoomId = null; | ||||
|         } else if (!isPersisted && val) { | ||||
|             this.persistentWidgetId = widgetId; | ||||
|             this.persistentRoomId = roomId; | ||||
|         } | ||||
|         this.emit(ActiveWidgetStoreEvent.Update); | ||||
|     } | ||||
| 
 | ||||
|     public getWidgetPersistence(widgetId: string): boolean { | ||||
|         return this.persistentWidgetId === widgetId; | ||||
|     public getWidgetPersistence(widgetId: string, roomId: string): boolean { | ||||
|         return this.persistentWidgetId === widgetId && this.persistentRoomId === roomId; | ||||
|     } | ||||
| 
 | ||||
|     public getPersistentWidgetId(): string { | ||||
|         return this.persistentWidgetId; | ||||
|     } | ||||
| 
 | ||||
|     public getRoomId(widgetId: string): string { | ||||
|         return this.roomIdByWidgetId.get(widgetId); | ||||
|     } | ||||
| 
 | ||||
|     public setRoomId(widgetId: string, roomId: string): void { | ||||
|         this.roomIdByWidgetId.set(widgetId, roomId); | ||||
|         this.emit(ActiveWidgetStoreEvent.Update); | ||||
|     } | ||||
| 
 | ||||
|     public delRoomId(widgetId: string): void { | ||||
|         this.roomIdByWidgetId.delete(widgetId); | ||||
|         this.emit(ActiveWidgetStoreEvent.Update); | ||||
|     public getPersistentRoomId(): string { | ||||
|         return this.persistentRoomId; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> { | |||
|     private static internalInstance = new ModalWidgetStore(); | ||||
|     private modalInstance: IHandle<void[]> = null; | ||||
|     private openSourceWidgetId: string = null; | ||||
|     private openSourceWidgetRoomId: string = null; | ||||
| 
 | ||||
|     private constructor() { | ||||
|         super(defaultDispatcher, {}); | ||||
|  | @ -57,31 +58,38 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> { | |||
|     ) => { | ||||
|         if (this.modalInstance) return; | ||||
|         this.openSourceWidgetId = sourceWidget.id; | ||||
|         this.openSourceWidgetRoomId = widgetRoomId; | ||||
|         this.modalInstance = Modal.createTrackedDialog('Modal Widget', '', ModalWidgetDialog, { | ||||
|             widgetDefinition: { ...requestData }, | ||||
|             widgetRoomId, | ||||
|             sourceWidgetId: sourceWidget.id, | ||||
|             onFinished: (success: boolean, data?: IModalWidgetReturnData) => { | ||||
|                 if (!success) { | ||||
|                     this.closeModalWidget(sourceWidget, { "m.exited": true }); | ||||
|                     this.closeModalWidget(sourceWidget, widgetRoomId, { "m.exited": true }); | ||||
|                 } else { | ||||
|                     this.closeModalWidget(sourceWidget, data); | ||||
|                     this.closeModalWidget(sourceWidget, widgetRoomId, data); | ||||
|                 } | ||||
| 
 | ||||
|                 this.openSourceWidgetId = null; | ||||
|                 this.openSourceWidgetRoomId = null; | ||||
|                 this.modalInstance = null; | ||||
|             }, | ||||
|         }, null, /* priority = */ false, /* static = */ true); | ||||
|     }; | ||||
| 
 | ||||
|     public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => { | ||||
|     public closeModalWidget = ( | ||||
|         sourceWidget: Widget, | ||||
|         widgetRoomId?: string, | ||||
|         data?: IModalWidgetReturnData, | ||||
|     ) => { | ||||
|         if (!this.modalInstance) return; | ||||
|         if (this.openSourceWidgetId === sourceWidget.id) { | ||||
|         if (this.openSourceWidgetId === sourceWidget.id && this.openSourceWidgetRoomId === widgetRoomId) { | ||||
|             this.openSourceWidgetId = null; | ||||
|             this.openSourceWidgetRoomId = null; | ||||
|             this.modalInstance.close(); | ||||
|             this.modalInstance = null; | ||||
| 
 | ||||
|             const sourceMessaging = WidgetMessagingStore.instance.getMessaging(sourceWidget); | ||||
|             const sourceMessaging = WidgetMessagingStore.instance.getMessaging(sourceWidget, widgetRoomId); | ||||
|             if (!sourceMessaging) { | ||||
|                 logger.error("No source widget messaging for modal widget"); | ||||
|                 return; | ||||
|  |  | |||
|  | @ -29,7 +29,6 @@ import ActiveWidgetStore from "../stores/ActiveWidgetStore"; | |||
| import WidgetUtils from "../utils/WidgetUtils"; | ||||
| import { WidgetType } from "../widgets/WidgetType"; | ||||
| import { UPDATE_EVENT } from "./AsyncStore"; | ||||
| import { MatrixClientPeg } from "../MatrixClientPeg"; | ||||
| 
 | ||||
| interface IState {} | ||||
| 
 | ||||
|  | @ -44,10 +43,6 @@ interface IRoomWidgets { | |||
|     widgets: IApp[]; | ||||
| } | ||||
| 
 | ||||
| function widgetUid(app: IApp): string { | ||||
|     return `${app.roomId ?? MatrixClientPeg.get().getUserId()}::${app.id}`; | ||||
| } | ||||
| 
 | ||||
| // TODO consolidate WidgetEchoStore into this
 | ||||
| // TODO consolidate ActiveWidgetStore into this
 | ||||
| export default class WidgetStore extends AsyncStoreWithClient<IState> { | ||||
|  | @ -119,13 +114,13 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|         // otherwise we are out of sync with the rest of the app with stale widget events during removal
 | ||||
|         Array.from(this.widgetMap.values()).forEach(app => { | ||||
|             if (app.roomId !== room.roomId) return; // skip - wrong room
 | ||||
|             this.widgetMap.delete(widgetUid(app)); | ||||
|             this.widgetMap.delete(WidgetUtils.getWidgetUid(app)); | ||||
|         }); | ||||
| 
 | ||||
|         let edited = false; | ||||
|         this.generateApps(room).forEach(app => { | ||||
|             // Sanity check for https://github.com/vector-im/element-web/issues/15705
 | ||||
|             const existingApp = this.widgetMap.get(widgetUid(app)); | ||||
|             const existingApp = this.widgetMap.get(WidgetUtils.getWidgetUid(app)); | ||||
|             if (existingApp) { | ||||
|                 logger.warn( | ||||
|                     `Possible widget ID conflict for ${app.id} - wants to store in room ${app.roomId} ` + | ||||
|  | @ -133,7 +128,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             this.widgetMap.set(widgetUid(app), app); | ||||
|             this.widgetMap.set(WidgetUtils.getWidgetUid(app), app); | ||||
|             roomInfo.widgets.push(app); | ||||
|             edited = true; | ||||
|         }); | ||||
|  | @ -144,14 +139,13 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|         // If a persistent widget is active, check to see if it's just been removed.
 | ||||
|         // If it has, it needs to destroyed otherwise unmounting the node won't kill it
 | ||||
|         const persistentWidgetId = ActiveWidgetStore.instance.getPersistentWidgetId(); | ||||
|         if (persistentWidgetId) { | ||||
|             if ( | ||||
|                 ActiveWidgetStore.instance.getRoomId(persistentWidgetId) === room.roomId && | ||||
|                 !roomInfo.widgets.some(w => w.id === persistentWidgetId) | ||||
|             ) { | ||||
|                 logger.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`); | ||||
|                 ActiveWidgetStore.instance.destroyPersistentWidget(persistentWidgetId); | ||||
|             } | ||||
|         if ( | ||||
|             persistentWidgetId && | ||||
|             ActiveWidgetStore.instance.getPersistentRoomId() === room.roomId && | ||||
|             !roomInfo.widgets.some(w => w.id === persistentWidgetId) | ||||
|         ) { | ||||
|             logger.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`); | ||||
|             ActiveWidgetStore.instance.destroyPersistentWidget(persistentWidgetId, room.roomId); | ||||
|         } | ||||
| 
 | ||||
|         this.emit(room.roomId); | ||||
|  | @ -196,7 +190,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
| 
 | ||||
|         // A persistent conference widget indicates that we're participating
 | ||||
|         const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); | ||||
|         return widgets.some(w => ActiveWidgetStore.instance.getWidgetPersistence(w.id)); | ||||
|         return widgets.some(w => ActiveWidgetStore.instance.getWidgetPersistence(w.id, room.roomId)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -264,11 +264,7 @@ export class StopGapWidget extends EventEmitter { | |||
|         this.messaging.on("ready", () => this.emit("ready")); | ||||
|         this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); | ||||
|         this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); | ||||
|         WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); | ||||
| 
 | ||||
|         if (!this.appTileProps.userWidget && this.appTileProps.room) { | ||||
|             ActiveWidgetStore.instance.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId); | ||||
|         } | ||||
|         WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.roomId, this.messaging); | ||||
| 
 | ||||
|         // Always attach a handler for ViewRoom, but permission check it internally
 | ||||
|         this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => { | ||||
|  | @ -318,7 +314,9 @@ export class StopGapWidget extends EventEmitter { | |||
|         this.messaging.on(`action:${WidgetApiFromWidgetAction.UpdateAlwaysOnScreen}`, | ||||
|             (ev: CustomEvent<IStickyActionRequest>) => { | ||||
|                 if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) { | ||||
|                     ActiveWidgetStore.instance.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value); | ||||
|                     ActiveWidgetStore.instance.setWidgetPersistence( | ||||
|                         this.mockWidget.id, this.roomId, ev.detail.data.value, | ||||
|                     ); | ||||
|                     ev.preventDefault(); | ||||
|                     this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
 | ||||
|                 } | ||||
|  | @ -384,7 +382,7 @@ export class StopGapWidget extends EventEmitter { | |||
|         await (WidgetVariableCustomisations?.isReady?.() ?? Promise.resolve()); | ||||
| 
 | ||||
|         if (this.scalarToken) return; | ||||
|         const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget); | ||||
|         const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget, this.roomId); | ||||
|         if (existingMessaging) this.messaging = existingMessaging; | ||||
|         try { | ||||
|             if (WidgetUtils.isScalarUrl(this.mockWidget.templateUrl)) { | ||||
|  | @ -410,13 +408,12 @@ export class StopGapWidget extends EventEmitter { | |||
|      * @param opts | ||||
|      */ | ||||
|     public stopMessaging(opts = { forceDestroy: false }) { | ||||
|         if (!opts?.forceDestroy && ActiveWidgetStore.instance.getPersistentWidgetId() === this.mockWidget.id) { | ||||
|         if (!opts?.forceDestroy && ActiveWidgetStore.instance.getWidgetPersistence(this.mockWidget.id, this.roomId)) { | ||||
|             logger.log("Skipping destroy - persistent widget"); | ||||
|             return; | ||||
|         } | ||||
|         if (!this.started) return; | ||||
|         WidgetMessagingStore.instance.stopMessaging(this.mockWidget); | ||||
|         ActiveWidgetStore.instance.delRoomId(this.mockWidget.id); | ||||
|         WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId); | ||||
|         this.messaging = null; | ||||
| 
 | ||||
|         if (MatrixClientPeg.get()) { | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; | |||
| import defaultDispatcher from "../../dispatcher/dispatcher"; | ||||
| import { ActionPayload } from "../../dispatcher/payloads"; | ||||
| import { EnhancedMap } from "../../utils/maps"; | ||||
| import WidgetUtils from "../../utils/WidgetUtils"; | ||||
| 
 | ||||
| /** | ||||
|  * Temporary holding store for widget messaging instances. This is eventually | ||||
|  | @ -29,8 +30,7 @@ import { EnhancedMap } from "../../utils/maps"; | |||
| export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> { | ||||
|     private static internalInstance = new WidgetMessagingStore(); | ||||
| 
 | ||||
|     // TODO: Fix uniqueness problem (widget IDs are not unique across the whole app)
 | ||||
|     private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
 | ||||
|     private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget UID, ClientWidgetAPi>
 | ||||
| 
 | ||||
|     public constructor() { | ||||
|         super(defaultDispatcher); | ||||
|  | @ -49,35 +49,34 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> { | |||
|         this.widgetMap.clear(); | ||||
|     } | ||||
| 
 | ||||
|     public storeMessaging(widget: Widget, widgetApi: ClientWidgetApi) { | ||||
|         this.stopMessaging(widget); | ||||
|         this.widgetMap.set(widget.id, widgetApi); | ||||
|     public storeMessaging(widget: Widget, roomId: string, widgetApi: ClientWidgetApi) { | ||||
|         this.stopMessaging(widget, roomId); | ||||
|         this.widgetMap.set(WidgetUtils.calcWidgetUid(widget.id, roomId), widgetApi); | ||||
|     } | ||||
| 
 | ||||
|     public stopMessaging(widget: Widget) { | ||||
|         this.widgetMap.remove(widget.id)?.stop(); | ||||
|     public stopMessaging(widget: Widget, roomId: string) { | ||||
|         this.widgetMap.remove(WidgetUtils.calcWidgetUid(widget.id, roomId))?.stop(); | ||||
|     } | ||||
| 
 | ||||
|     public getMessaging(widget: Widget): ClientWidgetApi { | ||||
|         return this.widgetMap.get(widget.id); | ||||
|     public getMessaging(widget: Widget, roomId: string): ClientWidgetApi { | ||||
|         return this.widgetMap.get(WidgetUtils.calcWidgetUid(widget.id, roomId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Stops the widget messaging instance for a given widget ID. | ||||
|      * @param {string} widgetId The widget ID. | ||||
|      * @deprecated Widget IDs are not globally unique. | ||||
|      * Stops the widget messaging instance for a given widget UID. | ||||
|      * @param {string} widgetId The widget UID. | ||||
|      */ | ||||
|     public stopMessagingById(widgetId: string) { | ||||
|         this.widgetMap.remove(widgetId)?.stop(); | ||||
|     public stopMessagingByUid(widgetUid: string) { | ||||
|         this.widgetMap.remove(widgetUid)?.stop(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the widget messaging class for a given widget ID. | ||||
|      * @param {string} widgetId The widget ID. | ||||
|      * Gets the widget messaging class for a given widget UID. | ||||
|      * @param {string} widgetId The widget UID. | ||||
|      * @returns {ClientWidgetApi} The widget API, or a falsey value if not found. | ||||
|      * @deprecated Widget IDs are not globally unique. | ||||
|      */ | ||||
|     public getMessagingForId(widgetId: string): ClientWidgetApi { | ||||
|         return this.widgetMap.get(widgetId); | ||||
|     public getMessagingForUid(widgetUid: string): ClientWidgetApi { | ||||
|         return this.widgetMap.get(widgetUid); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -508,6 +508,14 @@ export default class WidgetUtils { | |||
|         return app?.data?.title?.trim() || ""; | ||||
|     } | ||||
| 
 | ||||
|     static getWidgetUid(app?: IApp): string { | ||||
|         return app ? WidgetUtils.calcWidgetUid(app.id, app.roomId) : ""; | ||||
|     } | ||||
| 
 | ||||
|     static calcWidgetUid(widgetId: string, roomId?: string): string { | ||||
|         return roomId ? `room_${roomId}_${widgetId}` : `user_${widgetId}`; | ||||
|     } | ||||
| 
 | ||||
|     static editWidget(room: Room, app: IApp): void { | ||||
|         // TODO: Open the right manager for the widget
 | ||||
|         if (SettingsStore.getValue("feature_many_integration_managers")) { | ||||
|  |  | |||
|  | @ -44,7 +44,20 @@ describe("AppTile", () => { | |||
|     let r1; | ||||
|     let r2; | ||||
|     const resizeNotifier = new ResizeNotifier(); | ||||
|     let app: IApp; | ||||
|     let app1: IApp; | ||||
|     let app2: IApp; | ||||
| 
 | ||||
|     const waitForRps = (roomId: string) => new Promise<void>(resolve => { | ||||
|         const update = () => { | ||||
|             if ( | ||||
|                 RightPanelStore.instance.currentCardForRoom(roomId).phase !== | ||||
|                 RightPanelPhases.Widget | ||||
|             ) return; | ||||
|             RightPanelStore.instance.off(UPDATE_EVENT, update); | ||||
|             resolve(); | ||||
|         }; | ||||
|         RightPanelStore.instance.on(UPDATE_EVENT, update); | ||||
|     }); | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         stubClient(); | ||||
|  | @ -66,18 +79,31 @@ describe("AppTile", () => { | |||
|             return [r1, r2]; | ||||
|         }); | ||||
| 
 | ||||
|         // Adjust various widget stores to add a mock app
 | ||||
|         app = { | ||||
|         // Adjust various widget stores to add mock apps
 | ||||
|         app1 = { | ||||
|             id: "1", | ||||
|             eventId: "1", | ||||
|             roomId: "r1", | ||||
|             type: MatrixWidgetType.Custom, | ||||
|             url: "https://example.com", | ||||
|             name: "Example", | ||||
|             name: "Example 1", | ||||
|             creatorUserId: cli.getUserId(), | ||||
|             avatar_url: null, | ||||
|         }; | ||||
|         jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([app]); | ||||
|         app2 = { | ||||
|             id: "1", | ||||
|             eventId: "2", | ||||
|             roomId: "r2", | ||||
|             type: MatrixWidgetType.Custom, | ||||
|             url: "https://example.com", | ||||
|             name: "Example 2", | ||||
|             creatorUserId: cli.getUserId(), | ||||
|             avatar_url: null, | ||||
|         }; | ||||
|         jest.spyOn(WidgetStore.instance, "getApps").mockImplementation(roomId => { | ||||
|             if (roomId === "r1") return [app1]; | ||||
|             if (roomId === "r2") return [app2]; | ||||
|         }); | ||||
| 
 | ||||
|         // Wake up various stores we rely on
 | ||||
|         WidgetLayoutStore.instance.useUnitTestClient(cli); | ||||
|  | @ -88,6 +114,26 @@ describe("AppTile", () => { | |||
|         await RightPanelStore.instance.onReady(); | ||||
|     }); | ||||
| 
 | ||||
|     it("tracks live tiles correctly", () => { | ||||
|         expect(AppTile.isLive("1", "r1")).toEqual(false); | ||||
| 
 | ||||
|         // Try removing the tile before it gets added
 | ||||
|         AppTile.removeLiveTile("1", "r1"); | ||||
|         expect(AppTile.isLive("1", "r1")).toEqual(false); | ||||
| 
 | ||||
|         AppTile.addLiveTile("1", "r1"); | ||||
|         expect(AppTile.isLive("1", "r1")).toEqual(true); | ||||
| 
 | ||||
|         AppTile.addLiveTile("1", "r1"); | ||||
|         expect(AppTile.isLive("1", "r1")).toEqual(true); | ||||
| 
 | ||||
|         AppTile.removeLiveTile("1", "r1"); | ||||
|         expect(AppTile.isLive("1", "r1")).toEqual(true); | ||||
| 
 | ||||
|         AppTile.removeLiveTile("1", "r1"); | ||||
|         expect(AppTile.isLive("1", "r1")).toEqual(false); | ||||
|     }); | ||||
| 
 | ||||
|     it("destroys non-persisted right panel widget on room change", async () => { | ||||
|         // Set up right panel state
 | ||||
|         const realGetValue = SettingsStore.getValue; | ||||
|  | @ -115,24 +161,14 @@ describe("AppTile", () => { | |||
|             /> | ||||
|         </MatrixClientContext.Provider>); | ||||
|         // Wait for RPS room 1 updates to fire
 | ||||
|         const rpsUpdated = new Promise<void>(resolve => { | ||||
|             const update = () => { | ||||
|                 if ( | ||||
|                     RightPanelStore.instance.currentCardForRoom("r1").phase !== | ||||
|                     RightPanelPhases.Widget | ||||
|                 ) return; | ||||
|                 RightPanelStore.instance.off(UPDATE_EVENT, update); | ||||
|                 resolve(); | ||||
|             }; | ||||
|             RightPanelStore.instance.on(UPDATE_EVENT, update); | ||||
|         }); | ||||
|         const rpsUpdated = waitForRps("r1"); | ||||
|         dis.dispatch({ | ||||
|             action: Action.ViewRoom, | ||||
|             room_id: "r1", | ||||
|         }); | ||||
|         await rpsUpdated; | ||||
| 
 | ||||
|         expect(AppTile.isLive("1")).toBe(true); | ||||
|         expect(AppTile.isLive("1", "r1")).toBe(true); | ||||
| 
 | ||||
|         // We want to verify that as we change to room 2, we should close the
 | ||||
|         // right panel and destroy the widget.
 | ||||
|  | @ -152,11 +188,88 @@ describe("AppTile", () => { | |||
|         </MatrixClientContext.Provider>); | ||||
| 
 | ||||
|         expect(endWidgetActions.mock.calls.length).toBe(1); | ||||
|         expect(AppTile.isLive("1")).toBe(false); | ||||
|         expect(AppTile.isLive("1", "r1")).toBe(false); | ||||
| 
 | ||||
|         mockSettings.mockRestore(); | ||||
|     }); | ||||
| 
 | ||||
|     it("distinguishes widgets with the same ID in different rooms", async () => { | ||||
|         // Set up right panel state
 | ||||
|         const realGetValue = SettingsStore.getValue; | ||||
|         SettingsStore.getValue = (name, roomId) => { | ||||
|             if (name === "RightPanel.phases") { | ||||
|                 if (roomId === "r1") { | ||||
|                     return { | ||||
|                         history: [{ | ||||
|                             phase: RightPanelPhases.Widget, | ||||
|                             state: { | ||||
|                                 widgetId: "1", | ||||
|                             }, | ||||
|                         }], | ||||
|                         isOpen: true, | ||||
|                     }; | ||||
|                 } | ||||
|                 return null; | ||||
|             } | ||||
|             return realGetValue(name, roomId); | ||||
|         }; | ||||
| 
 | ||||
|         // Run initial render with room 1, and also running lifecycle methods
 | ||||
|         const renderer = TestRenderer.create(<MatrixClientContext.Provider value={cli}> | ||||
|             <RightPanel | ||||
|                 room={r1} | ||||
|                 resizeNotifier={resizeNotifier} | ||||
|             /> | ||||
|         </MatrixClientContext.Provider>); | ||||
|         // Wait for RPS room 1 updates to fire
 | ||||
|         const rpsUpdated1 = waitForRps("r1"); | ||||
|         dis.dispatch({ | ||||
|             action: Action.ViewRoom, | ||||
|             room_id: "r1", | ||||
|         }); | ||||
|         await rpsUpdated1; | ||||
| 
 | ||||
|         expect(AppTile.isLive("1", "r1")).toBe(true); | ||||
|         expect(AppTile.isLive("1", "r2")).toBe(false); | ||||
| 
 | ||||
|         SettingsStore.getValue = (name, roomId) => { | ||||
|             if (name === "RightPanel.phases") { | ||||
|                 if (roomId === "r2") { | ||||
|                     return { | ||||
|                         history: [{ | ||||
|                             phase: RightPanelPhases.Widget, | ||||
|                             state: { | ||||
|                                 widgetId: "1", | ||||
|                             }, | ||||
|                         }], | ||||
|                         isOpen: true, | ||||
|                     }; | ||||
|                 } | ||||
|                 return null; | ||||
|             } | ||||
|             return realGetValue(name, roomId); | ||||
|         }; | ||||
|         // Wait for RPS room 2 updates to fire
 | ||||
|         const rpsUpdated2 = waitForRps("r2"); | ||||
|         // Switch to room 2
 | ||||
|         dis.dispatch({ | ||||
|             action: Action.ViewRoom, | ||||
|             room_id: "r2", | ||||
|         }); | ||||
|         renderer.update(<MatrixClientContext.Provider value={cli}> | ||||
|             <RightPanel | ||||
|                 room={r2} | ||||
|                 resizeNotifier={resizeNotifier} | ||||
|             /> | ||||
|         </MatrixClientContext.Provider>); | ||||
|         await rpsUpdated2; | ||||
| 
 | ||||
|         expect(AppTile.isLive("1", "r1")).toBe(false); | ||||
|         expect(AppTile.isLive("1", "r2")).toBe(true); | ||||
| 
 | ||||
|         SettingsStore.getValue = realGetValue; | ||||
|     }); | ||||
| 
 | ||||
|     it("preserves non-persisted widget on container move", async () => { | ||||
|         // Set up widget in top container
 | ||||
|         const realGetValue = SettingsStore.getValue; | ||||
|  | @ -187,7 +300,7 @@ describe("AppTile", () => { | |||
|             /> | ||||
|         </MatrixClientContext.Provider>); | ||||
| 
 | ||||
|         expect(AppTile.isLive("1")).toBe(true); | ||||
|         expect(AppTile.isLive("1", "r1")).toBe(true); | ||||
| 
 | ||||
|         // We want to verify that as we move the widget to the center container,
 | ||||
|         // the widget frame remains running.
 | ||||
|  | @ -199,11 +312,11 @@ describe("AppTile", () => { | |||
|         // Stop mocking settings so that the widget move can take effect
 | ||||
|         mockSettings.mockRestore(); | ||||
|         TestRenderer.act(() => { | ||||
|             WidgetLayoutStore.instance.moveToContainer(r1, app, Container.Center); | ||||
|             WidgetLayoutStore.instance.moveToContainer(r1, app1, Container.Center); | ||||
|         }); | ||||
| 
 | ||||
|         expect(endWidgetActions.mock.calls.length).toBe(0); | ||||
|         expect(AppTile.isLive("1")).toBe(true); | ||||
|         expect(AppTile.isLive("1", "r1")).toBe(true); | ||||
|     }); | ||||
| 
 | ||||
|     afterAll(async () => { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Robin
						Robin