@@ -18,7 +18,16 @@ import {
18
18
of ,
19
19
switchMap ,
20
20
} from "rxjs" ;
21
- import { ClientEvent , SyncState , type MatrixClient } from "matrix-js-sdk" ;
21
+ import {
22
+ ClientEvent ,
23
+ SyncState ,
24
+ type MatrixClient ,
25
+ RoomEvent as MatrixRoomEvent ,
26
+ MatrixEvent ,
27
+ type IRoomTimelineData ,
28
+ EventType ,
29
+ type IEvent ,
30
+ } from "matrix-js-sdk" ;
22
31
import {
23
32
ConnectionState ,
24
33
type LocalParticipant ,
@@ -237,6 +246,23 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
237
246
) ;
238
247
}
239
248
249
+ function mockRingEvent (
250
+ eventId : string ,
251
+ lifetimeMs : number | undefined ,
252
+ sender = local . userId ,
253
+ ) : { event_id : string } & IRTCNotificationContent {
254
+ return {
255
+ event_id : eventId ,
256
+ ...( lifetimeMs === undefined ? { } : { lifetime : lifetimeMs } ) ,
257
+ notification_type : "ring" ,
258
+ sender,
259
+ } as unknown as { event_id : string } & IRTCNotificationContent ;
260
+ }
261
+
262
+ // The app doesn't really care about the content of these legacy events, we just
263
+ // need a value to fill in for them when emitting notifications
264
+ const mockLegacyRingEvent = { } as { event_id : string } & ICallNotifyContent ;
265
+
240
266
interface CallViewModelInputs {
241
267
remoteParticipants$ : Behavior < RemoteParticipant [ ] > ;
242
268
rtcMembers$ : Behavior < Partial < CallMembership > [ ] > ;
@@ -1205,10 +1231,8 @@ describe("waitForCallPickup$", () => {
1205
1231
r : ( ) => {
1206
1232
rtcSession . emit (
1207
1233
MatrixRTCSessionEvent . DidSendCallNotification ,
1208
- { lifetime : 30 } as unknown as {
1209
- event_id : string ;
1210
- } & IRTCNotificationContent ,
1211
- { } as unknown as { event_id : string } & ICallNotifyContent ,
1234
+ mockRingEvent ( "$notif1" , 30 ) ,
1235
+ mockLegacyRingEvent ,
1212
1236
) ;
1213
1237
} ,
1214
1238
} ) ;
@@ -1247,12 +1271,8 @@ describe("waitForCallPickup$", () => {
1247
1271
r : ( ) => {
1248
1272
rtcSession . emit (
1249
1273
MatrixRTCSessionEvent . DidSendCallNotification ,
1250
- { lifetime : 100 } as unknown as {
1251
- event_id : string ;
1252
- } & IRTCNotificationContent ,
1253
- { } as unknown as {
1254
- event_id : string ;
1255
- } & ICallNotifyContent ,
1274
+ mockRingEvent ( "$notif2" , 100 ) ,
1275
+ mockLegacyRingEvent ,
1256
1276
) ;
1257
1277
} ,
1258
1278
} ) ;
@@ -1290,12 +1310,8 @@ describe("waitForCallPickup$", () => {
1290
1310
r : ( ) => {
1291
1311
rtcSession . emit (
1292
1312
MatrixRTCSessionEvent . DidSendCallNotification ,
1293
- { lifetime : 50 } as unknown as {
1294
- event_id : string ;
1295
- } & IRTCNotificationContent ,
1296
- { } as unknown as {
1297
- event_id : string ;
1298
- } & ICallNotifyContent ,
1313
+ mockRingEvent ( "$notif3" , 50 ) ,
1314
+ mockLegacyRingEvent ,
1299
1315
) ;
1300
1316
} ,
1301
1317
} ) ;
@@ -1321,12 +1337,8 @@ describe("waitForCallPickup$", () => {
1321
1337
r : ( ) => {
1322
1338
rtcSession . emit (
1323
1339
MatrixRTCSessionEvent . DidSendCallNotification ,
1324
- { } as unknown as {
1325
- event_id : string ;
1326
- } & IRTCNotificationContent , // no lifetime
1327
- { } as unknown as {
1328
- event_id : string ;
1329
- } & ICallNotifyContent ,
1340
+ mockRingEvent ( "$notif4" , undefined ) ,
1341
+ mockLegacyRingEvent ,
1330
1342
) ;
1331
1343
} ,
1332
1344
} ) ;
@@ -1361,12 +1373,8 @@ describe("waitForCallPickup$", () => {
1361
1373
r : ( ) => {
1362
1374
rtcSession . emit (
1363
1375
MatrixRTCSessionEvent . DidSendCallNotification ,
1364
- { lifetime : 30 } as unknown as {
1365
- event_id : string ;
1366
- } & IRTCNotificationContent ,
1367
- { } as unknown as {
1368
- event_id : string ;
1369
- } & ICallNotifyContent ,
1376
+ mockRingEvent ( "$notif5" , 30 ) ,
1377
+ mockLegacyRingEvent ,
1370
1378
) ;
1371
1379
} ,
1372
1380
} ) ;
@@ -1381,6 +1389,149 @@ describe("waitForCallPickup$", () => {
1381
1389
) ;
1382
1390
} ) ;
1383
1391
} ) ;
1392
+
1393
+ test ( "decline before timeout window ends -> decline" , ( ) => {
1394
+ withTestScheduler ( ( { schedule, expectObservable } ) => {
1395
+ withCallViewModel (
1396
+ { } ,
1397
+ ( vm , rtcSession ) => {
1398
+ // Notify at 10ms with 50ms lifetime, decline at 40ms with matching id
1399
+ schedule ( " 10ms r 29ms d" , {
1400
+ r : ( ) => {
1401
+ rtcSession . emit (
1402
+ MatrixRTCSessionEvent . DidSendCallNotification ,
1403
+ mockRingEvent ( "$decl1" , 50 ) ,
1404
+ mockLegacyRingEvent ,
1405
+ ) ;
1406
+ } ,
1407
+ d : ( ) => {
1408
+ // Emit decline timeline event with id matching the notification
1409
+ rtcSession . room . emit (
1410
+ MatrixRoomEvent . Timeline ,
1411
+ new MatrixEvent ( {
1412
+ type : EventType . RTCDecline ,
1413
+ content : {
1414
+ "m.relates_to" : {
1415
+ rel_type : "m.reference" ,
1416
+ event_id : "$decl1" ,
1417
+ } ,
1418
+ } ,
1419
+ } ) ,
1420
+ rtcSession . room ,
1421
+ undefined ,
1422
+ false ,
1423
+ { } as IRoomTimelineData ,
1424
+ ) ;
1425
+ } ,
1426
+ } ) ;
1427
+ expectObservable ( vm . callPickupState$ ) . toBe ( "a 9ms b 29ms e" , {
1428
+ a : "unknown" ,
1429
+ b : "ringing" ,
1430
+ e : "decline" ,
1431
+ } ) ;
1432
+ } ,
1433
+ {
1434
+ waitForCallPickup : true ,
1435
+ encryptionSystem : { kind : E2eeType . PER_PARTICIPANT } ,
1436
+ } ,
1437
+ ) ;
1438
+ } ) ;
1439
+ } ) ;
1440
+
1441
+ test ( "decline after timeout window ends -> stays timeout" , ( ) => {
1442
+ withTestScheduler ( ( { schedule, expectObservable } ) => {
1443
+ withCallViewModel (
1444
+ { } ,
1445
+ ( vm , rtcSession ) => {
1446
+ // Notify at 10ms with 20ms lifetime (timeout at 30ms), decline at 40ms
1447
+ schedule ( " 10ms r 20ms t 10ms d" , {
1448
+ r : ( ) => {
1449
+ rtcSession . emit (
1450
+ MatrixRTCSessionEvent . DidSendCallNotification ,
1451
+ mockRingEvent ( "$decl2" , 20 ) ,
1452
+ mockLegacyRingEvent ,
1453
+ ) ;
1454
+ } ,
1455
+ t : ( ) => { } ,
1456
+ d : ( ) => {
1457
+ rtcSession . room . emit (
1458
+ MatrixRoomEvent . Timeline ,
1459
+ new MatrixEvent ( { event_id : "$decl2" , type : "m.rtc.decline" } ) ,
1460
+ rtcSession . room ,
1461
+ undefined ,
1462
+ false ,
1463
+ { } as IRoomTimelineData ,
1464
+ ) ;
1465
+ } ,
1466
+ } ) ;
1467
+ expectObservable ( vm . callPickupState$ ) . toBe ( "a 9ms b 19ms c" , {
1468
+ a : "unknown" ,
1469
+ b : "ringing" ,
1470
+ c : "timeout" ,
1471
+ } ) ;
1472
+ } ,
1473
+ {
1474
+ waitForCallPickup : true ,
1475
+ encryptionSystem : { kind : E2eeType . PER_PARTICIPANT } ,
1476
+ } ,
1477
+ ) ;
1478
+ } ) ;
1479
+ } ) ;
1480
+
1481
+ function testStaysRinging ( declineEvent : Partial < IEvent > ) : void {
1482
+ withTestScheduler ( ( { schedule, expectObservable } ) => {
1483
+ withCallViewModel (
1484
+ { } ,
1485
+ ( vm , rtcSession ) => {
1486
+ // Notify at 10ms with id A, decline arrives at 20ms with id B
1487
+ schedule ( " 10ms r 10ms d" , {
1488
+ r : ( ) => {
1489
+ rtcSession . emit (
1490
+ MatrixRTCSessionEvent . DidSendCallNotification ,
1491
+ mockRingEvent ( "$right" , 50 ) ,
1492
+ mockLegacyRingEvent ,
1493
+ ) ;
1494
+ } ,
1495
+ d : ( ) => {
1496
+ rtcSession . room . emit (
1497
+ MatrixRoomEvent . Timeline ,
1498
+ new MatrixEvent ( declineEvent ) ,
1499
+ rtcSession . room ,
1500
+ undefined ,
1501
+ false ,
1502
+ { } as IRoomTimelineData ,
1503
+ ) ;
1504
+ } ,
1505
+ } ) ;
1506
+ // We assert up to 21ms to see the ringing at 10ms and no change at 20ms
1507
+ expectObservable ( vm . callPickupState$ , "21ms !" ) . toBe ( "a 9ms b" , {
1508
+ a : "unknown" ,
1509
+ b : "ringing" ,
1510
+ } ) ;
1511
+ } ,
1512
+ {
1513
+ waitForCallPickup : true ,
1514
+ encryptionSystem : { kind : E2eeType . PER_PARTICIPANT } ,
1515
+ } ,
1516
+ ) ;
1517
+ } ) ;
1518
+ }
1519
+
1520
+ test ( "decline with wrong id is ignored (stays ringing)" , ( ) => {
1521
+ testStaysRinging ( {
1522
+ event_id : "$wrong" ,
1523
+ type : "m.rtc.decline" ,
1524
+ sender : local . userId ,
1525
+ } ) ;
1526
+ } ) ;
1527
+
1528
+ test ( "decline with sender being the local user is ignored (stays ringing)" , ( ) => {
1529
+ testStaysRinging ( {
1530
+ event_id : "$right" ,
1531
+ type : "m.rtc.decline" ,
1532
+ sender : alice . userId ,
1533
+ } ) ;
1534
+ } ) ;
1384
1535
} ) ;
1385
1536
1386
1537
test ( "audio output changes when toggling earpiece mode" , ( ) => {
0 commit comments