mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-05 04:06:44 +02:00
Room list: listen to call event to check number of participants (#32677)
* feat(call store): add new `CallEvent.Participants` event The room list needs to listen to `CallEvent.Participants` to be able to display the Call icon. This was working before https://github.com/element-hq/element-web/pull/32663 due to an excessive re-renders or relying on the notification events. * chore(room list): listen to `CallEvent.Participants` * test(room list): add test for new listener * test(call store): add tests for `CallEvent.Particpants` * Revert "feat(call store): add new `CallEvent.Participants` event" This reverts commit d2a7a009a4c55325404ad38f23fa662a8103cff4. * Revert "test(call store): add tests for `CallEvent.Particpants`" This reverts commit 4455182fb3aea54ea10cfabb8beb7946cfdf8a6c. * chore(room list): listen to `Call#CallEvent.Participants` insteaf of listening to `CallStore` * test(room list): update added test * fix(room list): clean properly listeners on previous call * test(room list): add missing test * fix(room list): don't use trackListeners to avoid leaking memory when listening to call event * fix(room list): listen to participant change when vm is created * test(room list): add test case when there is an existing call
This commit is contained in:
parent
738fa0f9bd
commit
15530ef075
@ -14,7 +14,7 @@ import {
|
||||
import { RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import type { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type { Room, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import type { RoomNotificationState } from "../../stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationStateEvents } from "../../stores/notifications/NotificationState";
|
||||
@ -36,6 +36,7 @@ import dispatcher from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { type Call, CallEvent } from "../../models/Call";
|
||||
|
||||
interface RoomItemProps {
|
||||
room: Room;
|
||||
@ -52,6 +53,10 @@ export class RoomListItemViewModel
|
||||
implements RoomListItemActions
|
||||
{
|
||||
private notifState: RoomNotificationState;
|
||||
/**
|
||||
* Track the current call for this room to manager listeners
|
||||
*/
|
||||
private currentCall: Call | null = null;
|
||||
|
||||
public constructor(props: RoomItemProps) {
|
||||
// Get notification state first so we can generate a complete initial snapshot
|
||||
@ -79,6 +84,8 @@ export class RoomListItemViewModel
|
||||
|
||||
// Subscribe to call state changes
|
||||
this.disposables.trackListener(CallStore.instance, CallStoreEvent.ConnectedCalls, this.onCallStateChanged);
|
||||
// If there is an active call for this room, listen to participant changes
|
||||
this.listenToCallParticipants();
|
||||
|
||||
// Subscribe to room-specific events
|
||||
this.disposables.trackListener(props.room, RoomEvent.Name, this.onRoomChanged);
|
||||
@ -88,6 +95,11 @@ export class RoomListItemViewModel
|
||||
void this.loadAndSetMessagePreview();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this.currentCall?.off(CallEvent.Participants, this.onCallParticipantsChanged);
|
||||
}
|
||||
|
||||
private onNotificationChanged = (): void => {
|
||||
this.updateItem();
|
||||
};
|
||||
@ -100,9 +112,38 @@ export class RoomListItemViewModel
|
||||
void this.loadAndSetMessagePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler for call participant changes. Only updates the item if the call moves between having participants and not having participants, to avoid unnecessary updates.
|
||||
* @param participants The current call participants
|
||||
*/
|
||||
private onCallParticipantsChanged = (participants: Map<RoomMember, Set<string>>): void => {
|
||||
const hasCall = Boolean(this.snapshot.current.notification.callType);
|
||||
// There is already an active call, we don't need to update the item
|
||||
if (hasCall && participants.size > 0) return;
|
||||
|
||||
this.updateItem();
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen to participant changes for the current call in this room (if any) to trigger updates when participants join/leave the call.
|
||||
*/
|
||||
private listenToCallParticipants(): void {
|
||||
const call = CallStore.instance.getCall(this.props.room.roomId);
|
||||
|
||||
// Remove listener from previous call (if any) and add to new call to track participant changes
|
||||
if (call !== this.currentCall) {
|
||||
this.currentCall?.off(CallEvent.Participants, this.onCallParticipantsChanged);
|
||||
call?.on(CallEvent.Participants, this.onCallParticipantsChanged);
|
||||
}
|
||||
this.currentCall = call;
|
||||
}
|
||||
|
||||
private onCallStateChanged = (): void => {
|
||||
// Only update if call state for this room actually changed
|
||||
const call = CallStore.instance.getCall(this.props.room.roomId);
|
||||
|
||||
this.listenToCallParticipants();
|
||||
|
||||
const currentCallType = this.snapshot.current.notification.callType;
|
||||
const newCallType =
|
||||
call && call.participants.size > 0 ? (call.callType === CallType.Voice ? "voice" : "video") : undefined;
|
||||
|
||||
@ -5,7 +5,14 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixClient, type MatrixEvent, Room, RoomEvent, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
Room,
|
||||
RoomEvent,
|
||||
PendingEventOrdering,
|
||||
type RoomMember,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import { createTestClient, flushPromises } from "../../test-utils";
|
||||
@ -270,6 +277,8 @@ describe("RoomListItemViewModel", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([[matrixClient.getUserId()!, {}]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
} as unknown as Call;
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||
@ -285,6 +294,8 @@ describe("RoomListItemViewModel", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Video,
|
||||
participants: new Map([[matrixClient.getUserId()!, {}]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
} as unknown as Call;
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||
@ -300,6 +311,8 @@ describe("RoomListItemViewModel", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map(),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
} as unknown as Call;
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||
@ -310,6 +323,120 @@ describe("RoomListItemViewModel", () => {
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should listen to call participant changes", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map(),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().notification.callType).toBeUndefined();
|
||||
|
||||
// Get the callback registered for call state changes
|
||||
const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls;
|
||||
const callStateCallback = mockCalls[mockCalls.length - 1][1];
|
||||
callStateCallback();
|
||||
|
||||
// Simulate participant joining
|
||||
mockCall.participants.set(matrixClient.getUserId()! as unknown as RoomMember, new Set());
|
||||
|
||||
// Get the callback registered for participant changes
|
||||
const participantsChangeCallback = mockCall.on.mock.calls[0][1];
|
||||
participantsChangeCallback();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
});
|
||||
|
||||
it("should not update the item when there is already an active call and participants join", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([[matrixClient.getUserId()! as unknown as RoomMember, new Set<string>()]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
// Trigger onCallStateChanged so the call is tracked and the participant listener is registered
|
||||
const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls;
|
||||
const callStateCallback = mockCalls[mockCalls.length - 1][1];
|
||||
callStateCallback();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
|
||||
// Record the snapshot version before the participant event fires
|
||||
const snapshotBefore = viewModel.getSnapshot();
|
||||
|
||||
// Simulate another participant joining while the call is already active
|
||||
mockCall.participants.set("@other:server" as unknown as RoomMember, new Set<string>());
|
||||
const participantsChangeCallback = mockCall.on.mock.calls[0][1];
|
||||
participantsChangeCallback(mockCall.participants);
|
||||
|
||||
// Snapshot should not have changed
|
||||
expect(viewModel.getSnapshot()).toBe(snapshotBefore);
|
||||
});
|
||||
|
||||
it("should react to participant changes when a call already exists at instantiation time", () => {
|
||||
const mockCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call);
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().notification.callType).toBeUndefined();
|
||||
|
||||
// Simulate participant joining
|
||||
mockCall.participants.set(matrixClient.getUserId()! as unknown as RoomMember, new Set());
|
||||
|
||||
// Get the callback registered for participant changes
|
||||
const participantsChangeCallback = mockCall.on.mock.calls[0][1];
|
||||
participantsChangeCallback();
|
||||
|
||||
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||
});
|
||||
|
||||
it("should unsubscribe from old call participants when the call changes", () => {
|
||||
const firstCall = {
|
||||
callType: CallType.Voice,
|
||||
participants: new Map([[matrixClient.getUserId()! as unknown as RoomMember, new Set<string>()]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
const secondCall = {
|
||||
callType: CallType.Video,
|
||||
participants: new Map([[matrixClient.getUserId()! as unknown as RoomMember, new Set<string>()]]),
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(firstCall as unknown as Call);
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
// Trigger onCallStateChanged to register the first call
|
||||
const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls;
|
||||
const callStateCallback = mockCalls[mockCalls.length - 1][1];
|
||||
callStateCallback();
|
||||
|
||||
const participantsCallback = firstCall.on.mock.calls[0][1];
|
||||
expect(firstCall.on).toHaveBeenCalledWith("participants", participantsCallback);
|
||||
|
||||
// Now switch to a different call
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(secondCall as unknown as Call);
|
||||
callStateCallback();
|
||||
|
||||
// The old call's listener must have been removed
|
||||
expect(firstCall.off).toHaveBeenCalledWith("participants", participantsCallback);
|
||||
// The new call must have a listener registered
|
||||
expect(secondCall.on).toHaveBeenCalledWith("participants", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room name updates", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user