Skip to content

Commit 28ec9c7

Browse files
robintowntoger5
authored andcommitted
Allow sending notification events when starting a call
1 parent 9b8f288 commit 28ec9c7

File tree

2 files changed

+97
-5
lines changed

2 files changed

+97
-5
lines changed

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ describe("MatrixRTCSession", () => {
340340
};
341341
});
342342
});
343-
sendEventMock = jest.fn();
343+
sendEventMock = jest.fn().mockResolvedValue(undefined);
344344
client.sendStateEvent = sendStateEventMock;
345345
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
346346
client.sendEvent = sendEventMock;
@@ -369,6 +369,51 @@ describe("MatrixRTCSession", () => {
369369
expect(sess!.isJoined()).toEqual(true);
370370
});
371371

372+
it("sends a notification when starting a call", async () => {
373+
// Simulate a join, including the update to the room state
374+
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
375+
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
376+
mockRoomState(mockRoom, [{ ...membershipTemplate, user_id: client.getUserId()! }]);
377+
sess!.onRTCSessionMemberUpdate();
378+
379+
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
380+
"application": "m.call",
381+
"m.mentions": { user_ids: [], room: true },
382+
"notify_type": "ring",
383+
"call_id": "",
384+
});
385+
});
386+
387+
it("doesn't send a notification when joining an existing call", async () => {
388+
// Add another member to the call so that it is considered an existing call
389+
mockRoomState(mockRoom, [membershipTemplate]);
390+
sess!.onRTCSessionMemberUpdate();
391+
392+
// Simulate a join, including the update to the room state
393+
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
394+
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
395+
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
396+
sess!.onRTCSessionMemberUpdate();
397+
398+
expect(client.sendEvent).not.toHaveBeenCalled();
399+
});
400+
401+
it("doesn't send a notification when someone else starts the call faster than us", async () => {
402+
// Simulate a join, including the update to the room state
403+
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
404+
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
405+
// But this time we want to simulate a race condition in which we receive a state event
406+
// from someone else, starting the call before our own state event has been sent
407+
mockRoomState(mockRoom, [membershipTemplate]);
408+
sess!.onRTCSessionMemberUpdate();
409+
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
410+
sess!.onRTCSessionMemberUpdate();
411+
412+
// We assume that the responsibility to send a notification, if any, lies with the other
413+
// participant that won the race
414+
expect(client.sendEvent).not.toHaveBeenCalled();
415+
});
416+
372417
it("sends a membership event when joining a call", async () => {
373418
const realSetTimeout = setTimeout;
374419
jest.useFakeTimers();

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { MembershipManager } from "./NewMembershipManager.ts";
2828
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
2929
import { LegacyMembershipManager } from "./LegacyMembershipManager.ts";
3030
import { logDurationSync } from "../utils.ts";
31-
import { type Statistics } from "./types.ts";
31+
import { type Statistics, type CallNotifyType, isMyMembership } from "./types.ts";
3232
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
3333
import type { IMembershipManager } from "./IMembershipManager.ts";
3434
import {
@@ -65,6 +65,15 @@ export type MatrixRTCSessionEventHandlerMap = {
6565
) => void;
6666
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
6767
};
68+
69+
export interface SessionConfig {
70+
/**
71+
* What kind of notification to send when starting the session.
72+
* @default `undefined` (no notification)
73+
*/
74+
notifyType?: CallNotifyType;
75+
}
76+
6877
// The names follow these principles:
6978
// - we use the technical term delay if the option is related to delayed events.
7079
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
@@ -167,7 +176,7 @@ export interface EncryptionConfig {
167176
*/
168177
useKeyDelay?: number;
169178
}
170-
export type JoinSessionConfig = MembershipConfig & EncryptionConfig;
179+
export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionConfig;
171180

172181
/**
173182
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
@@ -181,7 +190,15 @@ export class MatrixRTCSession extends TypedEventEmitter<
181190
private encryptionManager?: IEncryptionManager;
182191
// The session Id of the call, this is the call_id of the call Member event.
183192
private _callId: string | undefined;
193+
private joinConfig?: SessionConfig;
184194
private logger: Logger;
195+
196+
/**
197+
* Whether we're trying to join the session but still waiting for room state
198+
* to reflect our own membership.
199+
*/
200+
private joining = false;
201+
185202
/**
186203
* This timeout is responsible to track any expiration. We need to know when we have to start
187204
* to ignore other call members. There is no callback for this. This timeout will always be configured to
@@ -429,6 +446,11 @@ export class MatrixRTCSession extends TypedEventEmitter<
429446
);
430447
}
431448

449+
this.joinConfig = joinConfig;
450+
const userId = this.client.getUserId()!;
451+
const deviceId = this.client.getDeviceId()!;
452+
this.joining = !this.memberships.some((m) => isMyMembership(m, userId, deviceId));
453+
432454
// Join!
433455
this.membershipManager!.join(fociPreferred, fociActive, (e) => {
434456
this.logger.error("MembershipManager encountered an unrecoverable error: ", e);
@@ -458,11 +480,11 @@ export class MatrixRTCSession extends TypedEventEmitter<
458480

459481
this.logger.info(`Leaving call session in room ${this.roomSubset.roomId}`);
460482

483+
this.joining = false;
461484
this.encryptionManager!.leave();
462-
463485
const leavePromise = this.membershipManager!.leave(timeout);
464-
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
465486

487+
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
466488
return await leavePromise;
467489
}
468490

@@ -545,6 +567,22 @@ export class MatrixRTCSession extends TypedEventEmitter<
545567
}
546568
}
547569

570+
/**
571+
* Sends a notification corresponding to the configured notify type.
572+
*/
573+
private sendCallNotify(): void {
574+
if (this.joinConfig?.notifyType !== undefined) {
575+
this.client
576+
.sendEvent(this.roomSubset.roomId, EventType.CallNotify, {
577+
"application": "m.call",
578+
"m.mentions": { user_ids: [], room: true },
579+
"notify_type": this.joinConfig.notifyType,
580+
"call_id": this.callId!,
581+
})
582+
.catch((e) => this.logger.error("Failed to send call notification", e));
583+
}
584+
}
585+
548586
/**
549587
* Call this when the Matrix room members have changed.
550588
*/
@@ -585,6 +623,15 @@ export class MatrixRTCSession extends TypedEventEmitter<
585623
});
586624

587625
void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
626+
627+
const userId = this.client.getUserId()!;
628+
const deviceId = this.client.getDeviceId()!;
629+
if (this.joining && this.memberships.some((m) => isMyMembership(m, userId, deviceId))) {
630+
this.joining = false;
631+
// If we're the first member in the call, we're responsible for
632+
// sending the notification event
633+
if (oldMemberships.length === 0) this.sendCallNotify();
634+
}
588635
}
589636
// This also needs to be done if `changed` = false
590637
// A member might have updated their fingerprint (created_ts)

0 commit comments

Comments
 (0)