Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit b81582d

Browse files
authored
Improve live voice broadcast detection by testing if the started event has been redacted (#9780)
1 parent fbc3228 commit b81582d

15 files changed

+367
-85
lines changed

src/utils/arrays.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,13 @@ export const concat = (...arrays: Uint8Array[]): Uint8Array => {
313313
return concatenated;
314314
}, new Uint8Array(0));
315315
};
316+
317+
/**
318+
* Async version of Array.every.
319+
*/
320+
export async function asyncEvery<T>(values: T[], predicate: (value: T) => Promise<boolean>): Promise<boolean> {
321+
for (const value of values) {
322+
if (!(await predicate(value))) return false;
323+
}
324+
return true;
325+
}

src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,34 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { useState } from "react";
17+
import { useContext, useEffect, useMemo, useState } from "react";
1818
import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
1919

2020
import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast";
2121
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
22+
import { SDKContext } from "../../contexts/SDKContext";
2223

2324
export const useHasRoomLiveVoiceBroadcast = (room: Room) => {
24-
const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(hasRoomLiveVoiceBroadcast(room).hasBroadcast);
25-
26-
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, () => {
27-
setHasLiveVoiceBroadcast(hasRoomLiveVoiceBroadcast(room).hasBroadcast);
28-
});
29-
25+
const sdkContext = useContext(SDKContext);
26+
const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(false);
27+
28+
const update = useMemo(() => {
29+
return sdkContext.client
30+
? () => {
31+
hasRoomLiveVoiceBroadcast(sdkContext.client!, room).then(
32+
({ hasBroadcast }) => {
33+
setHasLiveVoiceBroadcast(hasBroadcast);
34+
},
35+
() => {}, // no update on error
36+
);
37+
}
38+
: () => {}; // noop without client
39+
}, [room, sdkContext, setHasLiveVoiceBroadcast]);
40+
41+
useEffect(() => {
42+
update();
43+
}, [update]);
44+
45+
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, () => update());
3046
return hasLiveVoiceBroadcast;
3147
};

src/voice-broadcast/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export * from "./utils/getChunkLength";
4949
export * from "./utils/getMaxBroadcastLength";
5050
export * from "./utils/hasRoomLiveVoiceBroadcast";
5151
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";
52+
export * from "./utils/retrieveStartedInfoEvent";
5253
export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile";
5354
export * from "./utils/shouldDisplayAsVoiceBroadcastTile";
5455
export * from "./utils/shouldDisplayAsVoiceBroadcastStoppedText";

src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ const showOthersAlreadyRecordingDialog = () => {
6767
});
6868
};
6969

70-
export const checkVoiceBroadcastPreConditions = (
70+
export const checkVoiceBroadcastPreConditions = async (
7171
room: Room,
7272
client: MatrixClient,
7373
recordingsStore: VoiceBroadcastRecordingsStore,
74-
): boolean => {
74+
): Promise<boolean> => {
7575
if (recordingsStore.getCurrent()) {
7676
showAlreadyRecordingDialog();
7777
return false;
@@ -86,7 +86,7 @@ export const checkVoiceBroadcastPreConditions = (
8686
return false;
8787
}
8888

89-
const { hasBroadcast, startedByUser } = hasRoomLiveVoiceBroadcast(room, currentUserId);
89+
const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId);
9090

9191
if (hasBroadcast && startedByUser) {
9292
showAlreadyRecordingDialog();

src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ import {
3434
* @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore
3535
* @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore
3636
*/
37-
export const doMaybeSetCurrentVoiceBroadcastPlayback = (
37+
export const doMaybeSetCurrentVoiceBroadcastPlayback = async (
3838
room: Room,
3939
client: MatrixClient,
4040
voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore,
4141
voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore,
42-
): void => {
42+
): Promise<void> => {
4343
// do not disturb the current recording
4444
if (voiceBroadcastRecordingsStore.hasCurrent()) return;
4545

@@ -50,7 +50,7 @@ export const doMaybeSetCurrentVoiceBroadcastPlayback = (
5050
return;
5151
}
5252

53-
const { infoEvent } = hasRoomLiveVoiceBroadcast(room);
53+
const { infoEvent } = await hasRoomLiveVoiceBroadcast(client, room);
5454

5555
if (infoEvent) {
5656
// live broadcast in the room + no recording + not listening yet: set the current broadcast

src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
17+
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
1818

19-
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
19+
import { retrieveStartedInfoEvent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
20+
import { asyncEvery } from "../../utils/arrays";
2021

2122
interface Result {
2223
// whether there is a live broadcast in the room
@@ -27,22 +28,26 @@ interface Result {
2728
startedByUser: boolean;
2829
}
2930

30-
export const hasRoomLiveVoiceBroadcast = (room: Room, userId?: string): Result => {
31+
export const hasRoomLiveVoiceBroadcast = async (client: MatrixClient, room: Room, userId?: string): Promise<Result> => {
3132
let hasBroadcast = false;
3233
let startedByUser = false;
3334
let infoEvent: MatrixEvent | null = null;
3435

3536
const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType);
36-
stateEvents.every((event: MatrixEvent) => {
37+
await asyncEvery(stateEvents, async (event: MatrixEvent) => {
3738
const state = event.getContent()?.state;
3839

3940
if (state && state !== VoiceBroadcastInfoState.Stopped) {
41+
const startEvent = await retrieveStartedInfoEvent(event, client);
42+
43+
// skip if started voice broadcast event is redacted
44+
if (startEvent?.isRedacted()) return true;
45+
4046
hasBroadcast = true;
41-
infoEvent = event;
47+
infoEvent = startEvent;
4248

4349
// state key = sender's MXID
4450
if (event.getStateKey() === userId) {
45-
infoEvent = event;
4651
startedByUser = true;
4752
// break here, because more than true / true is not possible
4853
return false;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
18+
19+
import { VoiceBroadcastInfoState } from "..";
20+
21+
export const retrieveStartedInfoEvent = async (
22+
event: MatrixEvent,
23+
client: MatrixClient,
24+
): Promise<MatrixEvent | null> => {
25+
// started event passed as argument
26+
if (event.getContent()?.state === VoiceBroadcastInfoState.Started) return event;
27+
28+
const relatedEventId = event.getRelation()?.event_id;
29+
30+
// no related event
31+
if (!relatedEventId) return null;
32+
33+
const roomId = event.getRoomId() || "";
34+
const relatedEventFromRoom = client.getRoom(roomId)?.findEventById(relatedEventId);
35+
36+
// event found
37+
if (relatedEventFromRoom) return relatedEventFromRoom;
38+
39+
try {
40+
const relatedEventData = await client.fetchRoomEvent(roomId, relatedEventId);
41+
return new MatrixEvent(relatedEventData);
42+
} catch (e) {}
43+
44+
return null;
45+
};

src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const startBroadcast = async (
3838
const userId = client.getUserId();
3939

4040
if (!userId) {
41-
reject("unable to start voice broadcast if current user is unkonwn");
41+
reject("unable to start voice broadcast if current user is unknown");
4242
return promise;
4343
}
4444

@@ -88,7 +88,7 @@ export const startNewVoiceBroadcastRecording = async (
8888
playbacksStore: VoiceBroadcastPlaybacksStore,
8989
recordingsStore: VoiceBroadcastRecordingsStore,
9090
): Promise<VoiceBroadcastRecording | null> => {
91-
if (!checkVoiceBroadcastPreConditions(room, client, recordingsStore)) {
91+
if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) {
9292
return null;
9393
}
9494

test/components/views/rooms/RoomTile-test.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,40 +32,45 @@ import {
3232
useMockedCalls,
3333
setupAsyncStoreWithClient,
3434
filterConsole,
35+
flushPromises,
3536
} from "../../../test-utils";
3637
import { CallStore } from "../../../../src/stores/CallStore";
3738
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
3839
import { DefaultTagID } from "../../../../src/stores/room-list/models";
3940
import DMRoomMap from "../../../../src/utils/DMRoomMap";
40-
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
4141
import PlatformPeg from "../../../../src/PlatformPeg";
4242
import BasePlatform from "../../../../src/BasePlatform";
4343
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
4444
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
4545
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
46+
import { TestSdkContext } from "../../../TestSdkContext";
47+
import { SDKContext } from "../../../../src/contexts/SDKContext";
4648

4749
describe("RoomTile", () => {
4850
jest.spyOn(PlatformPeg, "get").mockReturnValue({
4951
overrideBrowserShortcuts: () => false,
5052
} as unknown as BasePlatform);
5153
useMockedCalls();
5254

53-
const setUpVoiceBroadcast = (state: VoiceBroadcastInfoState): void => {
55+
const setUpVoiceBroadcast = async (state: VoiceBroadcastInfoState): Promise<void> => {
5456
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
5557
room.roomId,
5658
state,
57-
client.getUserId(),
58-
client.getDeviceId(),
59+
client.getSafeUserId(),
60+
client.getDeviceId()!,
5961
);
6062

61-
act(() => {
63+
await act(async () => {
6264
room.currentState.setStateEvents([voiceBroadcastInfoEvent]);
65+
await flushPromises();
6366
});
6467
};
6568

6669
const renderRoomTile = (): void => {
6770
renderResult = render(
68-
<RoomTile room={room} showMessagePreview={false} isMinimized={false} tag={DefaultTagID.Untagged} />,
71+
<SDKContext.Provider value={sdkContext}>
72+
<RoomTile room={room} showMessagePreview={false} isMinimized={false} tag={DefaultTagID.Untagged} />
73+
</SDKContext.Provider>,
6974
);
7075
};
7176

@@ -74,15 +79,18 @@ describe("RoomTile", () => {
7479
let voiceBroadcastInfoEvent: MatrixEvent;
7580
let room: Room;
7681
let renderResult: RenderResult;
82+
let sdkContext: TestSdkContext;
7783

7884
beforeEach(() => {
85+
sdkContext = new TestSdkContext();
86+
7987
restoreConsole = filterConsole(
8088
// irrelevant for this test
8189
"Room !1:example.org does not have an m.room.create event",
8290
);
8391

84-
stubClient();
85-
client = mocked(MatrixClientPeg.get());
92+
client = mocked(stubClient());
93+
sdkContext.client = client;
8694
DMRoomMap.makeShared();
8795

8896
room = new Room("!1:example.org", client, "@alice:example.org", {
@@ -136,7 +144,7 @@ describe("RoomTile", () => {
136144

137145
// Insert an await point in the connection method so we can inspect
138146
// the intermediate connecting state
139-
let completeConnection: () => void;
147+
let completeConnection: () => void = () => {};
140148
const connectionCompleted = new Promise<void>((resolve) => (completeConnection = resolve));
141149
jest.spyOn(call, "performConnection").mockReturnValue(connectionCompleted);
142150

@@ -180,8 +188,8 @@ describe("RoomTile", () => {
180188
});
181189

182190
describe("and a live broadcast starts", () => {
183-
beforeEach(() => {
184-
setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
191+
beforeEach(async () => {
192+
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
185193
});
186194

187195
it("should still render the call subtitle", () => {
@@ -192,25 +200,26 @@ describe("RoomTile", () => {
192200
});
193201

194202
describe("when a live voice broadcast starts", () => {
195-
beforeEach(() => {
196-
setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
203+
beforeEach(async () => {
204+
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
197205
});
198206

199207
it("should render the »Live« subtitle", () => {
200208
expect(screen.queryByText("Live")).toBeInTheDocument();
201209
});
202210

203211
describe("and the broadcast stops", () => {
204-
beforeEach(() => {
212+
beforeEach(async () => {
205213
const stopEvent = mkVoiceBroadcastInfoStateEvent(
206214
room.roomId,
207215
VoiceBroadcastInfoState.Stopped,
208-
client.getUserId(),
209-
client.getDeviceId(),
216+
client.getSafeUserId(),
217+
client.getDeviceId()!,
210218
voiceBroadcastInfoEvent,
211219
);
212-
act(() => {
220+
await act(async () => {
213221
room.currentState.setStateEvents([stopEvent]);
222+
await flushPromises();
214223
});
215224
});
216225

0 commit comments

Comments
 (0)