You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-09 10:22:46 +03:00
Reapply "Distinguish room state and timeline events in embedded clients" (#4790)
This reverts commit fd9a44e701
.
We are ready to reintroduce support for the `update_state` widget action (https://github.com/matrix-org/matrix-spec-proposals/pull/4237) now that matrix-rust-sdk is about to gain support for it as well.
This commit is contained in:
@@ -28,7 +28,6 @@ import {
|
|||||||
WidgetApiToWidgetAction,
|
WidgetApiToWidgetAction,
|
||||||
MatrixCapabilities,
|
MatrixCapabilities,
|
||||||
type ITurnServer,
|
type ITurnServer,
|
||||||
type IRoomEvent,
|
|
||||||
type IOpenIDCredentials,
|
type IOpenIDCredentials,
|
||||||
type ISendEventFromWidgetResponseData,
|
type ISendEventFromWidgetResponseData,
|
||||||
WidgetApiResponseError,
|
WidgetApiResponseError,
|
||||||
@@ -634,12 +633,20 @@ describe("RoomWidgetClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("receives", async () => {
|
it("receives", async () => {
|
||||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||||
|
// Client needs to be told that the room state is loaded
|
||||||
|
widgetApi.emit(
|
||||||
|
`action:${WidgetApiToWidgetAction.UpdateState}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
|
||||||
|
);
|
||||||
|
await init;
|
||||||
|
|
||||||
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
||||||
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
|
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
|
||||||
|
// Let's assume that a state event comes in but it doesn't actually
|
||||||
|
// update the state of the room just yet (maybe it's unauthorized)
|
||||||
widgetApi.emit(
|
widgetApi.emit(
|
||||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||||
@@ -648,26 +655,43 @@ describe("RoomWidgetClient", () => {
|
|||||||
// The client should've emitted about the received event
|
// The client should've emitted about the received event
|
||||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||||
// It should've also inserted the event into the room object
|
// However it should not have changed the room state
|
||||||
const room = client.getRoom("!1:example.org");
|
const room = client.getRoom("!1:example.org");
|
||||||
expect(room).not.toBeNull();
|
expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);
|
||||||
|
|
||||||
|
// Now assume that the state event becomes favored by state
|
||||||
|
// resolution for whatever reason and enters into the current state
|
||||||
|
// of the room
|
||||||
|
widgetApi.emit(
|
||||||
|
`action:${WidgetApiToWidgetAction.UpdateState}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
|
||||||
|
detail: { data: { state: [event] } },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// It should now have changed the room state
|
||||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("backfills", async () => {
|
it("ignores state updates for other rooms", async () => {
|
||||||
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
|
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||||
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
|
// Client needs to be told that the room state is loaded
|
||||||
? [event as IRoomEvent]
|
widgetApi.emit(
|
||||||
: [],
|
`action:${WidgetApiToWidgetAction.UpdateState}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
|
||||||
);
|
);
|
||||||
|
await init;
|
||||||
|
|
||||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
// Now a room we're not interested in receives a state update
|
||||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
widgetApi.emit(
|
||||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
`action:${WidgetApiToWidgetAction.UpdateState}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
|
||||||
const room = client.getRoom("!1:example.org");
|
detail: { data: { state: [{ ...event, room_id: "!other-room:example.org" }] } },
|
||||||
expect(room).not.toBeNull();
|
}),
|
||||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
);
|
||||||
|
// No change to the room state
|
||||||
|
for (const room of client.getRooms()) {
|
||||||
|
expect(room.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -28,6 +28,7 @@ import {
|
|||||||
type WidgetApiAction,
|
type WidgetApiAction,
|
||||||
type IWidgetApiResponse,
|
type IWidgetApiResponse,
|
||||||
type IWidgetApiResponseData,
|
type IWidgetApiResponseData,
|
||||||
|
type IUpdateStateToWidgetActionRequest,
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
|
|
||||||
import { MatrixEvent, type IEvent, type IContent, EventStatus } from "./models/event.ts";
|
import { MatrixEvent, type IEvent, type IContent, EventStatus } from "./models/event.ts";
|
||||||
@@ -135,6 +136,7 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: ()
|
|||||||
export class RoomWidgetClient extends MatrixClient {
|
export class RoomWidgetClient extends MatrixClient {
|
||||||
private room?: Room;
|
private room?: Room;
|
||||||
private readonly widgetApiReady: Promise<void>;
|
private readonly widgetApiReady: Promise<void>;
|
||||||
|
private readonly roomStateSynced: Promise<void>;
|
||||||
private lifecycle?: AbortController;
|
private lifecycle?: AbortController;
|
||||||
private syncState: SyncState | null = null;
|
private syncState: SyncState | null = null;
|
||||||
|
|
||||||
@@ -188,6 +190,11 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
|
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
|
||||||
|
this.roomStateSynced = capabilities.receiveState?.length
|
||||||
|
? new Promise<void>((resolve) =>
|
||||||
|
this.widgetApi.once(`action:${WidgetApiToWidgetAction.UpdateState}`, resolve),
|
||||||
|
)
|
||||||
|
: Promise.resolve();
|
||||||
|
|
||||||
// Request capabilities for the functionality this client needs to support
|
// Request capabilities for the functionality this client needs to support
|
||||||
if (
|
if (
|
||||||
@@ -240,6 +247,7 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
|
|
||||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||||
|
widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||||
|
|
||||||
// Open communication with the host
|
// Open communication with the host
|
||||||
widgetApi.start();
|
widgetApi.start();
|
||||||
@@ -275,28 +283,6 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
|
|
||||||
await this.widgetApiReady;
|
await this.widgetApiReady;
|
||||||
|
|
||||||
// Backfill the requested events
|
|
||||||
// We only get the most recent event for every type + state key combo,
|
|
||||||
// so it doesn't really matter what order we inject them in
|
|
||||||
await Promise.all(
|
|
||||||
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
|
|
||||||
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
|
|
||||||
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
|
|
||||||
|
|
||||||
if (this.syncApi instanceof SyncApi) {
|
|
||||||
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
|
|
||||||
// -> state events in `timelineEventList` will update the state.
|
|
||||||
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
|
|
||||||
} else {
|
|
||||||
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
|
|
||||||
}
|
|
||||||
events.forEach((event) => {
|
|
||||||
this.emit(ClientEvent.Event, event);
|
|
||||||
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
|
||||||
});
|
|
||||||
}) ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (opts.clientWellKnownPollPeriod !== undefined) {
|
if (opts.clientWellKnownPollPeriod !== undefined) {
|
||||||
this.clientWellKnownIntervalID = setInterval(() => {
|
this.clientWellKnownIntervalID = setInterval(() => {
|
||||||
this.fetchClientWellKnown();
|
this.fetchClientWellKnown();
|
||||||
@@ -304,8 +290,9 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
this.fetchClientWellKnown();
|
this.fetchClientWellKnown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.roomStateSynced;
|
||||||
this.setSyncState(SyncState.Syncing);
|
this.setSyncState(SyncState.Syncing);
|
||||||
logger.info("Finished backfilling events");
|
logger.info("Finished initial sync");
|
||||||
|
|
||||||
this.matrixRTC.start();
|
this.matrixRTC.start();
|
||||||
|
|
||||||
@@ -316,6 +303,7 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
public stopClient(): void {
|
public stopClient(): void {
|
||||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||||
|
this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||||
|
|
||||||
super.stopClient();
|
super.stopClient();
|
||||||
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
|
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
|
||||||
@@ -600,36 +588,15 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
// Only inject once we have update the txId
|
// Only inject once we have update the txId
|
||||||
await this.updateTxId(event);
|
await this.updateTxId(event);
|
||||||
|
|
||||||
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
|
|
||||||
if (this.syncApi instanceof SyncApi) {
|
if (this.syncApi instanceof SyncApi) {
|
||||||
// The code will want to be something like:
|
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||||
// ```
|
|
||||||
// if (!params.addToTimeline && !params.addToState) {
|
|
||||||
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
|
|
||||||
// // -> state events part of the `timelineEventList` parameter will update the state.
|
|
||||||
// this.injectRoomEvents(this.room!, [], undefined, [event]);
|
|
||||||
// } else {
|
|
||||||
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
|
|
||||||
// }
|
|
||||||
// ```
|
|
||||||
|
|
||||||
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
|
|
||||||
// -> state events in `timelineEventList` will update the state.
|
|
||||||
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
|
|
||||||
} else {
|
} else {
|
||||||
// The code will want to be something like:
|
// Sliding Sync
|
||||||
// ```
|
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||||
// if (!params.addToTimeline && !params.addToState) {
|
|
||||||
// this.injectRoomEvents(this.room!, [], [event]);
|
|
||||||
// } else {
|
|
||||||
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
|
|
||||||
// }
|
|
||||||
// ```
|
|
||||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
|
|
||||||
}
|
}
|
||||||
this.emit(ClientEvent.Event, event);
|
this.emit(ClientEvent.Event, event);
|
||||||
this.setSyncState(SyncState.Syncing);
|
this.setSyncState(SyncState.Syncing);
|
||||||
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||||
} else {
|
} else {
|
||||||
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||||
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
|
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
|
||||||
@@ -654,6 +621,32 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
await this.ack(ev);
|
await this.ack(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
for (const rawEvent of ev.detail.data.state) {
|
||||||
|
// Verify the room ID matches, since it's possible for the client to
|
||||||
|
// send us state updates from other rooms if this widget is always
|
||||||
|
// on screen
|
||||||
|
if (rawEvent.room_id === this.roomId) {
|
||||||
|
const event = new MatrixEvent(rawEvent as Partial<IEvent>);
|
||||||
|
|
||||||
|
if (this.syncApi instanceof SyncApi) {
|
||||||
|
await this.syncApi.injectRoomEvents(this.room!, undefined, [event]);
|
||||||
|
} else {
|
||||||
|
// Sliding Sync
|
||||||
|
await this.syncApi!.injectRoomEvents(this.room!, [event]);
|
||||||
|
}
|
||||||
|
logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
|
||||||
|
} else {
|
||||||
|
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||||
|
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.ack(ev);
|
||||||
|
};
|
||||||
|
|
||||||
private async watchTurnServers(): Promise<void> {
|
private async watchTurnServers(): Promise<void> {
|
||||||
const servers = this.widgetApi.getTurnServers();
|
const servers = this.widgetApi.getTurnServers();
|
||||||
const onClientStopped = (): void => {
|
const onClientStopped = (): void => {
|
||||||
|
Reference in New Issue
Block a user