diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 0472b1664b1..75dd158566a 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -6,7 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ -import { Room, MatrixEvent, MatrixEventEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix"; +import { + Room, + MatrixEvent, + MatrixEventEvent, + MatrixClient, + ClientEvent, + RoomStateEvent, +} from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { ClientWidgetApi, @@ -26,7 +33,6 @@ import { WidgetApiFromWidgetAction, WidgetKind, } from "matrix-widget-api"; -import { Optional } from "matrix-events-sdk"; import { EventEmitter } from "events"; import { logger } from "matrix-js-sdk/src/logger"; @@ -56,6 +62,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import Modal from "../../Modal"; import ErrorDialog from "../../components/views/dialogs/ErrorDialog"; import { SdkContextClass } from "../../contexts/SDKContext"; +import { UPDATE_EVENT } from "../AsyncStore"; // TODO: Destroy all of this code @@ -151,6 +158,7 @@ export class StopGapWidget extends EventEmitter { private mockWidget: ElementWidget; private scalarToken?: string; private roomId?: string; + private viewedRoomId: string | null = null; private kind: WidgetKind; private readonly virtual: boolean; private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID @@ -177,17 +185,6 @@ export class StopGapWidget extends EventEmitter { this.stickyPromise = appTileProps.stickyPromise; } - private get eventListenerRoomId(): Optional { - // When widgets are listening to events, we need to make sure they're only - // receiving events for the right room. In particular, room widgets get locked - // to the room they were added in while account widgets listen to the currently - // active room. - - if (this.roomId) return this.roomId; - - return SdkContextClass.instance.roomViewStore.getRoomId(); - } - public get widgetApi(): ClientWidgetApi | null { return this.messaging; } @@ -259,6 +256,15 @@ export class StopGapWidget extends EventEmitter { }); } }; + + private onRoomViewStoreUpdate = (): void => { + const roomId = SdkContextClass.instance.roomViewStore.getRoomId() ?? null; + if (roomId !== this.viewedRoomId) { + this.messaging!.setViewedRoomId(roomId); + this.viewedRoomId = roomId; + } + }; + /** * This starts the messaging for the widget if it is not in the state `started` yet. * @param iframe the iframe the widget should use @@ -285,6 +291,17 @@ export class StopGapWidget extends EventEmitter { this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); + // When widgets are listening to events, we need to make sure they're only + // receiving events for the right room + if (this.roomId === undefined) { + // Account widgets listen to the currently active room + this.messaging.setViewedRoomId(SdkContextClass.instance.roomViewStore.getRoomId() ?? null); + SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate); + } else { + // Room widgets get looked to the room they were added in + this.messaging.setViewedRoomId(this.roomId); + } + // Always attach a handler for ViewRoom, but permission check it internally this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent) => { ev.preventDefault(); // stop the widget API from auto-rejecting this @@ -329,6 +346,7 @@ export class StopGapWidget extends EventEmitter { // Attach listeners for feeding events - the underlying widget classes handle permissions for us this.client.on(ClientEvent.Event, this.onEvent); this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.on(RoomStateEvent.Events, this.onStateUpdate); this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.messaging.on( @@ -457,8 +475,11 @@ export class StopGapWidget extends EventEmitter { WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId); this.messaging = null; + SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate); + this.client.off(ClientEvent.Event, this.onEvent); this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.off(RoomStateEvent.Events, this.onStateUpdate); this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } @@ -471,6 +492,14 @@ export class StopGapWidget extends EventEmitter { this.feedEvent(ev); }; + private onStateUpdate = (ev: MatrixEvent): void => { + if (this.messaging === null) return; + const raw = ev.getEffectiveEvent(); + this.messaging.feedStateUpdate(raw as IRoomEvent).catch((e) => { + logger.error("Error sending state update to widget: ", e); + }); + }; + private onToDeviceEvent = async (ev: MatrixEvent): Promise => { await this.client.decryptEventIfNeeded(ev); if (ev.isDecryptionFailure()) return; @@ -570,7 +599,7 @@ export class StopGapWidget extends EventEmitter { this.eventsToFeed.add(ev); } else { const raw = ev.getEffectiveEvent(); - this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => { + this.messaging.feedEvent(raw as IRoomEvent).catch((e) => { logger.error("Error sending event to widget: ", e); }); } diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index de7a71fa800..e08a207c244 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -19,7 +19,6 @@ import { MatrixCapabilities, OpenIDRequestState, SimpleObservable, - Symbols, Widget, WidgetDriver, WidgetEventCapability, @@ -36,7 +35,6 @@ import { IContent, MatrixError, MatrixEvent, - Room, Direction, THREAD_RELATION_TYPE, SendDelayedEventResponse, @@ -469,70 +467,44 @@ export class StopGapWidgetDriver extends WidgetDriver { } } - private pickRooms(roomIds?: (string | Symbols.AnyRoom)[]): Room[] { - const client = MatrixClientPeg.get(); - if (!client) throw new Error("Not attached to a client"); - - const targetRooms = roomIds - ? roomIds.includes(Symbols.AnyRoom) - ? client.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors")) - : roomIds.map((r) => client.getRoom(r)) - : [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()!)]; - return targetRooms.filter((r) => !!r) as Room[]; - } - - public async readRoomEvents( + public async readRoomTimeline( + roomId: string, eventType: string, msgtype: string | undefined, - limitPerRoom: number, - roomIds?: (string | Symbols.AnyRoom)[], + stateKey: string | undefined, + limit: number, + since: string | undefined, ): Promise { - limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary - - const rooms = this.pickRooms(roomIds); - const allResults: IRoomEvent[] = []; - for (const room of rooms) { - const results: MatrixEvent[] = []; - const events = room.getLiveTimeline().getEvents(); // timelines are most recent last - for (let i = events.length - 1; i > 0; i--) { - if (results.length >= limitPerRoom) break; - - const ev = events[i]; - if (ev.getType() !== eventType || ev.isState()) continue; - if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue; - results.push(ev); - } - - results.forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent)); + limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary + + const room = MatrixClientPeg.safeGet().getRoom(roomId); + if (room === null) return []; + const results: MatrixEvent[] = []; + const events = room.getLiveTimeline().getEvents(); // timelines are most recent last + for (let i = events.length - 1; i > 0; i--) { + const ev = events[i]; + if (results.length >= limit) break; + if (since !== undefined && ev.getId() === since) break; + + if (ev.getType() !== eventType || ev.isState()) continue; + if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue; + if (ev.getStateKey() !== undefined && stateKey !== undefined && ev.getStateKey() !== stateKey) continue; + results.push(ev); } - return allResults; + + return results.map((e) => e.getEffectiveEvent() as IRoomEvent); } - public async readStateEvents( - eventType: string, - stateKey: string | undefined, - limitPerRoom: number, - roomIds?: (string | Symbols.AnyRoom)[], - ): Promise { - limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary - - const rooms = this.pickRooms(roomIds); - const allResults: IRoomEvent[] = []; - for (const room of rooms) { - const results: MatrixEvent[] = []; - const state = room.currentState.events.get(eventType); - if (state) { - if (stateKey === "" || !!stateKey) { - const forKey = state.get(stateKey); - if (forKey) results.push(forKey); - } else { - results.push(...Array.from(state.values())); - } - } + public async readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise { + const room = MatrixClientPeg.safeGet().getRoom(roomId); + if (room === null) return []; + const state = room.getLiveTimeline().getState(Direction.Forward); + if (state === undefined) return []; - results.slice(0, limitPerRoom).forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent)); - } - return allResults; + if (stateKey === undefined) + return state.getStateEvents(eventType).map((e) => e.getEffectiveEvent() as IRoomEvent); + const event = state.getStateEvents(eventType, stateKey); + return event === null ? [] : [event.getEffectiveEvent() as IRoomEvent]; } public async askOpenID(observer: SimpleObservable): Promise { @@ -693,6 +665,12 @@ export class StopGapWidgetDriver extends WidgetDriver { return { file: blob }; } + public getKnownRooms(): string[] { + return MatrixClientPeg.safeGet() + .getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors")) + .map((r) => r.roomId); + } + /** * Expresses a {@link MatrixError} as a JSON payload * for use by Widget API error responses.