Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change WidgetApi.sendStateEvent to return the Matrix widget Api result. #870

Merged
merged 2 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-peaches-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@matrix-widget-toolkit/api': minor
---

The api package now exposes some utility functions via the `utils` module
5 changes: 5 additions & 0 deletions .changeset/khaki-mayflies-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@matrix-widget-toolkit/api': major
---

WidgetApi.sendStateEvent no longer returns the event. Instead it returns the result of the Matrix Widget API.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const PowerLevelsPage = (): ReactElement => {
),
]}
>
{/*
{/*
The StoreProvider is located here to keep the example small. Normal
applications would located it outside of the router to establish a
single, global store.
Expand Down
18 changes: 5 additions & 13 deletions example-widget-mui/src/PowerLevelsPage/powerLevelsApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,14 @@ describe('getPowerLevels', () => {
});

describe('updatePowerLevels', () => {
it('should update the topic', async () => {
it('should update the power levels', async () => {
const store = createStore({ widgetApi });

await expect(
store
.dispatch(
powerLevelsApi.endpoints.updatePowerLevels.initiate({
users_default: 100,
}),
)
.unwrap(),
).resolves.toMatchObject({
content: {
await store.dispatch(
powerLevelsApi.endpoints.updatePowerLevels.initiate({
users_default: 100,
},
});
}),
);

expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
'm.room.power_levels',
Expand Down
18 changes: 7 additions & 11 deletions example-widget-mui/src/PowerLevelsPage/powerLevelsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import {
PowerLevelsStateEvent,
STATE_EVENT_POWER_LEVELS,
StateEvent,
isValidPowerLevelStateEvent,
} from '@matrix-widget-toolkit/api';
import { EventDirection, WidgetEventCapability } from 'matrix-widget-api';
Expand Down Expand Up @@ -91,11 +90,8 @@ export const powerLevelsApi = baseApi.injectEndpoints({
}),

/** Update the name of the current room */
updatePowerLevels: builder.mutation<
StateEvent<PowerLevelsStateEvent>,
PowerLevelsStateEvent
>({
// Optimistic update the local cache to instantly see the updated room name.
updatePowerLevels: builder.mutation<null, PowerLevelsStateEvent>({
// Optimistic update the local cache to instantly see the updated power levels.
// Undo the change if the query fails.
async onQueryStarted(content, { dispatch, queryFulfilled }) {
const { undo } = dispatch(
Expand Down Expand Up @@ -128,12 +124,12 @@ export const powerLevelsApi = baseApi.injectEndpoints({
),
]);

const newEvent = await widgetApi.sendStateEvent(
STATE_EVENT_POWER_LEVELS,
content,
);
await widgetApi.sendStateEvent(STATE_EVENT_POWER_LEVELS, content);

return { data: newEvent };
// We don't care about the result here.
// When executing the mutation, an optimistic update is already done.
// Otherwise, the new event should come down the sync.
return { data: null };
} catch (e) {
return {
error: {
Expand Down
11 changes: 9 additions & 2 deletions packages/api/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { IModalWidgetOpenRequestDataButton } from 'matrix-widget-api';
import { IModalWidgetReturnData } from 'matrix-widget-api';
import { IOpenIDCredentials } from 'matrix-widget-api';
import { IRoomEvent } from 'matrix-widget-api';
import { ISendEventFromWidgetResponseData } from 'matrix-widget-api';
import { IUploadFileActionFromWidgetResponseData } from 'matrix-widget-api';
import { IWidget } from 'matrix-widget-api';
import { IWidgetApiRequest } from 'matrix-widget-api';
Expand Down Expand Up @@ -91,6 +92,9 @@ export function isValidRedactionEvent(event: RoomEvent<unknown>): event is Redac
// @public
export function isValidRoomMemberStateEvent(event: StateEvent<unknown>): event is StateEvent<RoomMemberStateEventContent>;

// @public
export function makeEventFromSendStateEventResult<T>(type: string, stateKey: string, content: T, sender: string, sendResult: ISendEventFromWidgetResponseData): StateEvent<T>;

// @public
export type MembershipState = 'join' | 'invite' | 'leave' | 'ban' | 'knock';

Expand Down Expand Up @@ -171,6 +175,9 @@ export type RoomMemberStateEventContent = {
avatar_url?: string | null;
};

// @public
export function sendStateEventWithEventResult<T>(widgetApi: WidgetApi, type: string, stateKey: string, content: T): Promise<StateEvent<T>>;

// @public
export const STATE_EVENT_POWER_LEVELS = "m.room.power_levels";

Expand Down Expand Up @@ -222,7 +229,7 @@ export type WidgetApi = {
sendStateEvent<T>(eventType: string, content: T, options?: {
roomId?: string;
stateKey?: string;
}): Promise<StateEvent<T>>;
}): Promise<ISendEventFromWidgetResponseData>;
receiveRoomEvents<T>(eventType: string, options?: {
messageType?: string;
roomIds?: string[] | Symbols.AnyRoom;
Expand Down Expand Up @@ -344,7 +351,7 @@ export class WidgetApiImpl implements WidgetApi {
sendStateEvent<T>(eventType: string, content: T, { roomId, stateKey }?: {
roomId?: string;
stateKey?: string;
}): Promise<StateEvent<T>>;
}): Promise<ISendEventFromWidgetResponseData>;
sendToDeviceMessage<T>(eventType: string, encrypted: boolean, content: {
[userId: string]: {
[deviceId: string | '*']: T;
Expand Down
134 changes: 26 additions & 108 deletions packages/api/src/api/WidgetApiImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1060,149 +1060,72 @@ describe('WidgetApiImpl', () => {

describe('sendStateEvent', () => {
it('should send state event', async () => {
const preventDefault = vi.fn();
const stateEvent = { hello: 'world' };

matrixWidgetApi.sendStateEvent.mockResolvedValue({
matrixWidgetApi.sendStateEvent.mockResolvedValueOnce({
event_id: '$event-id',
room_id: '!current-room',
});
matrixWidgetApi.on.mockImplementationOnce((_, listener) => {
setTimeout(() => {
listener({
detail: {
data: mockRoomEvent({
state_key: '',
content: stateEvent,
}),
},
preventDefault,
});
});

return matrixWidgetApi;
});
matrixWidgetApi.off.mockReturnThis();

await expect(
widgetApi.sendStateEvent('com.example.test', stateEvent),
).resolves.toMatchObject({
event_id: '$event-id',
room_id: '!current-room',
sender: '@my-user-id',
state_key: '',
type: 'com.example.test',
content: stateEvent,
});
expect(matrixWidgetApi.on).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
);
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalled();
expect(matrixWidgetApi.off).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalledWith(
'com.example.test',
'',
stateEvent,
undefined,
);
expect(preventDefault).toHaveBeenCalled();
expect(matrixWidgetApi.transport.reply).toHaveBeenCalled();
});

it('should send state event with custom state key', async () => {
const preventDefault = vi.fn();
const stateEvent = { hello: 'world' };

matrixWidgetApi.sendStateEvent.mockResolvedValue({
matrixWidgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: '!current-room',
event_id: '$event-id',
});
matrixWidgetApi.on.mockImplementationOnce((_, listener) => {
setTimeout(() => {
listener({
detail: {
data: mockRoomEvent({
content: stateEvent,
state_key: 'custom-state-key',
}),
},
preventDefault,
});
});

return matrixWidgetApi;
});
matrixWidgetApi.off.mockReturnThis();

await expect(
widgetApi.sendStateEvent('com.example.test', stateEvent, {
stateKey: 'custom-state-key',
}),
).resolves.toMatchObject({
room_id: '!current-room',
sender: '@my-user-id',
state_key: 'custom-state-key',
type: 'com.example.test',
content: stateEvent,
event_id: '$event-id',
});
expect(matrixWidgetApi.on).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
);
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalled();
expect(matrixWidgetApi.off).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalledWith(
'com.example.test',
'custom-state-key',
stateEvent,
undefined,
);
expect(preventDefault).toHaveBeenCalled();
expect(matrixWidgetApi.transport.reply).toHaveBeenCalled();
});

it('should send state event to another room', async () => {
const preventDefault = vi.fn();
const stateEvent = { hello: 'world' };

matrixWidgetApi.sendStateEvent.mockResolvedValue({
matrixWidgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: '!another-room',
event_id: '$event-id',
});
matrixWidgetApi.on.mockImplementationOnce((_, listener) => {
setTimeout(() => {
listener({
detail: {
data: mockRoomEvent({
state_key: '',
room_id: '!another-room',
content: stateEvent,
}),
},
preventDefault,
});
});

return matrixWidgetApi;
});
matrixWidgetApi.off.mockReturnThis();

await expect(
widgetApi.sendStateEvent('com.example.test', stateEvent, {
roomId: '!another-room',
}),
).resolves.toMatchObject({
room_id: '!another-room',
sender: '@my-user-id',
state_key: '',
type: 'com.example.test',
content: stateEvent,
event_id: '$event-id',
});
expect(matrixWidgetApi.on).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
);
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalled();
expect(matrixWidgetApi.off).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalledWith(
'com.example.test',
'',
stateEvent,
'!another-room',
);
expect(preventDefault).toHaveBeenCalled();
expect(matrixWidgetApi.transport.reply).toHaveBeenCalled();
});

it('should reject on error while sending', async () => {
Expand All @@ -1211,20 +1134,15 @@ describe('WidgetApiImpl', () => {
matrixWidgetApi.sendStateEvent.mockRejectedValue(
new Error('Power to low'),
);
matrixWidgetApi.on.mockReturnThis();
matrixWidgetApi.off.mockReturnThis();

await expect(() =>
widgetApi.sendStateEvent('com.example.test', stateEvent),
).rejects.toThrow('Power to low');
expect(matrixWidgetApi.on).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
);
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalled();
expect(matrixWidgetApi.off).toHaveBeenCalledWith(
'action:send_event',
expect.any(Function),
expect(matrixWidgetApi.sendStateEvent).toHaveBeenCalledWith(
'com.example.test',
'',
stateEvent,
undefined,
);
});
});
Expand Down
40 changes: 9 additions & 31 deletions packages/api/src/api/WidgetApiImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
INotifyCapabilitiesActionRequest,
IOpenIDCredentials,
IRoomEvent,
ISendEventFromWidgetResponseData,
IUploadFileActionFromWidgetResponseData,
IWidgetApiRequest,
IWidgetApiRequestData,
Expand Down Expand Up @@ -419,40 +420,17 @@ export class WidgetApiImpl implements WidgetApi {
}

/** {@inheritDoc WidgetApi.sendStateEvent} */
async sendStateEvent<T>(
sendStateEvent<T>(
eventType: string,
content: T,
{ roomId, stateKey = '' }: { roomId?: string; stateKey?: string } = {},
): Promise<StateEvent<T>> {
const subject = new ReplaySubject<CustomEvent<IWidgetApiRequest>>();
const subscription = this.events$.subscribe((e) => subject.next(e));

try {
const { event_id, room_id } = await this.matrixWidgetApi.sendStateEvent(
eventType,
stateKey,
content,
roomId,
);
// TODO: Why do we even return the event, not just the event id, we never
// need it.
const event = await firstValueFrom(
subject.pipe(
filter((event) => {
const matrixEvent = event.detail.data as unknown as IRoomEvent;

return (
matrixEvent.event_id === event_id &&
matrixEvent.room_id === room_id
);
}),
map((event) => event.detail.data as StateEvent<T>),
),
);
return event;
} finally {
subscription.unsubscribe();
}
): Promise<ISendEventFromWidgetResponseData> {
return this.matrixWidgetApi.sendStateEvent(
eventType,
stateKey,
content,
roomId,
);
}

/** {@inheritDoc WidgetApi.receiveRoomEvents} */
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,9 @@ export type {
WidgetParameters,
WidgetRegistration,
} from './types';
export {
makeEventFromSendStateEventResult,
sendStateEventWithEventResult,
} from './utils';
export { WidgetApiImpl } from './WidgetApiImpl';
export type { WidgetApiOptions } from './WidgetApiImpl';
Loading
Loading