You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Allow sending notification events when starting a call (#4826)
* Make it easier to mock call memberships for specific user IDs * Allow sending notification events when starting a call * rename notify -> notification * replace `joining` concept with `ownMembership` * introduce new `m.rtc.notification` event alongside `m.call.notify` * send new notification event alongside the deprecated one * Test for new notification event type * update relation string to match msc * review * fix doc errors * fix tests + format * remove anything decline related --------- Co-authored-by: Timo <toger5@hotmail.de>
This commit is contained in:
@@ -16,11 +16,10 @@ limitations under the License.
|
|||||||
|
|
||||||
import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src";
|
import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src";
|
||||||
import { KnownMembership } from "../../../src/@types/membership";
|
import { KnownMembership } from "../../../src/@types/membership";
|
||||||
import { type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
|
||||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||||
import { type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
import { type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||||
import { secureRandomString } from "../../../src/randomstring";
|
import { secureRandomString } from "../../../src/randomstring";
|
||||||
import { makeMockEvent, makeMockRoom, makeMockRoomState, membershipTemplate, makeKey } from "./mocks";
|
import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks";
|
||||||
import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts";
|
import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts";
|
||||||
|
|
||||||
const mockFocus = { type: "mock" };
|
const mockFocus = { type: "mock" };
|
||||||
@@ -48,7 +47,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
|
|
||||||
describe("roomSessionForRoom", () => {
|
describe("roomSessionForRoom", () => {
|
||||||
it("creates a room-scoped session from room state", () => {
|
it("creates a room-scoped session from room state", () => {
|
||||||
const mockRoom = makeMockRoom(membershipTemplate);
|
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
expect(sess?.memberships.length).toEqual(1);
|
expect(sess?.memberships.length).toEqual(1);
|
||||||
@@ -75,7 +74,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("ignores memberships events of members not in the room", () => {
|
it("ignores memberships events of members not in the room", () => {
|
||||||
const mockRoom = makeMockRoom(membershipTemplate);
|
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||||
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
expect(sess?.memberships.length).toEqual(0);
|
expect(sess?.memberships.length).toEqual(0);
|
||||||
@@ -270,9 +269,15 @@ describe("MatrixRTCSession", () => {
|
|||||||
describe("joining", () => {
|
describe("joining", () => {
|
||||||
let mockRoom: Room;
|
let mockRoom: Room;
|
||||||
let sendEventMock: jest.Mock;
|
let sendEventMock: jest.Mock;
|
||||||
|
let sendStateEventMock: jest.Mock;
|
||||||
|
|
||||||
|
let sentStateEvent: Promise<void>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sendEventMock = jest.fn();
|
sentStateEvent = new Promise((resolve) => {
|
||||||
|
sendStateEventMock = jest.fn(resolve);
|
||||||
|
});
|
||||||
|
sendEventMock = jest.fn().mockResolvedValue(undefined);
|
||||||
|
client.sendStateEvent = sendStateEventMock;
|
||||||
client.sendEvent = sendEventMock;
|
client.sendEvent = sendEventMock;
|
||||||
|
|
||||||
client._unstable_updateDelayedEvent = jest.fn();
|
client._unstable_updateDelayedEvent = jest.fn();
|
||||||
@@ -298,11 +303,69 @@ describe("MatrixRTCSession", () => {
|
|||||||
sess!.joinRoomSession([mockFocus], mockFocus);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
expect(sess!.isJoined()).toEqual(true);
|
expect(sess!.isJoined()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sends a notification when starting a call", async () => {
|
||||||
|
// Simulate a join, including the update to the room state
|
||||||
|
sess!.joinRoomSession([mockFocus], mockFocus, { notificationType: "ring" });
|
||||||
|
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
|
||||||
|
mockRoomState(mockRoom, [{ ...membershipTemplate, user_id: client.getUserId()! }]);
|
||||||
|
sess!.onRTCSessionMemberUpdate();
|
||||||
|
const ownMembershipId = sess?.memberships[0].eventId;
|
||||||
|
|
||||||
|
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.RTCNotification, {
|
||||||
|
"m.mentions": { user_ids: [], room: true },
|
||||||
|
"notification_type": "ring",
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: ownMembershipId,
|
||||||
|
rel_type: "org.matrix.msc4075.rtc.notification.parent",
|
||||||
|
},
|
||||||
|
"lifetime": 30000,
|
||||||
|
"sender_ts": expect.any(Number),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if deprecated notify event is also sent.
|
||||||
|
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": { user_ids: [], room: true },
|
||||||
|
"notify_type": "ring",
|
||||||
|
"call_id": "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't send a notification when joining an existing call", async () => {
|
||||||
|
// Add another member to the call so that it is considered an existing call
|
||||||
|
mockRoomState(mockRoom, [membershipTemplate]);
|
||||||
|
sess!.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
|
// Simulate a join, including the update to the room state
|
||||||
|
sess!.joinRoomSession([mockFocus], mockFocus, { notificationType: "ring" });
|
||||||
|
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
|
||||||
|
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
|
||||||
|
sess!.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
|
expect(client.sendEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't send a notification when someone else starts the call faster than us", async () => {
|
||||||
|
// Simulate a join, including the update to the room state
|
||||||
|
sess!.joinRoomSession([mockFocus], mockFocus, { notificationType: "ring" });
|
||||||
|
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
|
||||||
|
// But this time we want to simulate a race condition in which we receive a state event
|
||||||
|
// from someone else, starting the call before our own state event has been sent
|
||||||
|
mockRoomState(mockRoom, [membershipTemplate]);
|
||||||
|
sess!.onRTCSessionMemberUpdate();
|
||||||
|
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
|
||||||
|
sess!.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
|
// We assume that the responsibility to send a notification, if any, lies with the other
|
||||||
|
// participant that won the race
|
||||||
|
expect(client.sendEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("onMembershipsChanged", () => {
|
describe("onMembershipsChanged", () => {
|
||||||
it("does not emit if no membership changes", () => {
|
it("does not emit if no membership changes", () => {
|
||||||
const mockRoom = makeMockRoom(membershipTemplate);
|
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
const onMembershipsChanged = jest.fn();
|
const onMembershipsChanged = jest.fn();
|
||||||
@@ -313,13 +376,13 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("emits on membership changes", () => {
|
it("emits on membership changes", () => {
|
||||||
const mockRoom = makeMockRoom(membershipTemplate);
|
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
const onMembershipsChanged = jest.fn();
|
const onMembershipsChanged = jest.fn();
|
||||||
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
||||||
|
|
||||||
mockRoom.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState([], mockRoom.roomId));
|
mockRoomState(mockRoom, []);
|
||||||
sess.onRTCSessionMemberUpdate();
|
sess.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
expect(onMembershipsChanged).toHaveBeenCalled();
|
expect(onMembershipsChanged).toHaveBeenCalled();
|
||||||
@@ -503,18 +566,14 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
|
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
|
||||||
|
|
||||||
// member2 leaves triggering key rotation
|
// member2 leaves triggering key rotation
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
mockRoomState(mockRoom, [membershipTemplate]);
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
|
|
||||||
sess.onRTCSessionMemberUpdate();
|
sess.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
// member2 re-joins which should trigger an immediate re-send
|
// member2 re-joins which should trigger an immediate re-send
|
||||||
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
||||||
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
||||||
});
|
});
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
mockRoomState(mockRoom, [membershipTemplate, member2]);
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
|
|
||||||
sess.onRTCSessionMemberUpdate();
|
sess.onRTCSessionMemberUpdate();
|
||||||
// but, that immediate resend is throttled so we need to wait a bit
|
// but, that immediate resend is throttled so we need to wait a bit
|
||||||
jest.advanceTimersByTime(1000);
|
jest.advanceTimersByTime(1000);
|
||||||
@@ -565,9 +624,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
device_id: "BBBBBBB",
|
device_id: "BBBBBBB",
|
||||||
});
|
});
|
||||||
|
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
mockRoomState(mockRoom, [membershipTemplate, member2]);
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
|
|
||||||
sess.onRTCSessionMemberUpdate();
|
sess.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
await keysSentPromise2;
|
await keysSentPromise2;
|
||||||
@@ -592,9 +649,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mockRoom = makeMockRoom([member1, member2]);
|
const mockRoom = makeMockRoom([member1, member2]);
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
mockRoomState(mockRoom, [member1, member2]);
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
|
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
@@ -641,10 +696,6 @@ describe("MatrixRTCSession", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockRoom = makeMockRoom([member1, member2]);
|
const mockRoom = makeMockRoom([member1, member2]);
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
|
|
||||||
|
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
|
|
||||||
@@ -674,6 +725,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
|
|
||||||
// update created_ts
|
// update created_ts
|
||||||
member2.created_ts = 5000;
|
member2.created_ts = 5000;
|
||||||
|
mockRoomState(mockRoom, [member1, member2]);
|
||||||
|
|
||||||
const keysSentPromise2 = new Promise((resolve) => {
|
const keysSentPromise2 = new Promise((resolve) => {
|
||||||
sendEventMock.mockImplementation(resolve);
|
sendEventMock.mockImplementation(resolve);
|
||||||
@@ -737,9 +789,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
||||||
});
|
});
|
||||||
|
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
mockRoomState(mockRoom, [membershipTemplate]);
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
|
|
||||||
sess.onRTCSessionMemberUpdate();
|
sess.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
jest.advanceTimersByTime(KEY_DELAY);
|
jest.advanceTimersByTime(KEY_DELAY);
|
||||||
@@ -784,7 +834,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
it("wraps key index around to 0 when it reaches the maximum", async () => {
|
it("wraps key index around to 0 when it reaches the maximum", async () => {
|
||||||
// this should give us keys with index [0...255, 0, 1]
|
// this should give us keys with index [0...255, 0, 1]
|
||||||
const membersToTest = 258;
|
const membersToTest = 258;
|
||||||
const members: SessionMembershipData[] = [];
|
const members: MembershipData[] = [];
|
||||||
for (let i = 0; i < membersToTest; i++) {
|
for (let i = 0; i < membersToTest; i++) {
|
||||||
members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` }));
|
members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` }));
|
||||||
}
|
}
|
||||||
@@ -804,11 +854,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
} else {
|
} else {
|
||||||
// otherwise update the state reducing the membership each time in order to trigger key rotation
|
// otherwise update the state reducing the membership each time in order to trigger key rotation
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
mockRoomState(mockRoom, members.slice(0, membersToTest - i));
|
||||||
.fn()
|
|
||||||
.mockReturnValue(
|
|
||||||
makeMockRoomState(members.slice(0, membersToTest - i), mockRoom.roomId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sess!.onRTCSessionMemberUpdate();
|
sess!.onRTCSessionMemberUpdate();
|
||||||
@@ -849,9 +895,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
device_id: "BBBBBBB",
|
device_id: "BBBBBBB",
|
||||||
});
|
});
|
||||||
|
|
||||||
mockRoom.getLiveTimeline().getState = jest
|
mockRoomState(mockRoom, [membershipTemplate, member2]);
|
||||||
.fn()
|
|
||||||
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
|
|
||||||
sess.onRTCSessionMemberUpdate();
|
sess.onRTCSessionMemberUpdate();
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
|
@@ -14,12 +14,10 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Mock } from "jest-mock";
|
|
||||||
|
|
||||||
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
|
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
|
||||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||||
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
||||||
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
|
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
|
||||||
|
|
||||||
describe("MatrixRTCSessionManager", () => {
|
describe("MatrixRTCSessionManager", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
@@ -52,19 +50,16 @@ describe("MatrixRTCSessionManager", () => {
|
|||||||
it("Fires event when session ends", () => {
|
it("Fires event when session ends", () => {
|
||||||
const onEnded = jest.fn();
|
const onEnded = jest.fn();
|
||||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||||
const room1 = makeMockRoom(membershipTemplate);
|
const room1 = makeMockRoom([membershipTemplate]);
|
||||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||||
|
|
||||||
client.emit(ClientEvent.Room, room1);
|
client.emit(ClientEvent.Room, room1);
|
||||||
|
|
||||||
(room1.getLiveTimeline as Mock).mockReturnValue({
|
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
||||||
getState: jest.fn().mockReturnValue(makeMockRoomState([{}], room1.roomId)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||||
const membEvent = roomState.getStateEvents("")[0];
|
const membEvent = roomState.getStateEvents("")[0];
|
||||||
|
|
||||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||||
|
|
||||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||||
|
@@ -81,7 +81,7 @@ describe("MembershipManager", () => {
|
|||||||
// Default to fake timers.
|
// Default to fake timers.
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
client = makeMockClient("@alice:example.org", "AAAAAAA");
|
client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||||
room = makeMockRoom(membershipTemplate);
|
room = makeMockRoom([membershipTemplate]);
|
||||||
// Provide a default mock that is like the default "non error" server behaviour.
|
// Provide a default mock that is like the default "non error" server behaviour.
|
||||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||||
@@ -436,11 +436,11 @@ describe("MembershipManager", () => {
|
|||||||
type: "livekit",
|
type: "livekit",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
device_id: client.getDeviceId(),
|
user_id: client.getUserId()!,
|
||||||
|
device_id: client.getDeviceId()!,
|
||||||
created_ts: 1000,
|
created_ts: 1000,
|
||||||
},
|
},
|
||||||
room.roomId,
|
room.roomId,
|
||||||
client.getUserId()!,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(manager.getActiveFocus()).toStrictEqual(focus);
|
expect(manager.getActiveFocus()).toStrictEqual(focus);
|
||||||
@@ -482,7 +482,10 @@ describe("MembershipManager", () => {
|
|||||||
|
|
||||||
await manager.onRTCSessionMemberUpdate([
|
await manager.onRTCSessionMemberUpdate([
|
||||||
mockCallMembership(membershipTemplate, room.roomId),
|
mockCallMembership(membershipTemplate, room.roomId),
|
||||||
mockCallMembership(myMembership as SessionMembershipData, room.roomId, client.getUserId() ?? undefined),
|
mockCallMembership(
|
||||||
|
{ ...(myMembership as SessionMembershipData), user_id: client.getUserId()! },
|
||||||
|
room.roomId,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await jest.advanceTimersByTimeAsync(1);
|
||||||
@@ -797,7 +800,7 @@ describe("MembershipManager", () => {
|
|||||||
|
|
||||||
it("Should prefix log with MembershipManager used", () => {
|
it("Should prefix log with MembershipManager used", () => {
|
||||||
const client = makeMockClient("@alice:example.org", "AAAAAAA");
|
const client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||||
const room = makeMockRoom(membershipTemplate);
|
const room = makeMockRoom([membershipTemplate]);
|
||||||
|
|
||||||
const membershipManager = new MembershipManager(undefined, room, client, () => undefined, logger);
|
const membershipManager = new MembershipManager(undefined, room, client, () => undefined, logger);
|
||||||
|
|
||||||
|
@@ -664,7 +664,7 @@ describe("RTCEncryptionManager", () => {
|
|||||||
|
|
||||||
await jest.runOnlyPendingTimersAsync();
|
await jest.runOnlyPendingTimersAsync();
|
||||||
|
|
||||||
// The key should have beed re-distributed to the room transport
|
// The key should have been re-distributed to the room transport
|
||||||
expect(mockRoomTransport.sendKey).toHaveBeenCalled();
|
expect(mockRoomTransport.sendKey).toHaveBeenCalled();
|
||||||
expect(mockToDeviceTransport.sendKey).toHaveBeenCalledWith(
|
expect(mockToDeviceTransport.sendKey).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
@@ -677,9 +677,8 @@ describe("RTCEncryptionManager", () => {
|
|||||||
|
|
||||||
function aCallMembership(userId: string, deviceId: string, ts: number = 1000): CallMembership {
|
function aCallMembership(userId: string, deviceId: string, ts: number = 1000): CallMembership {
|
||||||
return mockCallMembership(
|
return mockCallMembership(
|
||||||
Object.assign({}, membershipTemplate, { device_id: deviceId, created_ts: ts }),
|
{ ...membershipTemplate, user_id: userId, device_id: deviceId, created_ts: ts },
|
||||||
"!room:id",
|
"!room:id",
|
||||||
userId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -20,11 +20,12 @@ import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent }
|
|||||||
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||||
import { secureRandomString } from "../../../src/randomstring";
|
import { secureRandomString } from "../../../src/randomstring";
|
||||||
|
|
||||||
type MembershipData = SessionMembershipData[] | SessionMembershipData | {};
|
export type MembershipData = (SessionMembershipData | {}) & { user_id: string };
|
||||||
|
|
||||||
export const membershipTemplate: SessionMembershipData = {
|
export const membershipTemplate: SessionMembershipData & { user_id: string } = {
|
||||||
application: "m.call",
|
application: "m.call",
|
||||||
call_id: "",
|
call_id: "",
|
||||||
|
user_id: "@mock:user.example",
|
||||||
device_id: "AAAAAAA",
|
device_id: "AAAAAAA",
|
||||||
scope: "m.room",
|
scope: "m.room",
|
||||||
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
||||||
@@ -68,7 +69,7 @@ export function makeMockClient(userId: string, deviceId: string): MockClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function makeMockRoom(
|
export function makeMockRoom(
|
||||||
membershipData: MembershipData,
|
membershipData: MembershipData[],
|
||||||
): Room & { emitTimelineEvent: (event: MatrixEvent) => void } {
|
): Room & { emitTimelineEvent: (event: MatrixEvent) => void } {
|
||||||
const roomId = secureRandomString(8);
|
const roomId = secureRandomString(8);
|
||||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
||||||
@@ -87,10 +88,8 @@ export function makeMockRoom(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
|
function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
|
||||||
const events = Array.isArray(membershipData)
|
const events = membershipData.map((m) => mockRTCEvent(m, roomId));
|
||||||
? membershipData.map((m) => mockRTCEvent(m, roomId))
|
|
||||||
: [mockRTCEvent(membershipData, roomId)];
|
|
||||||
const keysAndEvents = events.map((e) => {
|
const keysAndEvents = events.map((e) => {
|
||||||
const data = e.getContent() as SessionMembershipData;
|
const data = e.getContent() as SessionMembershipData;
|
||||||
return [`_${e.sender?.userId}_${data.device_id}`];
|
return [`_${e.sender?.userId}_${data.device_id}`];
|
||||||
@@ -120,6 +119,10 @@ export function makeMockRoomState(membershipData: MembershipData, roomId: string
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mockRoomState(room: Room, membershipData: MembershipData[]): void {
|
||||||
|
room.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId));
|
||||||
|
}
|
||||||
|
|
||||||
export function makeMockEvent(
|
export function makeMockEvent(
|
||||||
type: string,
|
type: string,
|
||||||
sender: string,
|
sender: string,
|
||||||
@@ -138,13 +141,12 @@ export function makeMockEvent(
|
|||||||
} as unknown as MatrixEvent;
|
} as unknown as MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockRTCEvent(membershipData: MembershipData, roomId: string, customSender?: string): MatrixEvent {
|
export function mockRTCEvent({ user_id: sender, ...membershipData }: MembershipData, roomId: string): MatrixEvent {
|
||||||
const sender = customSender ?? "@mock:user.example";
|
|
||||||
return makeMockEvent(EventType.GroupCallMemberPrefix, sender, roomId, membershipData);
|
return makeMockEvent(EventType.GroupCallMemberPrefix, sender, roomId, membershipData);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockCallMembership(membershipData: MembershipData, roomId: string, sender?: string): CallMembership {
|
export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership {
|
||||||
return new CallMembership(mockRTCEvent(membershipData, roomId, sender), membershipData);
|
return new CallMembership(mockRTCEvent(membershipData, roomId), membershipData);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeKey(id: number, key: string): { key: string; index: number } {
|
export function makeKey(id: number, key: string): { key: string; index: number } {
|
||||||
|
@@ -51,7 +51,11 @@ import {
|
|||||||
type SDPStreamMetadata,
|
type SDPStreamMetadata,
|
||||||
type SDPStreamMetadataKey,
|
type SDPStreamMetadataKey,
|
||||||
} from "../webrtc/callEventTypes.ts";
|
} from "../webrtc/callEventTypes.ts";
|
||||||
import { type EncryptionKeysEventContent, type ICallNotifyContent } from "../matrixrtc/types.ts";
|
import {
|
||||||
|
type IRTCNotificationContent,
|
||||||
|
type EncryptionKeysEventContent,
|
||||||
|
type ICallNotifyContent,
|
||||||
|
} from "../matrixrtc/types.ts";
|
||||||
import { type M_POLL_END, type M_POLL_START, type PollEndEventContent, type PollStartEventContent } from "./polls.ts";
|
import { type M_POLL_END, type M_POLL_START, type PollEndEventContent, type PollStartEventContent } from "./polls.ts";
|
||||||
import { type SessionMembershipData } from "../matrixrtc/CallMembership.ts";
|
import { type SessionMembershipData } from "../matrixrtc/CallMembership.ts";
|
||||||
import { type LocalNotificationSettings } from "./local_notifications.ts";
|
import { type LocalNotificationSettings } from "./local_notifications.ts";
|
||||||
@@ -147,6 +151,7 @@ export enum EventType {
|
|||||||
|
|
||||||
// MatrixRTC events
|
// MatrixRTC events
|
||||||
CallNotify = "org.matrix.msc4075.call.notify",
|
CallNotify = "org.matrix.msc4075.call.notify",
|
||||||
|
RTCNotification = "org.matrix.msc4075.rtc.notification",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RelationType {
|
export enum RelationType {
|
||||||
@@ -158,6 +163,7 @@ export enum RelationType {
|
|||||||
// moreover, our tests currently use the unstable prefix. Use THREAD_RELATION_TYPE.name.
|
// moreover, our tests currently use the unstable prefix. Use THREAD_RELATION_TYPE.name.
|
||||||
// Once we support *only* the stable prefix, THREAD_RELATION_TYPE can die and we can switch to this.
|
// Once we support *only* the stable prefix, THREAD_RELATION_TYPE can die and we can switch to this.
|
||||||
Thread = "m.thread",
|
Thread = "m.thread",
|
||||||
|
unstable_RTCNotificationParent = "org.matrix.msc4075.rtc.notification.parent",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MsgType {
|
export enum MsgType {
|
||||||
@@ -325,6 +331,7 @@ export interface TimelineEvents {
|
|||||||
[EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase & { [SDPStreamMetadataKey]: SDPStreamMetadata };
|
[EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase & { [SDPStreamMetadataKey]: SDPStreamMetadata };
|
||||||
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
|
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
|
||||||
[EventType.CallNotify]: ICallNotifyContent;
|
[EventType.CallNotify]: ICallNotifyContent;
|
||||||
|
[EventType.RTCNotification]: IRTCNotificationContent;
|
||||||
[M_BEACON.name]: MBeaconEventContent;
|
[M_BEACON.name]: MBeaconEventContent;
|
||||||
[M_POLL_START.name]: PollStartEventContent;
|
[M_POLL_START.name]: PollStartEventContent;
|
||||||
[M_POLL_END.name]: PollEndEventContent;
|
[M_POLL_END.name]: PollEndEventContent;
|
||||||
|
@@ -55,9 +55,13 @@ export interface IMembershipManager {
|
|||||||
* Get the actual connection status of the manager.
|
* Get the actual connection status of the manager.
|
||||||
*/
|
*/
|
||||||
get status(): Status;
|
get status(): Status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current status while the manager is activated
|
* The Current own state event if the manger is connected.
|
||||||
|
* `undefined` if not connected.
|
||||||
*/
|
*/
|
||||||
|
get ownMembership(): CallMembership | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start sending all necessary events to make this user participate in the RTC session.
|
* Start sending all necessary events to make this user participate in the RTC session.
|
||||||
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
|
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
|
||||||
|
@@ -19,7 +19,7 @@ import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
|||||||
import { EventTimeline } from "../models/event-timeline.ts";
|
import { EventTimeline } from "../models/event-timeline.ts";
|
||||||
import { type Room } from "../models/room.ts";
|
import { type Room } from "../models/room.ts";
|
||||||
import { type MatrixClient } from "../client.ts";
|
import { type MatrixClient } from "../client.ts";
|
||||||
import { EventType } from "../@types/event.ts";
|
import { EventType, RelationType } from "../@types/event.ts";
|
||||||
import { CallMembership } from "./CallMembership.ts";
|
import { CallMembership } from "./CallMembership.ts";
|
||||||
import { RoomStateEvent } from "../models/room-state.ts";
|
import { RoomStateEvent } from "../models/room-state.ts";
|
||||||
import { type Focus } from "./focus.ts";
|
import { type Focus } from "./focus.ts";
|
||||||
@@ -27,7 +27,7 @@ import { KnownMembership } from "../@types/membership.ts";
|
|||||||
import { MembershipManager } from "./MembershipManager.ts";
|
import { MembershipManager } from "./MembershipManager.ts";
|
||||||
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
|
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
|
||||||
import { logDurationSync } from "../utils.ts";
|
import { logDurationSync } from "../utils.ts";
|
||||||
import { type Statistics } from "./types.ts";
|
import { type Statistics, type RTCNotificationType } from "./types.ts";
|
||||||
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
|
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
|
||||||
import type { IMembershipManager } from "./IMembershipManager.ts";
|
import type { IMembershipManager } from "./IMembershipManager.ts";
|
||||||
import { RTCEncryptionManager } from "./RTCEncryptionManager.ts";
|
import { RTCEncryptionManager } from "./RTCEncryptionManager.ts";
|
||||||
@@ -65,6 +65,15 @@ export type MatrixRTCSessionEventHandlerMap = {
|
|||||||
) => void;
|
) => void;
|
||||||
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
|
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface SessionConfig {
|
||||||
|
/**
|
||||||
|
* What kind of notification to send when starting the session.
|
||||||
|
* @default `undefined` (no notification)
|
||||||
|
*/
|
||||||
|
notificationType?: RTCNotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
// The names follow these principles:
|
// The names follow these principles:
|
||||||
// - we use the technical term delay if the option is related to delayed events.
|
// - we use the technical term delay if the option is related to delayed events.
|
||||||
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
|
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
|
||||||
@@ -168,7 +177,7 @@ export interface EncryptionConfig {
|
|||||||
*/
|
*/
|
||||||
useKeyDelay?: number;
|
useKeyDelay?: number;
|
||||||
}
|
}
|
||||||
export type JoinSessionConfig = MembershipConfig & EncryptionConfig;
|
export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
|
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
|
||||||
@@ -182,7 +191,10 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
private encryptionManager?: IEncryptionManager;
|
private encryptionManager?: IEncryptionManager;
|
||||||
// The session Id of the call, this is the call_id of the call Member event.
|
// The session Id of the call, this is the call_id of the call Member event.
|
||||||
private _callId: string | undefined;
|
private _callId: string | undefined;
|
||||||
|
private joinConfig?: SessionConfig;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
|
private pendingNotificationToSend: undefined | RTCNotificationType;
|
||||||
/**
|
/**
|
||||||
* This timeout is responsible to track any expiration. We need to know when we have to start
|
* This timeout is responsible to track any expiration. We need to know when we have to start
|
||||||
* to ignore other call members. There is no callback for this. This timeout will always be configured to
|
* to ignore other call members. There is no callback for this. This timeout will always be configured to
|
||||||
@@ -447,6 +459,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.joinConfig = joinConfig;
|
||||||
|
this.pendingNotificationToSend = this.joinConfig?.notificationType;
|
||||||
|
|
||||||
// Join!
|
// Join!
|
||||||
this.membershipManager!.join(fociPreferred, fociActive, (e) => {
|
this.membershipManager!.join(fociPreferred, fociActive, (e) => {
|
||||||
this.logger.error("MembershipManager encountered an unrecoverable error: ", e);
|
this.logger.error("MembershipManager encountered an unrecoverable error: ", e);
|
||||||
@@ -547,6 +562,35 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a notification corresponding to the configured notify type.
|
||||||
|
*/
|
||||||
|
private sendCallNotify(parentEventId: string, notificationType: RTCNotificationType): void {
|
||||||
|
// Send legacy event:
|
||||||
|
this.client
|
||||||
|
.sendEvent(this.roomSubset.roomId, EventType.CallNotify, {
|
||||||
|
"application": "m.call",
|
||||||
|
"m.mentions": { user_ids: [], room: true },
|
||||||
|
"notify_type": notificationType === "notification" ? "notify" : notificationType,
|
||||||
|
"call_id": this.callId!,
|
||||||
|
})
|
||||||
|
.catch((e) => this.logger.error("Failed to send call notification", e));
|
||||||
|
|
||||||
|
// Send new event:
|
||||||
|
this.client
|
||||||
|
.sendEvent(this.roomSubset.roomId, EventType.RTCNotification, {
|
||||||
|
"m.mentions": { user_ids: [], room: true },
|
||||||
|
"notification_type": notificationType,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: parentEventId,
|
||||||
|
rel_type: RelationType.unstable_RTCNotificationParent,
|
||||||
|
},
|
||||||
|
"sender_ts": Date.now(),
|
||||||
|
"lifetime": 30_000, // 30 seconds
|
||||||
|
})
|
||||||
|
.catch((e) => this.logger.error("Failed to send call notification", e));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call this when the Matrix room members have changed.
|
* Call this when the Matrix room members have changed.
|
||||||
*/
|
*/
|
||||||
@@ -587,6 +631,20 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
});
|
});
|
||||||
|
|
||||||
void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
|
void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
|
||||||
|
// The `ownMembership` will be set when calling `onRTCSessionMemberUpdate`.
|
||||||
|
const ownMembership = this.membershipManager?.ownMembership;
|
||||||
|
if (this.pendingNotificationToSend && ownMembership && oldMemberships.length === 0) {
|
||||||
|
// If we're the first member in the call, we're responsible for
|
||||||
|
// sending the notification event
|
||||||
|
if (ownMembership.eventId && this.joinConfig?.notificationType) {
|
||||||
|
this.sendCallNotify(ownMembership.eventId, this.joinConfig.notificationType);
|
||||||
|
} else {
|
||||||
|
this.logger.warn("Own membership eventId is undefined, cannot send call notification");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If anyone else joins the session it is no longer our responsibility to send the notification.
|
||||||
|
// (If we were the joiner we already did sent the notification in the block above.)
|
||||||
|
if (this.memberships.length > 0) this.pendingNotificationToSend = undefined;
|
||||||
}
|
}
|
||||||
// This also needs to be done if `changed` = false
|
// This also needs to be done if `changed` = false
|
||||||
// A member might have updated their fingerprint (created_ts)
|
// A member might have updated their fingerprint (created_ts)
|
||||||
|
@@ -221,7 +221,13 @@ export class MembershipManager
|
|||||||
public async onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise<void> {
|
public async onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise<void> {
|
||||||
const userId = this.client.getUserId();
|
const userId = this.client.getUserId();
|
||||||
const deviceId = this.client.getDeviceId();
|
const deviceId = this.client.getDeviceId();
|
||||||
if (userId && deviceId && this.isJoined() && !memberships.some((m) => isMyMembership(m, userId, deviceId))) {
|
if (!userId || !deviceId) {
|
||||||
|
this.logger.error("MembershipManager.onRTCSessionMemberUpdate called without user or device id");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
this._ownMembership = memberships.find((m) => isMyMembership(m, userId, deviceId));
|
||||||
|
|
||||||
|
if (this.isActivated() && !this._ownMembership) {
|
||||||
// If one of these actions are scheduled or are getting inserted in the next iteration, we should already
|
// If one of these actions are scheduled or are getting inserted in the next iteration, we should already
|
||||||
// take care of our missing membership.
|
// take care of our missing membership.
|
||||||
const sendingMembershipActions = [
|
const sendingMembershipActions = [
|
||||||
@@ -310,6 +316,11 @@ export class MembershipManager
|
|||||||
}, this.logger);
|
}, this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _ownMembership?: CallMembership;
|
||||||
|
public get ownMembership(): CallMembership | undefined {
|
||||||
|
return this._ownMembership;
|
||||||
|
}
|
||||||
|
|
||||||
// scheduler
|
// scheduler
|
||||||
private oldStatus?: Status;
|
private oldStatus?: Status;
|
||||||
private scheduler: ActionScheduler;
|
private scheduler: ActionScheduler;
|
||||||
|
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import type { IMentions } from "../matrix.ts";
|
import type { IMentions } from "../matrix.ts";
|
||||||
|
import type { RelationEvent } from "../types.ts";
|
||||||
import type { CallMembership } from "./CallMembership.ts";
|
import type { CallMembership } from "./CallMembership.ts";
|
||||||
|
|
||||||
export type ParticipantId = string;
|
export type ParticipantId = string;
|
||||||
@@ -80,9 +81,13 @@ export interface EncryptionKeysToDeviceEventContent {
|
|||||||
// Why is this needed?
|
// Why is this needed?
|
||||||
sent_ts?: number;
|
sent_ts?: number;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @deprecated Use `RTCNotificationType` instead.
|
||||||
|
*/
|
||||||
export type CallNotifyType = "ring" | "notify";
|
export type CallNotifyType = "ring" | "notify";
|
||||||
|
/**
|
||||||
|
* @deprecated Use `IRTCNotificationContent` instead.
|
||||||
|
*/
|
||||||
export interface ICallNotifyContent {
|
export interface ICallNotifyContent {
|
||||||
"application": string;
|
"application": string;
|
||||||
"m.mentions": IMentions;
|
"m.mentions": IMentions;
|
||||||
@@ -90,6 +95,15 @@ export interface ICallNotifyContent {
|
|||||||
"call_id": string;
|
"call_id": string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RTCNotificationType = "ring" | "notification";
|
||||||
|
export interface IRTCNotificationContent extends RelationEvent {
|
||||||
|
"m.mentions": IMentions;
|
||||||
|
"decline_reason"?: string;
|
||||||
|
"notification_type": RTCNotificationType;
|
||||||
|
"sender_ts": number;
|
||||||
|
"lifetime": number;
|
||||||
|
}
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
Disconnected = "Disconnected",
|
Disconnected = "Disconnected",
|
||||||
Connecting = "Connecting",
|
Connecting = "Connecting",
|
||||||
|
Reference in New Issue
Block a user