Skip to content

Commit fc449bc

Browse files
authored
Merge pull request #3467 from element-hq/toger5/call-pickup-state-decline-event
View model for decline logic
2 parents c8d3d58 + 1e32b35 commit fc449bc

File tree

5 files changed

+273
-84
lines changed

5 files changed

+273
-84
lines changed

src/room/InCallView.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
158158
};
159159
}, [livekitRoom]);
160160

161-
const { autoLeaveWhenOthersLeft } = useUrlParams();
161+
const { autoLeaveWhenOthersLeft, sendNotificationType, waitForCallPickup } =
162+
useUrlParams();
162163

163164
useEffect(() => {
164165
if (livekitRoom !== undefined) {
@@ -171,6 +172,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
171172
{
172173
encryptionSystem: props.e2eeSystem,
173174
autoLeaveWhenOthersLeft,
175+
waitForCallPickup:
176+
waitForCallPickup && sendNotificationType === "ring",
174177
},
175178
connStateObservable$,
176179
reactionsReader.raisedHands$,
@@ -190,6 +193,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
190193
props.e2eeSystem,
191194
connStateObservable$,
192195
autoLeaveWhenOthersLeft,
196+
sendNotificationType,
197+
waitForCallPickup,
193198
]);
194199

195200
if (livekitRoom === undefined || vm === null) return null;

src/state/CallViewModel.test.ts

Lines changed: 180 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
2231
import {
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+
240266
interface 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

13861537
test("audio output changes when toggling earpiece mode", () => {

0 commit comments

Comments
 (0)