Skip to content

Commit e475f56

Browse files
committed
waitForNotificationAnswer
Signed-off-by: Timo K <toger5@hotmail.de>
1 parent 9486ed5 commit e475f56

File tree

6 files changed

+384
-47
lines changed

6 files changed

+384
-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: 213 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 EventEmitter from "events";
1010
import {
1111
BehaviorSubject,
@@ -32,6 +32,9 @@ import {
3232
Status,
3333
type CallMembership,
3434
type MatrixRTCSession,
35+
type IRTCNotificationContent,
36+
type ICallNotifyContent,
37+
MatrixRTCSessionEvent,
3538
} from "matrix-js-sdk/lib/matrixrtc";
3639
import { deepCompare } from "matrix-js-sdk/lib/utils";
3740

@@ -1228,6 +1231,215 @@ test("autoLeaveWhenOthersLeft$ doesn't emits when autoLeaveWhenOthersLeft option
12281231
});
12291232
});
12301233

1234+
describe("waitForNotificationAnswer$", () => {
1235+
test("unknown -> ringing -> timeout when notified and nobody joins", () => {
1236+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1237+
// No one ever joins (only local user)
1238+
withCallViewModel(
1239+
{
1240+
remoteParticipants$: scope.behavior(hot("a", { a: [] }), []),
1241+
rtcMembers$: scope.behavior(hot("a", { a: [localRtcMember] }), []),
1242+
connectionState$: of(ConnectionState.Connected),
1243+
speaking: new Map(),
1244+
mediaDevices: mockMediaDevices({}),
1245+
},
1246+
(vm, rtcSession) => {
1247+
// Fire a call notification at 10ms with lifetime 30ms
1248+
schedule(" 10ms r", {
1249+
r: () => {
1250+
rtcSession.emit(
1251+
MatrixRTCSessionEvent.DidSendCallNotification,
1252+
{ lifetime: 30 } as unknown as IRTCNotificationContent,
1253+
{} as unknown as ICallNotifyContent,
1254+
);
1255+
},
1256+
});
1257+
1258+
expectObservable(vm.waitForNotificationAnswer$).toBe(
1259+
"a 9ms b 29ms c",
1260+
{ a: "unknown", b: "ringing", c: "timeout" },
1261+
);
1262+
},
1263+
{
1264+
waitForNotificationAnswer: true,
1265+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1266+
},
1267+
);
1268+
});
1269+
});
1270+
1271+
test("ringing -> success if someone joins before timeout", () => {
1272+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1273+
// Someone joins at 20ms (both LiveKit participant and MatrixRTC member)
1274+
const remote$ = scope.behavior(
1275+
hot("a--b", { a: [], b: [aliceParticipant] }),
1276+
[],
1277+
);
1278+
const rtc$ = scope.behavior(
1279+
hot("a--b", {
1280+
a: [localRtcMember],
1281+
b: [localRtcMember, aliceRtcMember],
1282+
}),
1283+
[],
1284+
);
1285+
1286+
withCallViewModel(
1287+
{
1288+
remoteParticipants$: remote$,
1289+
rtcMembers$: rtc$,
1290+
connectionState$: of(ConnectionState.Connected),
1291+
speaking: new Map(),
1292+
mediaDevices: mockMediaDevices({}),
1293+
},
1294+
(vm, rtcSession) => {
1295+
// Notify at 5ms so we enter ringing, then success at 20ms
1296+
schedule(" 5ms r", {
1297+
r: () => {
1298+
rtcSession.emit(
1299+
MatrixRTCSessionEvent.DidSendCallNotification,
1300+
{ lifetime: 100 } as unknown as IRTCNotificationContent,
1301+
{} as unknown as ICallNotifyContent,
1302+
);
1303+
},
1304+
});
1305+
1306+
expectObservable(vm.waitForNotificationAnswer$).toBe("a 2ms c", {
1307+
a: "unknown",
1308+
c: "success",
1309+
});
1310+
},
1311+
{
1312+
waitForNotificationAnswer: true,
1313+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1314+
},
1315+
);
1316+
});
1317+
});
1318+
1319+
test("success when someone joins before we notify", () => {
1320+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1321+
// Join at 10ms, notify later at 20ms (state should stay success)
1322+
const remote$ = scope.behavior(
1323+
hot("a-b", { a: [], b: [aliceParticipant] }),
1324+
[],
1325+
);
1326+
const rtc$ = scope.behavior(
1327+
hot("a-b", {
1328+
a: [localRtcMember],
1329+
b: [localRtcMember, aliceRtcMember],
1330+
}),
1331+
[],
1332+
);
1333+
1334+
withCallViewModel(
1335+
{
1336+
remoteParticipants$: remote$,
1337+
rtcMembers$: rtc$,
1338+
connectionState$: of(ConnectionState.Connected),
1339+
speaking: new Map(),
1340+
mediaDevices: mockMediaDevices({}),
1341+
},
1342+
(vm, rtcSession) => {
1343+
schedule(" 20ms r", {
1344+
r: () => {
1345+
rtcSession.emit(
1346+
MatrixRTCSessionEvent.DidSendCallNotification,
1347+
{ lifetime: 50 } as unknown as IRTCNotificationContent,
1348+
{} as unknown as ICallNotifyContent,
1349+
);
1350+
},
1351+
});
1352+
expectObservable(vm.waitForNotificationAnswer$).toBe("a 1ms b", {
1353+
a: "unknown",
1354+
b: "success",
1355+
});
1356+
},
1357+
{
1358+
waitForNotificationAnswer: true,
1359+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1360+
},
1361+
);
1362+
});
1363+
});
1364+
1365+
test("notify without lifetime -> immediate timeout", () => {
1366+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1367+
withCallViewModel(
1368+
{
1369+
remoteParticipants$: scope.behavior(hot("a", { a: [] }), []),
1370+
rtcMembers$: scope.behavior(hot("a", { a: [localRtcMember] }), []),
1371+
connectionState$: of(ConnectionState.Connected),
1372+
speaking: new Map(),
1373+
mediaDevices: mockMediaDevices({}),
1374+
},
1375+
(vm, rtcSession) => {
1376+
schedule(" 10ms r", {
1377+
r: () => {
1378+
rtcSession.emit(
1379+
MatrixRTCSessionEvent.DidSendCallNotification,
1380+
{ lifetime: 0 } as unknown as IRTCNotificationContent, // no lifetime
1381+
{} as unknown as ICallNotifyContent,
1382+
);
1383+
},
1384+
});
1385+
expectObservable(vm.waitForNotificationAnswer$).toBe("a 9ms b", {
1386+
a: "unknown",
1387+
b: "timeout",
1388+
});
1389+
},
1390+
{
1391+
waitForNotificationAnswer: true,
1392+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1393+
},
1394+
);
1395+
});
1396+
});
1397+
1398+
test("stays null when waitForNotificationAnswer=false", () => {
1399+
withTestScheduler(({ hot, schedule, expectObservable, scope }) => {
1400+
const remote$ = scope.behavior(
1401+
hot("a--b", { a: [], b: [aliceParticipant] }),
1402+
[],
1403+
);
1404+
const rtc$ = scope.behavior(
1405+
hot("a--b", {
1406+
a: [localRtcMember],
1407+
b: [localRtcMember, aliceRtcMember],
1408+
}),
1409+
[],
1410+
);
1411+
1412+
withCallViewModel(
1413+
{
1414+
remoteParticipants$: remote$,
1415+
rtcMembers$: rtc$,
1416+
connectionState$: of(ConnectionState.Connected),
1417+
speaking: new Map(),
1418+
mediaDevices: mockMediaDevices({}),
1419+
},
1420+
(vm, rtcSession) => {
1421+
schedule(" 5ms r", {
1422+
r: () => {
1423+
rtcSession.emit(
1424+
MatrixRTCSessionEvent.DidSendCallNotification,
1425+
{ lifetime: 30 } as unknown as IRTCNotificationContent,
1426+
{} as unknown as ICallNotifyContent,
1427+
);
1428+
},
1429+
});
1430+
expectObservable(vm.waitForNotificationAnswer$).toBe("(n)", {
1431+
n: null,
1432+
});
1433+
},
1434+
{
1435+
waitForNotificationAnswer: false,
1436+
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
1437+
},
1438+
);
1439+
});
1440+
});
1441+
});
1442+
12311443
test("audio output changes when toggling earpiece mode", () => {
12321444
withTestScheduler(({ schedule, expectObservable }) => {
12331445
getUrlParams.mockReturnValue({ controlledAudioDevices: true });

0 commit comments

Comments
 (0)