diff --git a/.gitignore b/.gitignore index ec6947a..f65f07c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ lib/ dist/ .npmrc .idea +.vscode # Logs logs diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 1fd0cd0..4449d10 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -40,7 +40,7 @@ import { ISupportedVersionsActionRequest, ISupportedVersionsActionResponseData, } from "./interfaces/SupportedVersionsAction"; -import { CurrentApiVersions } from "./interfaces/ApiVersion"; +import { ApiVersion, CurrentApiVersions, UnstableApiVersion } from "./interfaces/ApiVersion"; import { IScreenshotActionResponseData } from "./interfaces/ScreenshotAction"; import { IVisibilityActionRequestData } from "./interfaces/VisibilityAction"; import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponseData } from "./interfaces/IWidgetApiResponse"; @@ -138,6 +138,7 @@ import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction" export class ClientWidgetApi extends EventEmitter { public readonly transport: ITransport; + private cachedWidgetVersions: ApiVersion[] | null = null; // contentLoadedActionSent is used to check that only one ContentLoaded request is send. private contentLoadedActionSent = false; private allowedCapabilities = new Set(); @@ -230,6 +231,24 @@ export class ClientWidgetApi extends EventEmitter { this.transport.stop(); } + public async getWidgetVersions(): Promise { + if (Array.isArray(this.cachedWidgetVersions)) { + return Promise.resolve(this.cachedWidgetVersions); + } + + try { + const r = await this.transport.send( + WidgetApiToWidgetAction.SupportedApiVersions, + {}, + ); + this.cachedWidgetVersions = r.supported_versions; + return r.supported_versions; + } catch (e) { + console.warn("non-fatal error getting supported widget versions: ", e); + return []; + } + } + private beginCapabilities(): void { // widget has loaded - tell all the listeners that this.emit("preparing"); @@ -285,7 +304,7 @@ export class ClientWidgetApi extends EventEmitter { private onIframeLoad(ev: Event): void { if (this.widget.waitForIframeLoad) { - // If the widget is set to waitForIframeLoad the capabilities immediatly get setup after load. + // If the widget is set to waitForIframeLoad the capabilities immediately get setup after load. // The client does not wait for the ContentLoaded action. this.beginCapabilities(); } else { @@ -1007,7 +1026,7 @@ export class ClientWidgetApi extends EventEmitter { public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise; /** * Feeds an event to the widget. As a client you are expected to call this - * for every new event in every room to which you are joined or invited. + * for every new event (including state events) in every room to which you are joined or invited. * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. * @returns {Promise} Resolves when delivered or if the widget is not * able to read the event due to permissions, rejects if the widget failed @@ -1087,10 +1106,12 @@ export class ClientWidgetApi extends EventEmitter { events.push(...stateKeyMap.values()); } } - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { state: events }, - ); + if ((await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE)) { + // Only send state updates when using UpdateState. Otherwise the SendEvent action will be responsible for state updates. + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: events, + }); + } } finally { this.flushRoomStateTask = null; } @@ -1149,10 +1170,10 @@ export class ClientWidgetApi extends EventEmitter { * room state entry. * @returns {Promise} Resolves when delivered or if the widget is not * able to receive the room state due to permissions, rejects if the - widget failed to handle the update. + * widget failed to handle the update. */ public async feedStateUpdate(rawEvent: IRoomEvent): Promise { - if (rawEvent.state_key === undefined) throw new Error('Not a state event'); + if (rawEvent.state_key === undefined) throw new Error("Not a state event"); if ( (rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id)) && this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) @@ -1160,10 +1181,12 @@ export class ClientWidgetApi extends EventEmitter { // Updates could race with the initial push of the room's state if (this.pushRoomStateTasks.size === 0) { // No initial push tasks are pending; safe to send immediately - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { state: [rawEvent] }, - ); + if ((await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE)) { + // Only send state updates when using UpdateState. Otherwise the SendEvent action will be responsible for state updates. + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: [rawEvent], + }); + } } else { // Lump the update in with whatever data will be sent in the // initial push later. Even if we set it to an "outdated" entry diff --git a/src/interfaces/ApiVersion.ts b/src/interfaces/ApiVersion.ts index ab0546e..8c7fd9a 100644 --- a/src/interfaces/ApiVersion.ts +++ b/src/interfaces/ApiVersion.ts @@ -22,6 +22,7 @@ export enum MatrixApiVersion { export enum UnstableApiVersion { MSC2762 = "org.matrix.msc2762", + MSC2762_UPDATE_STATE = "org.matrix.msc2762_update_state", MSC2871 = "org.matrix.msc2871", MSC2873 = "org.matrix.msc2873", MSC2931 = "org.matrix.msc2931", @@ -41,6 +42,7 @@ export const CurrentApiVersions: ApiVersion[] = [ MatrixApiVersion.Prerelease2, //MatrixApiVersion.V010, UnstableApiVersion.MSC2762, + UnstableApiVersion.MSC2762_UPDATE_STATE, UnstableApiVersion.MSC2871, UnstableApiVersion.MSC2873, UnstableApiVersion.MSC2931, diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index fd446d7..6743ad6 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -19,17 +19,17 @@ import { waitFor } from '@testing-library/dom'; import { ClientWidgetApi } from "../src/ClientWidgetApi"; import { WidgetDriver } from "../src/driver/WidgetDriver"; -import { UnstableApiVersion } from '../src/interfaces/ApiVersion'; -import { Capability } from '../src/interfaces/Capabilities'; -import { IRoomEvent } from '../src/interfaces/IRoomEvent'; -import { IWidgetApiRequest } from '../src/interfaces/IWidgetApiRequest'; -import { IReadRelationsFromWidgetActionRequest } from '../src/interfaces/ReadRelationsAction'; -import { ISupportedVersionsActionRequest } from '../src/interfaces/SupportedVersionsAction'; -import { IUserDirectorySearchFromWidgetActionRequest } from '../src/interfaces/UserDirectorySearchAction'; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from '../src/interfaces/WidgetApiAction'; -import { WidgetApiDirection } from '../src/interfaces/WidgetApiDirection'; -import { Widget } from '../src/models/Widget'; -import { PostmessageTransport } from '../src/transport/PostmessageTransport'; +import { CurrentApiVersions, UnstableApiVersion } from "../src/interfaces/ApiVersion"; +import { Capability } from "../src/interfaces/Capabilities"; +import { IRoomEvent } from "../src/interfaces/IRoomEvent"; +import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest"; +import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction"; +import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction"; +import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "../src/interfaces/WidgetApiAction"; +import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection"; +import { Widget } from "../src/models/Widget"; +import { PostmessageTransport } from "../src/transport/PostmessageTransport"; import { IDownloadFileActionFromWidgetActionRequest, IGetOpenIDActionRequest, @@ -792,6 +792,14 @@ describe('ClientWidgetApi', () => { const roomId = '!room:example.org'; const otherRoomId = '!other-room:example.org'; clientWidgetApi.setViewedRoomId(roomId); + + jest.spyOn(transport, "send").mockImplementation((action, data) => { + if (action === WidgetApiToWidgetAction.SupportedApiVersions) { + return Promise.resolve({ supported_versions: CurrentApiVersions }); + } + return Promise.resolve({}); + }); + const topicEvent = createRoomEvent({ room_id: roomId, type: 'm.room.topic', @@ -904,6 +912,37 @@ describe('ClientWidgetApi', () => { }); }); + describe('dont receive UpdateState if version not supported', () => { + it('syncs initial state and feeds updates', async () => { + const roomId = '!room:example.org'; + clientWidgetApi.setViewedRoomId(roomId); + jest.spyOn(transport, "send").mockImplementation((action, data) => { + if (action === WidgetApiToWidgetAction.SupportedApiVersions) { + return Promise.resolve({ supported_versions: [] }); + } + return Promise.resolve({}); + }); + + await loadIframe([ + 'org.matrix.msc2762.receive.state_event:m.room.join_rules#', + ]); + + const newJoinRulesEvent = createRoomEvent({ + room_id: roomId, + type: 'm.room.join_rules', + state_key: '', + content: { join_rule: 'invite' }, + }); + clientWidgetApi.feedStateUpdate(newJoinRulesEvent); + + await waitFor(() => { + + // Only the updated join rules should have been delivered + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState); + }); + }); + }); + describe('update_delayed_event action', () => { it('fails to update delayed events', async () => { const event: IUpdateDelayedEventFromWidgetActionRequest = {