Skip to content

Commit 7961bb3

Browse files
authored
Merge pull request #3466 from element-hq/toger5/waitForNotificationAnswer
Add dialing/ringing state to CallViewModel (`callPickupState$`)
2 parents 075a4cb + c599d6e commit 7961bb3

12 files changed

+447
-217
lines changed

docs/url-params.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
7070
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
7171
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
7272
| `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. |
73+
| `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. |
74+
| `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. |
7375

7476
### Widget-only parameters
7577

src/UrlParams.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,17 @@ export interface UrlConfiguration {
216216
* This is one part to make the call matrixRTC session behave like a telephone call.
217217
*/
218218
autoLeaveWhenOthersLeft: boolean;
219+
220+
/**
221+
* If the client should behave like it is awaiting an answer if a notification was sent (wait for call pick up).
222+
* This is a no-op if not combined with sendNotificationType.
223+
*
224+
* This entails:
225+
* - show ui that it is awaiting an answer
226+
* - play a sound that indicates that it is awaiting an answer
227+
* - auto-dismiss the call widget once the notification lifetime expires on the receivers side.
228+
*/
229+
waitForCallPickup: boolean;
219230
}
220231

221232
// If you need to add a new flag to this interface, prefer a name that describes
@@ -347,6 +358,7 @@ export const getUrlParams = (
347358
returnToLobby: false,
348359
sendNotificationType: "notification" as RTCNotificationType,
349360
autoLeaveWhenOthersLeft: false,
361+
waitForCallPickup: false,
350362
};
351363
switch (intent) {
352364
case UserIntent.StartNewCall:
@@ -366,6 +378,7 @@ export const getUrlParams = (
366378
...inAppDefault,
367379
skipLobby: true,
368380
autoLeaveWhenOthersLeft: true,
381+
waitForCallPickup: true,
369382
};
370383
break;
371384
case UserIntent.JoinExistingCallDM:
@@ -391,6 +404,7 @@ export const getUrlParams = (
391404
returnToLobby: false,
392405
sendNotificationType: undefined,
393406
autoLeaveWhenOthersLeft: false,
407+
waitForCallPickup: false,
394408
};
395409
}
396410

@@ -442,6 +456,7 @@ export const getUrlParams = (
442456
"ring",
443457
"notification",
444458
]),
459+
waitForCallPickup: parser.getFlag("waitForCallPickup"),
445460
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
446461
};
447462

src/livekit/useECConnectionState.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe("Leaking connection prevention", () => {
9494
test("Should cancel pending connections when the component is unmounted", async () => {
9595
const connectCall = vi.fn();
9696
const pendingConnection = Promise.withResolvers<void>();
97-
// let pendingDisconnection = defer<void>()
97+
// let pendingDisconnection = Promise.withResolvers<void>()
9898
const disconnectMock = vi.fn();
9999

100100
const mockRoom = {
@@ -142,7 +142,7 @@ describe("Leaking connection prevention", () => {
142142
test("Should cancel about to open but not yet opened connection", async () => {
143143
const createTracksCall = vi.fn();
144144
const pendingCreateTrack = Promise.withResolvers<void>();
145-
// let pendingDisconnection = defer<void>()
145+
// let pendingDisconnection = Promise.withResolvers<void>()
146146
const disconnectMock = vi.fn();
147147
const connectMock = vi.fn();
148148

src/room/CallEventAudioRenderer.test.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ import { act } from "react";
1919
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
2020

2121
import { mockRtcMembership } from "../utils/test";
22-
import {
23-
CallEventAudioRenderer,
24-
MAX_PARTICIPANT_COUNT_FOR_SOUND,
25-
} from "./CallEventAudioRenderer";
22+
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
2623
import { useAudioContext } from "../useAudioContext";
2724
import { prefetchSounds } from "../soundUtils";
2825
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
@@ -33,6 +30,7 @@ import {
3330
local,
3431
localRtcMember,
3532
} from "../utils/test-fixtures";
33+
import { MAX_PARTICIPANT_COUNT_FOR_SOUND } from "../state/CallViewModel";
3634

3735
vitest.mock("../useAudioContext");
3836
vitest.mock("../soundUtils");
@@ -158,6 +156,7 @@ test("should not play a sound when a hand raise is retracted", () => {
158156
]);
159157
render(<CallEventAudioRenderer vm={vm} />);
160158

159+
playSound.mockClear();
161160
act(() => {
162161
handRaisedSubject$.next({
163162
["foo"]: {
@@ -172,7 +171,7 @@ test("should not play a sound when a hand raise is retracted", () => {
172171
},
173172
});
174173
});
175-
expect(playSound).toHaveBeenCalledTimes(2);
174+
expect(playSound).toHaveBeenCalledExactlyOnceWith("raiseHand");
176175
act(() => {
177176
handRaisedSubject$.next({
178177
["foo"]: {
@@ -182,5 +181,5 @@ test("should not play a sound when a hand raise is retracted", () => {
182181
},
183182
});
184183
});
185-
expect(playSound).toHaveBeenCalledTimes(2);
184+
expect(playSound).toHaveBeenCalledExactlyOnceWith("raiseHand");
186185
});

src/room/CallEventAudioRenderer.tsx

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ Please see LICENSE in the repository root for full details.
66
*/
77

88
import { type ReactNode, useEffect } from "react";
9-
import { filter, interval, throttle } from "rxjs";
109

1110
import { type CallViewModel } from "../state/CallViewModel";
1211
import joinCallSoundMp3 from "../sound/join_call.mp3";
@@ -21,11 +20,6 @@ import { useAudioContext } from "../useAudioContext";
2120
import { prefetchSounds } from "../soundUtils";
2221
import { useLatest } from "../useLatest";
2322

24-
// Do not play any sounds if the participant count has exceeded this
25-
// number.
26-
export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8;
27-
export const THROTTLE_SOUND_EFFECT_MS = 500;
28-
2923
export const callEventAudioSounds = prefetchSounds({
3024
join: {
3125
mp3: joinCallSoundMp3,
@@ -60,37 +54,18 @@ export function CallEventAudioRenderer({
6054
const audioEngineRef = useLatest(audioEngineCtx);
6155

6256
useEffect(() => {
63-
const joinSub = vm.participantChanges$
64-
.pipe(
65-
filter(
66-
({ joined, ids }) =>
67-
ids.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND && joined.length > 0,
68-
),
69-
throttle(() => interval(THROTTLE_SOUND_EFFECT_MS)),
70-
)
71-
.subscribe(() => {
72-
void audioEngineRef.current?.playSound("join");
73-
});
74-
75-
const leftSub = vm.participantChanges$
76-
.pipe(
77-
filter(
78-
({ ids, left }) =>
79-
ids.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND && left.length > 0,
80-
),
81-
throttle(() => interval(THROTTLE_SOUND_EFFECT_MS)),
82-
)
83-
.subscribe(() => {
84-
void audioEngineRef.current?.playSound("left");
85-
});
86-
87-
const handRaisedSub = vm.newHandRaised$.subscribe(() => {
88-
void audioEngineRef.current?.playSound("raiseHand");
89-
});
90-
91-
const screenshareSub = vm.newScreenShare$.subscribe(() => {
92-
void audioEngineRef.current?.playSound("screenshareStarted");
93-
});
57+
const joinSub = vm.joinSoundEffect$.subscribe(
58+
() => void audioEngineRef.current?.playSound("join"),
59+
);
60+
const leftSub = vm.leaveSoundEffect$.subscribe(
61+
() => void audioEngineRef.current?.playSound("left"),
62+
);
63+
const handRaisedSub = vm.newHandRaised$.subscribe(
64+
() => void audioEngineRef.current?.playSound("raiseHand"),
65+
);
66+
const screenshareSub = vm.newScreenShare$.subscribe(
67+
() => void audioEngineRef.current?.playSound("screenshareStarted"),
68+
);
9469

9570
return (): void => {
9671
joinSub.unsubscribe();

src/room/GroupCallView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,6 @@ export const GroupCallView: FC<Props> = ({
453453
matrixInfo={matrixInfo}
454454
rtcSession={rtcSession as MatrixRTCSession}
455455
matrixRoom={room}
456-
participantCount={participantCount}
457456
onLeave={onLeave}
458457
header={header}
459458
muteStates={muteStates}

src/room/InCallView.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ function createInCallView(): RenderResult & {
177177
}}
178178
matrixRoom={room}
179179
livekitRoom={livekitRoom}
180-
participantCount={0}
181180
onLeave={function (): void {
182181
throw new Error("Function not implemented.");
183182
}}

src/room/InCallView.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ export interface InCallViewProps {
216216
matrixRoom: MatrixRoom;
217217
livekitRoom: LivekitRoom;
218218
muteStates: MuteStates;
219-
participantCount: number;
220219
/** Function to call when the user explicitly ends the call */
221220
onLeave: () => void;
222221
header: HeaderStyle;
@@ -233,7 +232,6 @@ export const InCallView: FC<InCallViewProps> = ({
233232
matrixRoom,
234233
livekitRoom,
235234
muteStates,
236-
participantCount,
237235
onLeave,
238236
header: headerStyle,
239237
connState,
@@ -312,6 +310,7 @@ export const InCallView: FC<InCallViewProps> = ({
312310
() => void toggleRaisedHand(),
313311
);
314312

313+
const participantCount = useBehavior(vm.participantCount$);
315314
const reconnecting = useBehavior(vm.reconnecting$);
316315
const windowMode = useBehavior(vm.windowMode$);
317316
const layout = useBehavior(vm.layout$);
@@ -322,7 +321,7 @@ export const InCallView: FC<InCallViewProps> = ({
322321
const showFooter = useBehavior(vm.showFooter$);
323322
const earpieceMode = useBehavior(vm.earpieceMode$);
324323
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
325-
useSubscription(vm.autoLeaveWhenOthersLeft$, onLeave);
324+
useSubscription(vm.autoLeave$, onLeave);
326325

327326
// Ideally we could detect taps by listening for click events and checking
328327
// that the pointerType of the event is "touch", but this isn't yet supported

src/room/__snapshots__/InCallView.test.tsx.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,34 @@ exports[`InCallView > rendering > renders 1`] = `
4949
</svg>
5050
</span>
5151
</div>
52+
<div
53+
class="participantsLine"
54+
>
55+
<svg
56+
aria-label="Participants"
57+
fill="currentColor"
58+
height="20"
59+
viewBox="0 0 24 24"
60+
width="20"
61+
xmlns="http://www.w3.org/2000/svg"
62+
>
63+
<path
64+
d="M9.175 13.825Q10.35 15 12 15t2.825-1.175T16 11t-1.175-2.825T12 7 9.175 8.175 8 11t1.175 2.825m4.237-1.412A1.93 1.93 0 0 1 12 13q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 11q0-.825.588-1.412A1.93 1.93 0 0 1 12 9q.825 0 1.412.588Q14 10.175 14 11t-.588 1.412"
65+
/>
66+
<path
67+
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0"
68+
/>
69+
<path
70+
d="M16.23 18.792a13 13 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0q-.73.18-1.455.455a8 8 0 0 1-1.729-1.454q1.336-.618 2.709-.95A13.8 13.8 0 0 1 12 16q1.65 0 3.25.387 1.373.333 2.709.95a8 8 0 0 1-1.73 1.455"
71+
/>
72+
</svg>
73+
<span
74+
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41"
75+
data-testid="roomHeader_participants_count"
76+
>
77+
2
78+
</span>
79+
</div>
5280
</div>
5381
</div>
5482
<div

0 commit comments

Comments
 (0)