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>
This commit is contained in:
parent
bc8fdac491
commit
744eeb53fe
13 changed files with 276 additions and 159 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue