Skip to content

Commit 8b67916

Browse files
committed
waitForNotificationAnswer
Signed-off-by: Timo K <toger5@hotmail.de>
1 parent 31892f4 commit 8b67916

File tree

6 files changed

+374
-47
lines changed

6 files changed

+374
-47
lines changed

src/UrlParams.ts

Lines changed: 12 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 show behave like it is awaiting an answer if a notification was sent.
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+
awaitingAnswer: boolean;
219230
}
220231

221232
// If you need to add a new flag to this interface, prefer a name that describes
@@ -442,6 +453,7 @@ export const getUrlParams = (
442453
"ring",
443454
"notification",
444455
]),
456+
awaitingAnswer: parser.getFlag("showAwaitingAnswerFeedback"),
445457
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
446458
};
447459

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.tsx

Lines changed: 1 addition & 2 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$);

src/state/CallViewModel.test.ts

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
Please see LICENSE in the repository root for full details.
66
*/
77

8-
import { test, vi, onTestFinished, it } from "vitest";
8+
import { test, vi, onTestFinished, it, describe } from "vitest";
99
import {
1010
BehaviorSubject,
1111
combineLatest,
@@ -30,6 +30,9 @@ import * as ComponentsCore from "@livekit/components-core";
3030
import {
3131
type CallMembership,
3232
type MatrixRTCSession,
33+
type IRTCNotificationContent,
34+
type ICallNotifyContent,
35+
MatrixRTCSessionEvent,
3336
} from "matrix-js-sdk/lib/matrixrtc";
3437
import { deepCompare } from "matrix-js-sdk/lib/utils";
3538

@@ -1199,6 +1202,205 @@ test("autoLeaveWhenOthersLeft$ doesn't emits when autoLeaveWhenOthersLeft option
11991202
});
12001203
});
12011204

1205+
describe("waitForNotificationAnswer$", () => {
1206+
test("unknown -> ringing -> timeout when notified and nobody joins", () => {
1207+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1208+
// No one ever joins (only local user)
1209+
withCallViewModel(
1210+
scope.behavior(hot("a", { a: [] }), []), // remote participants
1211+
scope.behavior(hot("a", { a: [localRtcMember] }), []), // rtc members
1212+
of(ConnectionState.Connected),
1213+
new Map(),
1214+
mockMediaDevices({}),
1215+
(vm, rtcSession) => {
1216+
// Fire a call notification at 10ms with lifetime 30ms
1217+
schedule(" 10ms r", {
1218+
r: () => {
1219+
rtcSession.emit(
1220+
MatrixRTCSessionEvent.DidSendCallNotification,
1221+
{ lifetime: 30 } as unknown as IRTCNotificationContent,
1222+
{} as unknown as ICallNotifyContent,
1223+
);
1224+
},
1225+
});
1226+
1227+
expectObservable(vm.waitForNotificationAnswer$).toBe(
1228+
"a 9ms b 29ms c",
1229+
{ a: "unknown", b: "ringing", c: "timeout" },
1230+
);
1231+
},
1232+
{
1233+
waitForNotificationAnswer: true,
1234+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1235+
},
1236+
);
1237+
});
1238+
});
1239+
1240+
test("ringing -> success if someone joins before timeout", () => {
1241+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1242+
// Someone joins at 20ms (both LiveKit participant and MatrixRTC member)
1243+
const remote$ = scope.behavior(
1244+
hot("a--b", { a: [], b: [aliceParticipant] }),
1245+
[],
1246+
);
1247+
const rtc$ = scope.behavior(
1248+
hot("a--b", {
1249+
a: [localRtcMember],
1250+
b: [localRtcMember, aliceRtcMember],
1251+
}),
1252+
[],
1253+
);
1254+
1255+
withCallViewModel(
1256+
remote$,
1257+
rtc$,
1258+
of(ConnectionState.Connected),
1259+
new Map(),
1260+
mockMediaDevices({}),
1261+
(vm, rtcSession) => {
1262+
// Notify at 5ms so we enter ringing, then success at 20ms
1263+
schedule(" 5ms r", {
1264+
r: () => {
1265+
rtcSession.emit(
1266+
MatrixRTCSessionEvent.DidSendCallNotification,
1267+
{ lifetime: 100 } as unknown as IRTCNotificationContent,
1268+
{} as unknown as ICallNotifyContent,
1269+
);
1270+
},
1271+
});
1272+
1273+
expectObservable(vm.waitForNotificationAnswer$).toBe("a 2ms c", {
1274+
a: "unknown",
1275+
c: "success",
1276+
});
1277+
},
1278+
{
1279+
waitForNotificationAnswer: true,
1280+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1281+
},
1282+
);
1283+
});
1284+
});
1285+
1286+
test("success when someone joins before we notify", () => {
1287+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1288+
// Join at 10ms, notify later at 20ms (state should stay success)
1289+
const remote$ = scope.behavior(
1290+
hot("a-b", { a: [], b: [aliceParticipant] }),
1291+
[],
1292+
);
1293+
const rtc$ = scope.behavior(
1294+
hot("a-b", {
1295+
a: [localRtcMember],
1296+
b: [localRtcMember, aliceRtcMember],
1297+
}),
1298+
[],
1299+
);
1300+
1301+
withCallViewModel(
1302+
remote$,
1303+
rtc$,
1304+
of(ConnectionState.Connected),
1305+
new Map(),
1306+
mockMediaDevices({}),
1307+
(vm, rtcSession) => {
1308+
schedule(" 20ms r", {
1309+
r: () => {
1310+
rtcSession.emit(
1311+
MatrixRTCSessionEvent.DidSendCallNotification,
1312+
{ lifetime: 50 } as unknown as IRTCNotificationContent,
1313+
{} as unknown as ICallNotifyContent,
1314+
);
1315+
},
1316+
});
1317+
expectObservable(vm.waitForNotificationAnswer$).toBe("a 1ms b", {
1318+
a: "unknown",
1319+
b: "success",
1320+
});
1321+
},
1322+
{
1323+
waitForNotificationAnswer: true,
1324+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1325+
},
1326+
);
1327+
});
1328+
});
1329+
1330+
test("notify without lifetime -> immediate timeout", () => {
1331+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1332+
withCallViewModel(
1333+
scope.behavior(hot("a", { a: [] }), []),
1334+
scope.behavior(hot("a", { a: [localRtcMember] }), []),
1335+
of(ConnectionState.Connected),
1336+
new Map(),
1337+
mockMediaDevices({}),
1338+
(vm, rtcSession) => {
1339+
schedule(" 10ms r", {
1340+
r: () => {
1341+
rtcSession.emit(
1342+
MatrixRTCSessionEvent.DidSendCallNotification,
1343+
{ lifetime: 0 } as unknown as IRTCNotificationContent, // no lifetime
1344+
{} as unknown as ICallNotifyContent,
1345+
);
1346+
},
1347+
});
1348+
expectObservable(vm.waitForNotificationAnswer$).toBe("a 9ms b", {
1349+
a: "unknown",
1350+
b: "timeout",
1351+
});
1352+
},
1353+
{
1354+
waitForNotificationAnswer: true,
1355+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1356+
},
1357+
);
1358+
});
1359+
});
1360+
1361+
test("stays null when waitForNotificationAnswer=false", () => {
1362+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1363+
const remote$ = scope.behavior(
1364+
hot("a--b", { a: [], b: [aliceParticipant] }),
1365+
[],
1366+
);
1367+
const rtc$ = scope.behavior(
1368+
hot("a--b", {
1369+
a: [localRtcMember],
1370+
b: [localRtcMember, aliceRtcMember],
1371+
}),
1372+
[],
1373+
);
1374+
1375+
withCallViewModel(
1376+
remote$,
1377+
rtc$,
1378+
of(ConnectionState.Connected),
1379+
new Map(),
1380+
mockMediaDevices({}),
1381+
(vm, rtcSession) => {
1382+
schedule(" 5ms r", {
1383+
r: () => {
1384+
rtcSession.emit(
1385+
MatrixRTCSessionEvent.DidSendCallNotification,
1386+
{ lifetime: 30 } as unknown as IRTCNotificationContent,
1387+
{} as unknown as ICallNotifyContent,
1388+
);
1389+
},
1390+
});
1391+
expectObservable(vm.waitForNotificationAnswer$).toBe("(n)", {
1392+
n: null,
1393+
});
1394+
},
1395+
{
1396+
waitForNotificationAnswer: false,
1397+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1398+
},
1399+
);
1400+
});
1401+
});
1402+
});
1403+
12021404
test("audio output changes when toggling earpiece mode", () => {
12031405
withTestScheduler(({ schedule, expectObservable }) => {
12041406
getUrlParams.mockReturnValue({ controlledAudioDevices: true });

0 commit comments

Comments
 (0)