Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support delayed events (MSC4140) for call widget
Browse files Browse the repository at this point in the history
The Widget API spec for delayed events is defined by MSC4157.

Also support "parent" delayed events, which were in a previous version
of MSC4140 and may be reintroduced or be part of a new MSC later.
AndrewFerr committed Aug 7, 2024

Verified

This commit was signed with the committer’s verified signature.
macmv Neil Macneale V
1 parent a35bf68 commit f5fcf55
Showing 5 changed files with 230 additions and 11 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -119,7 +119,7 @@
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.5.0",
"matrix-widget-api": "^1.8.2",
"memoize-one": "^6.0.0",
"minimist": "^1.2.5",
"oidc-client-ts": "^3.0.1",
105 changes: 99 additions & 6 deletions src/stores/widgets/StopGapWidgetDriver.ts
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import {
EventDirection,
IOpenIDCredentials,
IOpenIDUpdate,
ISendDelayedEventDetails,
ISendEventDetails,
ITurnServer,
IReadEventRelationsResult,
@@ -33,6 +34,7 @@ import {
WidgetKind,
ISearchUserDirectoryResult,
IGetMediaConfigResult,
UpdateDelayedEventAction,
} from "matrix-widget-api";
import {
ClientEvent,
@@ -43,6 +45,7 @@ import {
Room,
Direction,
THREAD_RELATION_TYPE,
SendDelayedEventResponse,
StateEvents,
TimelineEvents,
} from "matrix-js-sdk/src/matrix";
@@ -128,6 +131,8 @@ export class StopGapWidgetDriver extends WidgetDriver {
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`);
this.allowedCapabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
this.allowedCapabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);

this.allowedCapabilities.add(
WidgetEventCapability.forRoomEvent(EventDirection.Send, "org.matrix.rageshake_request").raw,
@@ -160,7 +165,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
`_${clientUserId}_${clientDeviceId}`,
).raw,
);
// MSC3779 version, with no leading underscore
// Version with no leading underscore, for room versions whose auth rules allow it
this.allowedCapabilities.add(
WidgetEventCapability.forStateEvent(
EventDirection.Send,
@@ -271,20 +276,20 @@ export class StopGapWidgetDriver extends WidgetDriver {
public async sendEvent<K extends keyof StateEvents>(
eventType: K,
content: StateEvents[K],
stateKey?: string,
targetRoomId?: string,
stateKey: string | null,
targetRoomId: string | null,
): Promise<ISendEventDetails>;
public async sendEvent<K extends keyof TimelineEvents>(
eventType: K,
content: TimelineEvents[K],
stateKey: null,
targetRoomId?: string,
targetRoomId: string | null,
): Promise<ISendEventDetails>;
public async sendEvent(
eventType: string,
content: IContent,
stateKey?: string | null,
targetRoomId?: string,
stateKey: string | null = null,
targetRoomId: string | null = null,
): Promise<ISendEventDetails> {
const client = MatrixClientPeg.get();
const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId();
@@ -328,6 +333,94 @@ export class StopGapWidgetDriver extends WidgetDriver {
return { roomId, eventId: r.event_id };
}

/**
* @experimental Part of MSC4140 & MSC4157
* @see {@link WidgetDriver#sendDelayedEvent}
*/
public async sendDelayedEvent<K extends keyof StateEvents>(
delay: number | null,
parentDelayId: string | null,
eventType: K,
content: StateEvents[K],
stateKey: string | null,
targetRoomId: string | null,
): Promise<ISendDelayedEventDetails>;
/**
* @experimental Part of MSC4140 & MSC4157
*/
public async sendDelayedEvent<K extends keyof TimelineEvents>(
delay: number | null,
parentDelayId: string | null,
eventType: K,
content: TimelineEvents[K],
stateKey: null,
targetRoomId: string | null,
): Promise<ISendDelayedEventDetails>;
public async sendDelayedEvent(
delay: number | null,
parentDelayId: string | null,
eventType: string,
content: IContent,
stateKey: string | null = null,
targetRoomId: string | null = null,
): Promise<ISendDelayedEventDetails> {
const client = MatrixClientPeg.get();
const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId();

if (!client || !roomId) throw new Error("Not in a room or not attached to a client");

let delayOpts;
if (delay !== null) {
delayOpts = {
delay,
...(parentDelayId !== null && { parent_delay_id: parentDelayId }),
};
} else if (parentDelayId !== null) {
delayOpts = {
parent_delay_id: parentDelayId,
};
} else {
throw new Error("Must provide at least one of delay or parentDelayId");
}

let r: SendDelayedEventResponse | null;
if (stateKey !== null) {
// state event
r = await client._unstable_sendDelayedStateEvent(
roomId,
delayOpts,
eventType as keyof StateEvents,
content as StateEvents[keyof StateEvents],
stateKey,
);
} else {
// message event
r = await client._unstable_sendDelayedEvent(
roomId,
delayOpts,
null,
eventType as keyof TimelineEvents,
content as TimelineEvents[keyof TimelineEvents],
);
}

return {
roomId,
delayId: r.delay_id,
};
}

/**
* @experimental Part of MSC4140 & MSC4157
*/
public async updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<void> {
const client = MatrixClientPeg.get();

if (!client) throw new Error("Not in a room or not attached to a client");

await client._unstable_updateDelayedEvent(delayId, action);
}

public async sendToDevice(
eventType: string,
encrypted: boolean,
122 changes: 122 additions & 0 deletions test/stores/widgets/StopGapWidgetDriver-test.ts
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ import {
SimpleObservable,
OpenIDRequestState,
IOpenIDUpdate,
UpdateDelayedEventAction,
} from "matrix-widget-api";
import {
ApprovalOpts,
@@ -122,6 +123,8 @@ describe("StopGapWidgetDriver", () => {
"org.matrix.msc3819.receive.to_device:org.matrix.call.sdp_stream_metadata_changed",
"org.matrix.msc3819.send.to_device:m.call.replaces",
"org.matrix.msc3819.receive.to_device:m.call.replaces",
"org.matrix.msc4157.send.delayed_event",
"org.matrix.msc4157.update_delayed_event",
]);

// As long as this resolves, we'll know that it didn't try to pop up a modal
@@ -388,6 +391,125 @@ describe("StopGapWidgetDriver", () => {
});
});

describe("sendDelayedEvent", () => {
let driver: WidgetDriver;
const roomId = "!this-room-id";

beforeEach(() => {
driver = mkDefaultDriver();
});

it("cannot send delayed events with missing arguments", async () => {
await expect(driver.sendDelayedEvent(null, null, EventType.RoomMessage, {})).rejects.toThrow(
"Must provide at least one of",
);
});

it("sends delayed message events", async () => {
client._unstable_sendDelayedEvent.mockResolvedValue({
delay_id: "id",
});

await expect(driver.sendDelayedEvent(2000, null, EventType.RoomMessage, {})).resolves.toEqual({
roomId,
delayId: "id",
});

expect(client._unstable_sendDelayedEvent).toHaveBeenCalledWith(
roomId,
{ delay: 2000 },
null,
EventType.RoomMessage,
{},
);
});

it("sends child action delayed message events", async () => {
client._unstable_sendDelayedEvent.mockResolvedValue({
delay_id: "id-child",
});

await expect(driver.sendDelayedEvent(null, "id-parent", EventType.RoomMessage, {})).resolves.toEqual({
roomId,
delayId: "id-child",
});

expect(client._unstable_sendDelayedEvent).toHaveBeenCalledWith(
roomId,
{ parent_delay_id: "id-parent" },
null,
EventType.RoomMessage,
{},
);
});

it("sends delayed state events", async () => {
client._unstable_sendDelayedStateEvent.mockResolvedValue({
delay_id: "id",
});

await expect(driver.sendDelayedEvent(2000, null, EventType.RoomTopic, {}, "")).resolves.toEqual({
roomId,
delayId: "id",
});

expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
roomId,
{ delay: 2000 },
EventType.RoomTopic,
{},
"",
);
});

it("sends child action delayed state events", async () => {
client._unstable_sendDelayedStateEvent.mockResolvedValue({
delay_id: "id-child",
});

await expect(driver.sendDelayedEvent(null, "id-parent", EventType.RoomTopic, {}, "")).resolves.toEqual({
roomId,
delayId: "id-child",
});

expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
roomId,
{ parent_delay_id: "id-parent" },
EventType.RoomTopic,
{},
"",
);
});
});

describe("updateDelayedEvent", () => {
let driver: WidgetDriver;

beforeEach(() => {
driver = mkDefaultDriver();
});

it("updates delayed events", async () => {
client._unstable_updateDelayedEvent.mockResolvedValue({});
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await expect(driver.updateDelayedEvent("id", action)).resolves.toBeUndefined();
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledWith("id", action);
}
});

it("fails to update delayed events", async () => {
const errorMessage = "Cannot restart this delayed event";
client._unstable_updateDelayedEvent.mockRejectedValue(new Error(errorMessage));
await expect(driver.updateDelayedEvent("id", UpdateDelayedEventAction.Restart)).rejects.toThrow(
errorMessage,
);
});
});

describe("If the feature_dynamic_room_predecessors feature is not enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
4 changes: 4 additions & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
@@ -252,6 +252,10 @@ export function createTestClient(): MatrixClient {
});
}),

_unstable_sendDelayedEvent: jest.fn(),
_unstable_sendDelayedStateEvent: jest.fn(),
_unstable_updateDelayedEvent: jest.fn(),

searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
setDeviceVerified: jest.fn(),
joinRoom: jest.fn(),
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -6938,10 +6938,10 @@ matrix-web-i18n@^3.2.1:
minimist "^1.2.8"
walk "^2.3.15"

matrix-widget-api@^1.5.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.7.0.tgz#ae3b44380f11bb03519d0bf0373dfc3341634667"
integrity sha512-dzSnA5Va6CeIkyWs89xZty/uv38HLyfjOrHGbbEikCa2ZV0HTkUNtrBMKlrn4CRYyDJ6yoO/3ssRwiR0jJvOkQ==
matrix-widget-api@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.8.2.tgz#28d344502a85593740f560b0f8120e474a054505"
integrity sha512-kdmks3CvFNPIYN669Y4rO13KrazDvX8KHC7i6jOzJs8uZ8s54FNkuRVVyiQHeVCSZG5ixUqW9UuCj9lf03qxTQ==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"

0 comments on commit f5fcf55

Please sign in to comment.