mirror of https://github.com/vector-im/riot-web
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.dbkr/sss
parent
a35bf68f22
commit
a437c677bb
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue