Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ lib/
dist/
.npmrc
.idea
.vscode

# Logs
logs
Expand Down
47 changes: 36 additions & 11 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Capability>();
Expand Down Expand Up @@ -230,6 +231,24 @@ export class ClientWidgetApi extends EventEmitter {
this.transport.stop();
}

public async getWidgetVersions(): Promise<ApiVersion[]> {
if (Array.isArray(this.cachedWidgetVersions)) {
return Promise.resolve(this.cachedWidgetVersions);
}

try {
const r = await this.transport.send<IWidgetApiRequestEmptyData, ISupportedVersionsActionResponseData>(
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");
Expand Down Expand Up @@ -1007,7 +1026,7 @@ export class ClientWidgetApi extends EventEmitter {
public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise<void>;
/**
* 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<void>} Resolves when delivered or if the widget is not
* able to read the event due to permissions, rejects if the widget failed
Expand Down Expand Up @@ -1076,6 +1095,7 @@ export class ClientWidgetApi extends EventEmitter {
}

private async flushRoomState(): Promise<void> {
const useUpdateState = (await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
try {
// Only send a single action once all concurrent tasks have completed
do await Promise.all([...this.pushRoomStateTasks]);
Expand All @@ -1087,10 +1107,11 @@ export class ClientWidgetApi extends EventEmitter {
events.push(...stateKeyMap.values());
}
}
await this.transport.send<IUpdateStateToWidgetRequestData>(
WidgetApiToWidgetAction.UpdateState,
{ state: events },
);
if (useUpdateState) {
await this.transport.send<IUpdateStateToWidgetRequestData>(WidgetApiToWidgetAction.UpdateState, {
state: events,
});
}
} finally {
this.flushRoomStateTask = null;
}
Expand Down Expand Up @@ -1152,18 +1173,22 @@ export class ClientWidgetApi extends EventEmitter {
widget failed to handle the update.
*/
public async feedStateUpdate(rawEvent: IRoomEvent): Promise<void> {
if (rawEvent.state_key === undefined) throw new Error('Not a state event');
const useUpdateState = (await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, this could be moved to line 1186 so that it won't be called unless needed. Then again, this will cache the result, so it shouldn't be expensive & should get called eventually.


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)
) {
// 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<IUpdateStateToWidgetRequestData>(
WidgetApiToWidgetAction.UpdateState,
{ state: [rawEvent] },
);
if (useUpdateState) {
// Only send state updates when using UpdateState. Otherwise we will use SendEvent.
await this.transport.send<IUpdateStateToWidgetRequestData>(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
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/ApiVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -41,6 +42,7 @@ export const CurrentApiVersions: ApiVersion[] = [
MatrixApiVersion.Prerelease2,
//MatrixApiVersion.V010,
UnstableApiVersion.MSC2762,
UnstableApiVersion.MSC2762_UPDATE_STATE,
UnstableApiVersion.MSC2871,
UnstableApiVersion.MSC2873,
UnstableApiVersion.MSC2931,
Expand Down
61 changes: 50 additions & 11 deletions test/ClientWidgetApi-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 = {
Expand Down
Loading