Skip to content

Commit b3b45cd

Browse files
robintowntoger5
authored andcommitted
Allow sending notification events when starting a call
1 parent ebb973b commit b3b45cd

File tree

2 files changed

+103
-5
lines changed

2 files changed

+103
-5
lines changed

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,15 @@ describe("MatrixRTCSession", () => {
322322
describe("joining", () => {
323323
let mockRoom: Room;
324324
let sendEventMock: jest.Mock;
325+
let sendStateEventMock: jest.Mock;
325326

327+
let sentStateEvent: Promise<void>;
326328
beforeEach(() => {
327-
sendEventMock = jest.fn();
329+
sentStateEvent = new Promise((resolve) => {
330+
sendStateEventMock = jest.fn(resolve);
331+
});
332+
sendEventMock = jest.fn().mockResolvedValue(undefined);
333+
client.sendStateEvent = sendStateEventMock;
328334
client.sendEvent = sendEventMock;
329335

330336
client._unstable_updateDelayedEvent = jest.fn();
@@ -350,6 +356,51 @@ describe("MatrixRTCSession", () => {
350356
sess!.joinRoomSession([mockFocus], mockFocus);
351357
expect(sess!.isJoined()).toEqual(true);
352358
});
359+
360+
it("sends a notification when starting a call", async () => {
361+
// Simulate a join, including the update to the room state
362+
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
363+
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
364+
mockRoomState(mockRoom, [{ ...membershipTemplate, user_id: client.getUserId()! }]);
365+
sess!.onRTCSessionMemberUpdate();
366+
367+
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
368+
"application": "m.call",
369+
"m.mentions": { user_ids: [], room: true },
370+
"notify_type": "ring",
371+
"call_id": "",
372+
});
373+
});
374+
375+
it("doesn't send a notification when joining an existing call", async () => {
376+
// Add another member to the call so that it is considered an existing call
377+
mockRoomState(mockRoom, [membershipTemplate]);
378+
sess!.onRTCSessionMemberUpdate();
379+
380+
// Simulate a join, including the update to the room state
381+
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
382+
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
383+
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
384+
sess!.onRTCSessionMemberUpdate();
385+
386+
expect(client.sendEvent).not.toHaveBeenCalled();
387+
});
388+
389+
it("doesn't send a notification when someone else starts the call faster than us", async () => {
390+
// Simulate a join, including the update to the room state
391+
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
392+
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
393+
// But this time we want to simulate a race condition in which we receive a state event
394+
// from someone else, starting the call before our own state event has been sent
395+
mockRoomState(mockRoom, [membershipTemplate]);
396+
sess!.onRTCSessionMemberUpdate();
397+
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
398+
sess!.onRTCSessionMemberUpdate();
399+
400+
// We assume that the responsibility to send a notification, if any, lies with the other
401+
// participant that won the race
402+
expect(client.sendEvent).not.toHaveBeenCalled();
403+
});
353404
});
354405

355406
describe("onMembershipsChanged", () => {

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { KnownMembership } from "../@types/membership.ts";
2727
import { MembershipManager } from "./MembershipManager.ts";
2828
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
2929
import { logDurationSync } from "../utils.ts";
30-
import { type Statistics } from "./types.ts";
30+
import { type Statistics, type CallNotifyType, isMyMembership } from "./types.ts";
3131
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
3232
import type { IMembershipManager } from "./IMembershipManager.ts";
3333
import { RTCEncryptionManager } from "./RTCEncryptionManager.ts";
@@ -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.
@@ -168,7 +177,7 @@ export interface EncryptionConfig {
168177
*/
169178
useKeyDelay?: number;
170179
}
171-
export type JoinSessionConfig = MembershipConfig & EncryptionConfig;
180+
export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionConfig;
172181

173182
/**
174183
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
@@ -182,7 +191,15 @@ export class MatrixRTCSession extends TypedEventEmitter<
182191
private encryptionManager?: IEncryptionManager;
183192
// The session Id of the call, this is the call_id of the call Member event.
184193
private _callId: string | undefined;
194+
private joinConfig?: SessionConfig;
185195
private logger: Logger;
196+
197+
/**
198+
* Whether we're trying to join the session but still waiting for room state
199+
* to reflect our own membership.
200+
*/
201+
private joining = false;
202+
186203
/**
187204
* This timeout is responsible to track any expiration. We need to know when we have to start
188205
* to ignore other call members. There is no callback for this. This timeout will always be configured to
@@ -447,6 +464,11 @@ export class MatrixRTCSession extends TypedEventEmitter<
447464
}
448465
}
449466

467+
this.joinConfig = joinConfig;
468+
const userId = this.client.getUserId()!;
469+
const deviceId = this.client.getDeviceId()!;
470+
this.joining = !this.memberships.some((m) => isMyMembership(m, userId, deviceId));
471+
450472
// Join!
451473
this.membershipManager!.join(fociPreferred, fociActive, (e) => {
452474
this.logger.error("MembershipManager encountered an unrecoverable error: ", e);
@@ -476,11 +498,11 @@ export class MatrixRTCSession extends TypedEventEmitter<
476498

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

501+
this.joining = false;
479502
this.encryptionManager!.leave();
480-
481503
const leavePromise = this.membershipManager!.leave(timeout);
482-
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
483504

505+
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
484506
return await leavePromise;
485507
}
486508

@@ -547,6 +569,22 @@ export class MatrixRTCSession extends TypedEventEmitter<
547569
}
548570
}
549571

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

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

0 commit comments

Comments
 (0)