@@ -18,7 +18,16 @@ import {
1818 of ,
1919 switchMap ,
2020} 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" ;
2231import {
2332 ConnectionState ,
2433 type LocalParticipant ,
@@ -237,6 +246,23 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
237246 ) ;
238247}
239248
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+
240266interface CallViewModelInputs {
241267 remoteParticipants$ : Behavior < RemoteParticipant [ ] > ;
242268 rtcMembers$ : Behavior < Partial < CallMembership > [ ] > ;
@@ -1205,10 +1231,8 @@ describe("waitForCallPickup$", () => {
12051231 r : ( ) => {
12061232 rtcSession . emit (
12071233 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 ,
12121236 ) ;
12131237 } ,
12141238 } ) ;
@@ -1247,12 +1271,8 @@ describe("waitForCallPickup$", () => {
12471271 r : ( ) => {
12481272 rtcSession . emit (
12491273 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 ,
12561276 ) ;
12571277 } ,
12581278 } ) ;
@@ -1290,12 +1310,8 @@ describe("waitForCallPickup$", () => {
12901310 r : ( ) => {
12911311 rtcSession . emit (
12921312 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 ,
12991315 ) ;
13001316 } ,
13011317 } ) ;
@@ -1321,12 +1337,8 @@ describe("waitForCallPickup$", () => {
13211337 r : ( ) => {
13221338 rtcSession . emit (
13231339 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 ,
13301342 ) ;
13311343 } ,
13321344 } ) ;
@@ -1361,12 +1373,8 @@ describe("waitForCallPickup$", () => {
13611373 r : ( ) => {
13621374 rtcSession . emit (
13631375 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 ,
13701378 ) ;
13711379 } ,
13721380 } ) ;
@@ -1381,6 +1389,149 @@ describe("waitForCallPickup$", () => {
13811389 ) ;
13821390 } ) ;
13831391 } ) ;
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+ } ) ;
13841535} ) ;
13851536
13861537test ( "audio output changes when toggling earpiece mode" , ( ) => {
0 commit comments