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

Commit a437c67

Browse files
authored
Support delayed events (MSC4140) for call widget (#12714)
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.
1 parent a35bf68 commit a437c67

File tree

5 files changed

+230
-11
lines changed

5 files changed

+230
-11
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
"matrix-encrypt-attachment": "^1.0.3",
120120
"matrix-events-sdk": "0.0.1",
121121
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
122-
"matrix-widget-api": "^1.5.0",
122+
"matrix-widget-api": "^1.8.2",
123123
"memoize-one": "^6.0.0",
124124
"minimist": "^1.2.5",
125125
"oidc-client-ts": "^3.0.1",

src/stores/widgets/StopGapWidgetDriver.ts

+99-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
EventDirection,
2020
IOpenIDCredentials,
2121
IOpenIDUpdate,
22+
ISendDelayedEventDetails,
2223
ISendEventDetails,
2324
ITurnServer,
2425
IReadEventRelationsResult,
@@ -33,6 +34,7 @@ import {
3334
WidgetKind,
3435
ISearchUserDirectoryResult,
3536
IGetMediaConfigResult,
37+
UpdateDelayedEventAction,
3638
} from "matrix-widget-api";
3739
import {
3840
ClientEvent,
@@ -43,6 +45,7 @@ import {
4345
Room,
4446
Direction,
4547
THREAD_RELATION_TYPE,
48+
SendDelayedEventResponse,
4649
StateEvents,
4750
TimelineEvents,
4851
} from "matrix-js-sdk/src/matrix";
@@ -128,6 +131,8 @@ export class StopGapWidgetDriver extends WidgetDriver {
128131
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
129132
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
130133
this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`);
134+
this.allowedCapabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
135+
this.allowedCapabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
131136

132137
this.allowedCapabilities.add(
133138
WidgetEventCapability.forRoomEvent(EventDirection.Send, "org.matrix.rageshake_request").raw,
@@ -160,7 +165,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
160165
`_${clientUserId}_${clientDeviceId}`,
161166
).raw,
162167
);
163-
// MSC3779 version, with no leading underscore
168+
// Version with no leading underscore, for room versions whose auth rules allow it
164169
this.allowedCapabilities.add(
165170
WidgetEventCapability.forStateEvent(
166171
EventDirection.Send,
@@ -271,20 +276,20 @@ export class StopGapWidgetDriver extends WidgetDriver {
271276
public async sendEvent<K extends keyof StateEvents>(
272277
eventType: K,
273278
content: StateEvents[K],
274-
stateKey?: string,
275-
targetRoomId?: string,
279+
stateKey: string | null,
280+
targetRoomId: string | null,
276281
): Promise<ISendEventDetails>;
277282
public async sendEvent<K extends keyof TimelineEvents>(
278283
eventType: K,
279284
content: TimelineEvents[K],
280285
stateKey: null,
281-
targetRoomId?: string,
286+
targetRoomId: string | null,
282287
): Promise<ISendEventDetails>;
283288
public async sendEvent(
284289
eventType: string,
285290
content: IContent,
286-
stateKey?: string | null,
287-
targetRoomId?: string,
291+
stateKey: string | null = null,
292+
targetRoomId: string | null = null,
288293
): Promise<ISendEventDetails> {
289294
const client = MatrixClientPeg.get();
290295
const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId();
@@ -328,6 +333,94 @@ export class StopGapWidgetDriver extends WidgetDriver {
328333
return { roomId, eventId: r.event_id };
329334
}
330335

336+
/**
337+
* @experimental Part of MSC4140 & MSC4157
338+
* @see {@link WidgetDriver#sendDelayedEvent}
339+
*/
340+
public async sendDelayedEvent<K extends keyof StateEvents>(
341+
delay: number | null,
342+
parentDelayId: string | null,
343+
eventType: K,
344+
content: StateEvents[K],
345+
stateKey: string | null,
346+
targetRoomId: string | null,
347+
): Promise<ISendDelayedEventDetails>;
348+
/**
349+
* @experimental Part of MSC4140 & MSC4157
350+
*/
351+
public async sendDelayedEvent<K extends keyof TimelineEvents>(
352+
delay: number | null,
353+
parentDelayId: string | null,
354+
eventType: K,
355+
content: TimelineEvents[K],
356+
stateKey: null,
357+
targetRoomId: string | null,
358+
): Promise<ISendDelayedEventDetails>;
359+
public async sendDelayedEvent(
360+
delay: number | null,
361+
parentDelayId: string | null,
362+
eventType: string,
363+
content: IContent,
364+
stateKey: string | null = null,
365+
targetRoomId: string | null = null,
366+
): Promise<ISendDelayedEventDetails> {
367+
const client = MatrixClientPeg.get();
368+
const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId();
369+
370+
if (!client || !roomId) throw new Error("Not in a room or not attached to a client");
371+
372+
let delayOpts;
373+
if (delay !== null) {
374+
delayOpts = {
375+
delay,
376+
...(parentDelayId !== null && { parent_delay_id: parentDelayId }),
377+
};
378+
} else if (parentDelayId !== null) {
379+
delayOpts = {
380+
parent_delay_id: parentDelayId,
381+
};
382+
} else {
383+
throw new Error("Must provide at least one of delay or parentDelayId");
384+
}
385+
386+
let r: SendDelayedEventResponse | null;
387+
if (stateKey !== null) {
388+
// state event
389+
r = await client._unstable_sendDelayedStateEvent(
390+
roomId,
391+
delayOpts,
392+
eventType as keyof StateEvents,
393+
content as StateEvents[keyof StateEvents],
394+
stateKey,
395+
);
396+
} else {
397+
// message event
398+
r = await client._unstable_sendDelayedEvent(
399+
roomId,
400+
delayOpts,
401+
null,
402+
eventType as keyof TimelineEvents,
403+
content as TimelineEvents[keyof TimelineEvents],
404+
);
405+
}
406+
407+
return {
408+
roomId,
409+
delayId: r.delay_id,
410+
};
411+
}
412+
413+
/**
414+
* @experimental Part of MSC4140 & MSC4157
415+
*/
416+
public async updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<void> {
417+
const client = MatrixClientPeg.get();
418+
419+
if (!client) throw new Error("Not in a room or not attached to a client");
420+
421+
await client._unstable_updateDelayedEvent(delayId, action);
422+
}
423+
331424
public async sendToDevice(
332425
eventType: string,
333426
encrypted: boolean,

test/stores/widgets/StopGapWidgetDriver-test.ts

+122
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
SimpleObservable,
3636
OpenIDRequestState,
3737
IOpenIDUpdate,
38+
UpdateDelayedEventAction,
3839
} from "matrix-widget-api";
3940
import {
4041
ApprovalOpts,
@@ -122,6 +123,8 @@ describe("StopGapWidgetDriver", () => {
122123
"org.matrix.msc3819.receive.to_device:org.matrix.call.sdp_stream_metadata_changed",
123124
"org.matrix.msc3819.send.to_device:m.call.replaces",
124125
"org.matrix.msc3819.receive.to_device:m.call.replaces",
126+
"org.matrix.msc4157.send.delayed_event",
127+
"org.matrix.msc4157.update_delayed_event",
125128
]);
126129

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

394+
describe("sendDelayedEvent", () => {
395+
let driver: WidgetDriver;
396+
const roomId = "!this-room-id";
397+
398+
beforeEach(() => {
399+
driver = mkDefaultDriver();
400+
});
401+
402+
it("cannot send delayed events with missing arguments", async () => {
403+
await expect(driver.sendDelayedEvent(null, null, EventType.RoomMessage, {})).rejects.toThrow(
404+
"Must provide at least one of",
405+
);
406+
});
407+
408+
it("sends delayed message events", async () => {
409+
client._unstable_sendDelayedEvent.mockResolvedValue({
410+
delay_id: "id",
411+
});
412+
413+
await expect(driver.sendDelayedEvent(2000, null, EventType.RoomMessage, {})).resolves.toEqual({
414+
roomId,
415+
delayId: "id",
416+
});
417+
418+
expect(client._unstable_sendDelayedEvent).toHaveBeenCalledWith(
419+
roomId,
420+
{ delay: 2000 },
421+
null,
422+
EventType.RoomMessage,
423+
{},
424+
);
425+
});
426+
427+
it("sends child action delayed message events", async () => {
428+
client._unstable_sendDelayedEvent.mockResolvedValue({
429+
delay_id: "id-child",
430+
});
431+
432+
await expect(driver.sendDelayedEvent(null, "id-parent", EventType.RoomMessage, {})).resolves.toEqual({
433+
roomId,
434+
delayId: "id-child",
435+
});
436+
437+
expect(client._unstable_sendDelayedEvent).toHaveBeenCalledWith(
438+
roomId,
439+
{ parent_delay_id: "id-parent" },
440+
null,
441+
EventType.RoomMessage,
442+
{},
443+
);
444+
});
445+
446+
it("sends delayed state events", async () => {
447+
client._unstable_sendDelayedStateEvent.mockResolvedValue({
448+
delay_id: "id",
449+
});
450+
451+
await expect(driver.sendDelayedEvent(2000, null, EventType.RoomTopic, {}, "")).resolves.toEqual({
452+
roomId,
453+
delayId: "id",
454+
});
455+
456+
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
457+
roomId,
458+
{ delay: 2000 },
459+
EventType.RoomTopic,
460+
{},
461+
"",
462+
);
463+
});
464+
465+
it("sends child action delayed state events", async () => {
466+
client._unstable_sendDelayedStateEvent.mockResolvedValue({
467+
delay_id: "id-child",
468+
});
469+
470+
await expect(driver.sendDelayedEvent(null, "id-parent", EventType.RoomTopic, {}, "")).resolves.toEqual({
471+
roomId,
472+
delayId: "id-child",
473+
});
474+
475+
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
476+
roomId,
477+
{ parent_delay_id: "id-parent" },
478+
EventType.RoomTopic,
479+
{},
480+
"",
481+
);
482+
});
483+
});
484+
485+
describe("updateDelayedEvent", () => {
486+
let driver: WidgetDriver;
487+
488+
beforeEach(() => {
489+
driver = mkDefaultDriver();
490+
});
491+
492+
it("updates delayed events", async () => {
493+
client._unstable_updateDelayedEvent.mockResolvedValue({});
494+
for (const action of [
495+
UpdateDelayedEventAction.Cancel,
496+
UpdateDelayedEventAction.Restart,
497+
UpdateDelayedEventAction.Send,
498+
]) {
499+
await expect(driver.updateDelayedEvent("id", action)).resolves.toBeUndefined();
500+
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledWith("id", action);
501+
}
502+
});
503+
504+
it("fails to update delayed events", async () => {
505+
const errorMessage = "Cannot restart this delayed event";
506+
client._unstable_updateDelayedEvent.mockRejectedValue(new Error(errorMessage));
507+
await expect(driver.updateDelayedEvent("id", UpdateDelayedEventAction.Restart)).rejects.toThrow(
508+
errorMessage,
509+
);
510+
});
511+
});
512+
391513
describe("If the feature_dynamic_room_predecessors feature is not enabled", () => {
392514
beforeEach(() => {
393515
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);

test/test-utils/test-utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ export function createTestClient(): MatrixClient {
252252
});
253253
}),
254254

255+
_unstable_sendDelayedEvent: jest.fn(),
256+
_unstable_sendDelayedStateEvent: jest.fn(),
257+
_unstable_updateDelayedEvent: jest.fn(),
258+
255259
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
256260
setDeviceVerified: jest.fn(),
257261
joinRoom: jest.fn(),

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -6938,10 +6938,10 @@ matrix-web-i18n@^3.2.1:
69386938
minimist "^1.2.8"
69396939
walk "^2.3.15"
69406940

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

0 commit comments

Comments
 (0)