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.
This commit is contained in:
parent
a35bf68f22
commit
a437c677bb
5 changed files with 230 additions and 11 deletions
|
@ -119,7 +119,7 @@
|
||||||
"matrix-encrypt-attachment": "^1.0.3",
|
"matrix-encrypt-attachment": "^1.0.3",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"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",
|
"memoize-one": "^6.0.0",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"oidc-client-ts": "^3.0.1",
|
"oidc-client-ts": "^3.0.1",
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
EventDirection,
|
EventDirection,
|
||||||
IOpenIDCredentials,
|
IOpenIDCredentials,
|
||||||
IOpenIDUpdate,
|
IOpenIDUpdate,
|
||||||
|
ISendDelayedEventDetails,
|
||||||
ISendEventDetails,
|
ISendEventDetails,
|
||||||
ITurnServer,
|
ITurnServer,
|
||||||
IReadEventRelationsResult,
|
IReadEventRelationsResult,
|
||||||
|
@ -33,6 +34,7 @@ import {
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
ISearchUserDirectoryResult,
|
ISearchUserDirectoryResult,
|
||||||
IGetMediaConfigResult,
|
IGetMediaConfigResult,
|
||||||
|
UpdateDelayedEventAction,
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
import {
|
import {
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
|
@ -43,6 +45,7 @@ import {
|
||||||
Room,
|
Room,
|
||||||
Direction,
|
Direction,
|
||||||
THREAD_RELATION_TYPE,
|
THREAD_RELATION_TYPE,
|
||||||
|
SendDelayedEventResponse,
|
||||||
StateEvents,
|
StateEvents,
|
||||||
TimelineEvents,
|
TimelineEvents,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
@ -128,6 +131,8 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
|
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
|
||||||
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
||||||
this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`);
|
this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`);
|
||||||
|
this.allowedCapabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||||
|
this.allowedCapabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||||
|
|
||||||
this.allowedCapabilities.add(
|
this.allowedCapabilities.add(
|
||||||
WidgetEventCapability.forRoomEvent(EventDirection.Send, "org.matrix.rageshake_request").raw,
|
WidgetEventCapability.forRoomEvent(EventDirection.Send, "org.matrix.rageshake_request").raw,
|
||||||
|
@ -160,7 +165,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
`_${clientUserId}_${clientDeviceId}`,
|
`_${clientUserId}_${clientDeviceId}`,
|
||||||
).raw,
|
).raw,
|
||||||
);
|
);
|
||||||
// MSC3779 version, with no leading underscore
|
// Version with no leading underscore, for room versions whose auth rules allow it
|
||||||
this.allowedCapabilities.add(
|
this.allowedCapabilities.add(
|
||||||
WidgetEventCapability.forStateEvent(
|
WidgetEventCapability.forStateEvent(
|
||||||
EventDirection.Send,
|
EventDirection.Send,
|
||||||
|
@ -271,20 +276,20 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
public async sendEvent<K extends keyof StateEvents>(
|
public async sendEvent<K extends keyof StateEvents>(
|
||||||
eventType: K,
|
eventType: K,
|
||||||
content: StateEvents[K],
|
content: StateEvents[K],
|
||||||
stateKey?: string,
|
stateKey: string | null,
|
||||||
targetRoomId?: string,
|
targetRoomId: string | null,
|
||||||
): Promise<ISendEventDetails>;
|
): Promise<ISendEventDetails>;
|
||||||
public async sendEvent<K extends keyof TimelineEvents>(
|
public async sendEvent<K extends keyof TimelineEvents>(
|
||||||
eventType: K,
|
eventType: K,
|
||||||
content: TimelineEvents[K],
|
content: TimelineEvents[K],
|
||||||
stateKey: null,
|
stateKey: null,
|
||||||
targetRoomId?: string,
|
targetRoomId: string | null,
|
||||||
): Promise<ISendEventDetails>;
|
): Promise<ISendEventDetails>;
|
||||||
public async sendEvent(
|
public async sendEvent(
|
||||||
eventType: string,
|
eventType: string,
|
||||||
content: IContent,
|
content: IContent,
|
||||||
stateKey?: string | null,
|
stateKey: string | null = null,
|
||||||
targetRoomId?: string,
|
targetRoomId: string | null = null,
|
||||||
): Promise<ISendEventDetails> {
|
): Promise<ISendEventDetails> {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId();
|
const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId();
|
||||||
|
@ -328,6 +333,94 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
return { roomId, eventId: r.event_id };
|
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(
|
public async sendToDevice(
|
||||||
eventType: string,
|
eventType: string,
|
||||||
encrypted: boolean,
|
encrypted: boolean,
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
SimpleObservable,
|
SimpleObservable,
|
||||||
OpenIDRequestState,
|
OpenIDRequestState,
|
||||||
IOpenIDUpdate,
|
IOpenIDUpdate,
|
||||||
|
UpdateDelayedEventAction,
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
import {
|
import {
|
||||||
ApprovalOpts,
|
ApprovalOpts,
|
||||||
|
@ -122,6 +123,8 @@ describe("StopGapWidgetDriver", () => {
|
||||||
"org.matrix.msc3819.receive.to_device:org.matrix.call.sdp_stream_metadata_changed",
|
"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.send.to_device:m.call.replaces",
|
||||||
"org.matrix.msc3819.receive.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
|
// 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", () => {
|
describe("If the feature_dynamic_room_predecessors feature is not enabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||||
|
|
|
@ -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: [] }),
|
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
|
||||||
setDeviceVerified: jest.fn(),
|
setDeviceVerified: jest.fn(),
|
||||||
joinRoom: jest.fn(),
|
joinRoom: jest.fn(),
|
||||||
|
|
|
@ -6938,10 +6938,10 @@ matrix-web-i18n@^3.2.1:
|
||||||
minimist "^1.2.8"
|
minimist "^1.2.8"
|
||||||
walk "^2.3.15"
|
walk "^2.3.15"
|
||||||
|
|
||||||
matrix-widget-api@^1.5.0:
|
matrix-widget-api@^1.8.2:
|
||||||
version "1.7.0"
|
version "1.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.7.0.tgz#ae3b44380f11bb03519d0bf0373dfc3341634667"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.8.2.tgz#28d344502a85593740f560b0f8120e474a054505"
|
||||||
integrity sha512-dzSnA5Va6CeIkyWs89xZty/uv38HLyfjOrHGbbEikCa2ZV0HTkUNtrBMKlrn4CRYyDJ6yoO/3ssRwiR0jJvOkQ==
|
integrity sha512-kdmks3CvFNPIYN669Y4rO13KrazDvX8KHC7i6jOzJs8uZ8s54FNkuRVVyiQHeVCSZG5ixUqW9UuCj9lf03qxTQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/events" "^3.0.0"
|
"@types/events" "^3.0.0"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue