Skip to content

Commit 56a2fa1

Browse files
Fix Powerlevels and add Room Version 12 changes (#980)
* Fix powelevel calulations * Partially fix the PowerLevelsPage Signed-off-by: MTRNord <[email protected]> * Use valid sender IDs Signed-off-by: MTRNord <[email protected]> * Add changeset Signed-off-by: MTRNord <[email protected]> * Fix room version 12 powerlevel tests Signed-off-by: MTRNord <[email protected]> * Fix the test cases and ensure we also work in hydra world Signed-off-by: MTRNord <[email protected]> * Add tests for the changed creator fallback logic in case we have a missing powerlevel Signed-off-by: MTRNord <[email protected]> * Add some more test to the changeset for room version 12 powerlevels Signed-off-by: MTRNord <[email protected]> * Ensure api report is updated Signed-off-by: MTRNord <[email protected]> * Fix review comments Signed-off-by: MTRNord <[email protected]> * Mention UserPowerLevelType in the calculateUserPowerLevel jsdoc Signed-off-by: MTRNord <[email protected]> * Add test for ROOM_VERSION_12_CREATOR selecttion Signed-off-by: MTRNord <[email protected]> * Add tests for isValidCreateEventSchema Signed-off-by: MTRNord <[email protected]> --------- Signed-off-by: MTRNord <[email protected]>
1 parent 09c9126 commit 56a2fa1

32 files changed

+1165
-440
lines changed

.changeset/fruity-bats-fly.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@matrix-widget-toolkit/api': major
3+
'@matrix-widget-toolkit/testing': major
4+
---
5+
6+
Rework powerlevel calculations to comply with spec in all room versions.
7+
8+
Note this now requires the create room event to be passed to the power level functions.
9+
Additionally, the mock widget api now has changed user id and room id defaults to comply with matrix spec.

example-widget-mui/src/AllRoomsPage/AllRoomsPage.test.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { RoomNameEvent } from '../events';
2828
import { AllRoomsPage } from './AllRoomsPage';
2929

3030
function mockRoomNameEvent({
31-
room_id = '!room-id',
31+
room_id = '!room-id:example.com',
3232
content = {},
3333
}: {
3434
room_id?: string;
@@ -118,19 +118,34 @@ describe('<AllRoomsPage />', () => {
118118

119119
it('should render a list of rooms', async () => {
120120
widgetApi.mockSendStateEvent(
121-
mockRoomNameEvent({ room_id: '!room-id-1', content: { name: 'Room 1' } }),
121+
mockRoomNameEvent({
122+
room_id: '!room-id-1:example.com',
123+
content: { name: 'Room 1' },
124+
}),
122125
);
123126
widgetApi.mockSendStateEvent(
124-
mockRoomNameEvent({ room_id: '!room-id-2', content: { name: 'Room 2' } }),
127+
mockRoomNameEvent({
128+
room_id: '!room-id-2:example.com',
129+
content: { name: 'Room 2' },
130+
}),
125131
);
126132
widgetApi.mockSendStateEvent(
127-
mockRoomNameEvent({ room_id: '!room-id-3', content: { name: 'Room 3' } }),
133+
mockRoomNameEvent({
134+
room_id: '!room-id-3:example.com',
135+
content: { name: 'Room 3' },
136+
}),
128137
);
129138
widgetApi.mockSendStateEvent(
130-
mockRoomNameEvent({ room_id: '!room-id-4', content: { name: 'Room 4' } }),
139+
mockRoomNameEvent({
140+
room_id: '!room-id-4:example.com',
141+
content: { name: 'Room 4' },
142+
}),
131143
);
132144
widgetApi.mockSendStateEvent(
133-
mockRoomNameEvent({ room_id: '!room-id-5', content: { name: 'Room 5' } }),
145+
mockRoomNameEvent({
146+
room_id: '!room-id-5:example.com',
147+
content: { name: 'Room 5' },
148+
}),
134149
);
135150

136151
render(<AllRoomsPage />, { wrapper });
@@ -152,7 +167,10 @@ describe('<AllRoomsPage />', () => {
152167
).resolves.toBeInTheDocument();
153168

154169
widgetApi.mockSendStateEvent(
155-
mockRoomNameEvent({ room_id: '!room-id-1', content: { name: 'Room 1' } }),
170+
mockRoomNameEvent({
171+
room_id: '!room-id-1:example.com',
172+
content: { name: 'Room 1' },
173+
}),
156174
);
157175

158176
await userEvent.click(
@@ -167,7 +185,10 @@ describe('<AllRoomsPage />', () => {
167185

168186
it('should navigate to the room', async () => {
169187
widgetApi.mockSendStateEvent(
170-
mockRoomNameEvent({ room_id: '!room-id-1', content: { name: 'Room 1' } }),
188+
mockRoomNameEvent({
189+
room_id: '!room-id-1:example.com',
190+
content: { name: 'Room 1' },
191+
}),
171192
);
172193

173194
render(<AllRoomsPage />, { wrapper });
@@ -176,7 +197,7 @@ describe('<AllRoomsPage />', () => {
176197
await userEvent.click(button);
177198

178199
expect(widgetApi.navigateTo).toHaveBeenCalledWith(
179-
'https://matrix.to/#/!room-id-1',
200+
'https://matrix.to/#/!room-id-1%3Aexample.com',
180201
);
181202
});
182203
});

example-widget-mui/src/DicePage/DicePage.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,15 @@ describe('<DicePage />', () => {
107107
type: 'net.nordeck.throw_dice',
108108
event_id: '$0',
109109
origin_server_ts: 0,
110-
room_id: '!room-id',
110+
room_id: '!room-id:example.com',
111111
sender: '@user-id',
112112
content: { pips: 5 },
113113
});
114114
widgetApi.mockSendRoomEvent({
115115
type: 'net.nordeck.throw_dice',
116116
event_id: '$1',
117117
origin_server_ts: 1,
118-
room_id: '!room-id',
118+
room_id: '!room-id:example.com',
119119
sender: '@user-id',
120120
content: { pips: 3 },
121121
});

example-widget-mui/src/ModalPage/ModalDialog.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ describe('<ModalDialog />', () => {
5353

5454
await expect(screen.findByText(/a title/i)).resolves.toBeInTheDocument();
5555
expect(screen.getByText(/some content/i)).toBeInTheDocument();
56-
expect(screen.getByText(/Room ID: !room-id/i)).toBeInTheDocument();
56+
expect(
57+
screen.getByText(/Room ID: !room-id:example.com/i),
58+
).toBeInTheDocument();
5759
expect(
5860
screen.getByRole('button', { name: /i am confident!/i }),
5961
).toBeInTheDocument();

example-widget-mui/src/PowerLevelsPage/PowerLevelsPage.test.tsx

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,35 +33,48 @@ afterEach(() => widgetApi.stop());
3333

3434
beforeEach(() => {
3535
widgetApi = mockWidgetApi();
36+
// @ts-expect-error - This is a test, we can set the userId directly
37+
widgetApi.widgetParameters.userId = '@user-id:example.com';
3638

39+
widgetApi.mockSendStateEvent({
40+
type: 'm.room.create',
41+
sender: '@user-id:example.com',
42+
state_key: '',
43+
content: {
44+
room_version: '11',
45+
},
46+
origin_server_ts: 0,
47+
event_id: '$create-event-id',
48+
room_id: '!room-id:example.com',
49+
});
3750
widgetApi.mockSendStateEvent({
3851
type: 'm.room.power_levels',
39-
sender: '@user-id',
52+
sender: '@user-id:example.com',
4053
state_key: '',
4154
content: {
42-
users: { '@user-id': 100 },
55+
users: { '@user-id:example.com': 100 },
4356
},
4457
origin_server_ts: 0,
4558
event_id: '$event-id',
46-
room_id: '!room-id',
59+
room_id: '!room-id:example.com',
4760
});
4861
widgetApi.mockSendStateEvent({
4962
type: 'm.room.member',
50-
sender: '@user-id',
51-
state_key: '@another-user',
63+
sender: '@user-id:example.com',
64+
state_key: '@another-user:example.com',
5265
content: { membership: 'join' },
5366
origin_server_ts: 0,
5467
event_id: '$event-id',
55-
room_id: '!room-id',
68+
room_id: '!room-id:example.com',
5669
});
5770
widgetApi.mockSendStateEvent({
5871
type: 'm.room.member',
59-
sender: '@user-id',
60-
state_key: '@user-id',
72+
sender: '@user-id:example.com',
73+
state_key: '@user-id:example.com',
6174
content: { membership: 'join' },
6275
origin_server_ts: 0,
6376
event_id: '$event-id',
64-
room_id: '!room-id',
77+
room_id: '!room-id:example.com',
6578
});
6679

6780
wrapper = ({ children }: PropsWithChildren) => (
@@ -93,7 +106,7 @@ describe('<PowerLevelsPage />', () => {
93106

94107
await userEvent.click(
95108
await within(listbox).findByRole('option', {
96-
name: '@another-user',
109+
name: '@another-user:example.com',
97110
selected: true,
98111
checked: true,
99112
}),
@@ -137,7 +150,7 @@ describe('<PowerLevelsPage />', () => {
137150

138151
await userEvent.click(
139152
within(listbox).getByRole('option', {
140-
name: '@user-id You',
153+
name: '@user-id:example.com You',
141154
selected: false,
142155
}),
143156
);
@@ -146,7 +159,7 @@ describe('<PowerLevelsPage />', () => {
146159
screen.getByRole('combobox', {
147160
name: 'Username',
148161
}),
149-
).toHaveTextContent('@user-id');
162+
).toHaveTextContent('@user-id:example.com');
150163
});
151164

152165
it('should request the capabilities', async () => {
@@ -161,6 +174,10 @@ describe('<PowerLevelsPage />', () => {
161174
EventDirection.Receive,
162175
'm.room.member',
163176
),
177+
WidgetEventCapability.forStateEvent(
178+
EventDirection.Receive,
179+
'm.room.create',
180+
),
164181
]);
165182

166183
const button = await screen.findByRole('button', { name: /promote/i });
@@ -191,7 +208,7 @@ describe('<PowerLevelsPage />', () => {
191208

192209
await userEvent.click(
193210
within(listbox).getByRole('option', {
194-
name: '@user-id You',
211+
name: '@user-id:example.com You',
195212
selected: false,
196213
}),
197214
);
@@ -208,16 +225,16 @@ describe('<PowerLevelsPage />', () => {
208225
it('should disable actions if the user has no power to update the power of others', async () => {
209226
widgetApi.mockSendStateEvent({
210227
type: 'm.room.power_levels',
211-
sender: '@user-id',
228+
sender: '@user-id:example.com',
212229
state_key: '',
213230
content: {
214231
users: {
215-
'@user-id': 0,
232+
'@user-id:example.com': 0,
216233
},
217234
},
218235
origin_server_ts: 0,
219236
event_id: '$event-id',
220-
room_id: '!room-id',
237+
room_id: '!room-id:example.com',
221238
});
222239

223240
render(<PowerLevelsPage />, { wrapper });
@@ -261,8 +278,8 @@ describe('<PowerLevelsPage />', () => {
261278
'm.room.power_levels',
262279
{
263280
users: {
264-
'@another-user': 50,
265-
'@user-id': 100,
281+
'@another-user:example.com': 50,
282+
'@user-id:example.com': 100,
266283
},
267284
},
268285
);
@@ -271,17 +288,17 @@ describe('<PowerLevelsPage />', () => {
271288
it('should demote the user', async () => {
272289
widgetApi.mockSendStateEvent({
273290
type: 'm.room.power_levels',
274-
sender: '@user-id',
291+
sender: '@user-id:example.com',
275292
state_key: '',
276293
content: {
277294
users: {
278-
'@another-user': 50,
279-
'@user-id': 100,
295+
'@another-user:example.com': 50,
296+
'@user-id:example.com': 100,
280297
},
281298
},
282299
origin_server_ts: 0,
283300
event_id: '$event-id',
284-
room_id: '!room-id',
301+
room_id: '!room-id:example.com',
285302
});
286303

287304
render(<PowerLevelsPage />, { wrapper });
@@ -306,8 +323,8 @@ describe('<PowerLevelsPage />', () => {
306323
'm.room.power_levels',
307324
{
308325
users: {
309-
'@another-user': 0,
310-
'@user-id': 100,
326+
'@another-user:example.com': 0,
327+
'@user-id:example.com': 100,
311328
},
312329
},
313330
);

example-widget-mui/src/PowerLevelsPage/PowerLevelsPage.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
hasRoomEventPower,
2121
hasStateEventPower,
2222
PowerLevelsActions,
23+
STATE_EVENT_CREATE,
2324
STATE_EVENT_POWER_LEVELS,
2425
STATE_EVENT_ROOM_MEMBER,
2526
} from '@matrix-widget-toolkit/api';
@@ -48,6 +49,7 @@ import { STATE_EVENT_ROOM_NAME } from '../events';
4849
import { NavigationBar } from '../NavigationPage';
4950
import { StoreProvider } from '../store';
5051
import {
52+
useGetCreateEventQuery,
5153
useGetPowerLevelsQuery,
5254
useUpdatePowerLevelsMutation,
5355
} from './powerLevelsApi';
@@ -90,6 +92,10 @@ export const PowerLevelsPage = (): ReactElement => {
9092
EventDirection.Receive,
9193
STATE_EVENT_ROOM_MEMBER,
9294
),
95+
WidgetEventCapability.forStateEvent(
96+
EventDirection.Receive,
97+
STATE_EVENT_CREATE,
98+
),
9399
]}
94100
>
95101
{/*
@@ -142,6 +148,7 @@ export const PowerLevelsView = (): ReactElement => {
142148

143149
const { data: powerLevelsEvent } = useGetPowerLevelsQuery();
144150
const { data: roomMembersData } = useGetRoomMembersQuery();
151+
const { data: createEvent } = useGetCreateEventQuery();
145152

146153
const [selectedMember, setSelectedMember] = useState<string | undefined>();
147154

@@ -177,17 +184,23 @@ export const PowerLevelsView = (): ReactElement => {
177184
? selectAllRoomMembers(roomMembersData)
178185
: [];
179186

187+
if (selectedMember === undefined) {
188+
return <Box>Loading...</Box>;
189+
}
190+
180191
// check if we (=the user of the widget) has the power to promote or
181192
// demote others
182193
const canPromoteOrDemote = hasStateEventPower(
183194
powerLevelsEvent?.content,
195+
createEvent?.event,
184196
widgetApi.widgetParameters.userId,
185197
STATE_EVENT_POWER_LEVELS,
186198
);
187199

188200
// we assume that users that can change the name can be promoted or demoted
189201
const userIsModerator = hasStateEventPower(
190202
powerLevelsEvent?.content,
203+
createEvent?.event,
191204
selectedMember,
192205
STATE_EVENT_ROOM_NAME,
193206
);
@@ -231,6 +244,7 @@ export const PowerLevelsView = (): ReactElement => {
231244
title={type}
232245
permitted={hasStateEventPower(
233246
powerLevelsEvent?.content,
247+
createEvent?.event,
234248
selectedMember,
235249
type,
236250
)}
@@ -252,6 +266,7 @@ export const PowerLevelsView = (): ReactElement => {
252266
title={type}
253267
permitted={hasRoomEventPower(
254268
powerLevelsEvent?.content,
269+
createEvent?.event,
255270
selectedMember,
256271
type,
257272
)}
@@ -273,6 +288,7 @@ export const PowerLevelsView = (): ReactElement => {
273288
title={action}
274289
permitted={hasActionPower(
275290
powerLevelsEvent?.content,
291+
createEvent?.event,
276292
selectedMember,
277293
action,
278294
)}

0 commit comments

Comments
 (0)