mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
I noticed that calls to createDisplayNameBehavior$ and createAvatarUrlBehavior$ were technically leaking resources since they reused the ObservableScope from their outer scope, which in practice lasts for the entire lifetime of the CallViewModel. This would not have had any noticeable effect unless you had other participants leave and rejoin the same call many thousands of times.
162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
/*
|
|
Copyright 2025 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { type RoomMember, RoomStateEvent } from "matrix-js-sdk";
|
|
import { combineLatest, fromEvent, map } from "rxjs";
|
|
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
|
import {
|
|
KnownMembership,
|
|
type Room as MatrixRoom,
|
|
} from "matrix-js-sdk/lib/matrix";
|
|
// eslint-disable-next-line rxjs/no-internal
|
|
|
|
import { type ObservableScope } from "../../ObservableScope";
|
|
import {
|
|
calculateDisplayName,
|
|
shouldDisambiguate,
|
|
} from "../../../utils/displayname";
|
|
import { type Behavior } from "../../Behavior";
|
|
|
|
const logger = rootLogger.getChild("[MatrixMemberMetadata]");
|
|
|
|
export type RoomMemberMap = Map<
|
|
string,
|
|
Pick<RoomMember, "userId" | "getMxcAvatarUrl" | "rawDisplayName">
|
|
>;
|
|
export function roomToMembersMap(matrixRoom: MatrixRoom): RoomMemberMap {
|
|
const members = matrixRoom
|
|
.getMembersWithMembership(KnownMembership.Join)
|
|
.concat(matrixRoom.getMembersWithMembership(KnownMembership.Invite));
|
|
return members.reduce((acc, member) => {
|
|
acc.set(member.userId, {
|
|
userId: member.userId,
|
|
getMxcAvatarUrl: member.getMxcAvatarUrl.bind(member),
|
|
rawDisplayName: member.rawDisplayName,
|
|
});
|
|
return acc;
|
|
}, new Map());
|
|
}
|
|
|
|
export function createRoomMembers$(
|
|
scope: ObservableScope,
|
|
matrixRoom: MatrixRoom,
|
|
): Behavior<RoomMemberMap> {
|
|
return scope.behavior(
|
|
fromEvent(matrixRoom, RoomStateEvent.Members).pipe(
|
|
map(() => roomToMembersMap(matrixRoom)),
|
|
),
|
|
roomToMembersMap(matrixRoom),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Displayname for each member of the call. This will disambiguate
|
|
* any displayname that clashes with another member. Only members
|
|
* joined to the call are considered here.
|
|
*
|
|
* @returns Map<userId, displayname> uses the Matrix user ID as the key.
|
|
*/
|
|
// don't do this work more times than we need to. This is achieved by converting to a behavior:
|
|
export const memberDisplaynames$ = (
|
|
scope: ObservableScope,
|
|
memberships$: Behavior<Pick<CallMembership, "userId">[]>,
|
|
roomMembers$: Behavior<RoomMemberMap>,
|
|
): Behavior<Map<string, string>> => {
|
|
// This map tracks userIds that at some point needed disambiguation.
|
|
// This is a memory leak bound to the number of participants.
|
|
// A call application will always increase the memory if there have been more members in a call.
|
|
// Its capped by room member participants.
|
|
const shouldDisambiguateTrackerMap = new Set<string>();
|
|
return scope.behavior(
|
|
combineLatest([
|
|
// Handle call membership changes
|
|
memberships$,
|
|
// Additionally handle display name changes (implicitly reacting to them)
|
|
roomMembers$,
|
|
// TODO: do we need: pauseWhen(this.pretendToBeDisconnected$),
|
|
]).pipe(
|
|
map(([memberships, roomMembers]) => {
|
|
const displaynameMap = new Map<string, string>();
|
|
// We only consider RTC members for disambiguation as they are the only visible members.
|
|
for (const rtcMember of memberships) {
|
|
const member = roomMembers.get(rtcMember.userId);
|
|
if (!member) {
|
|
logger.error(`Could not find member for user ${rtcMember.userId}`);
|
|
continue;
|
|
}
|
|
const disambiguateComputed = shouldDisambiguate(
|
|
member,
|
|
memberships,
|
|
roomMembers,
|
|
);
|
|
|
|
const disambiguate =
|
|
shouldDisambiguateTrackerMap.has(rtcMember.userId) ||
|
|
disambiguateComputed;
|
|
if (disambiguate) shouldDisambiguateTrackerMap.add(rtcMember.userId);
|
|
displaynameMap.set(
|
|
rtcMember.userId,
|
|
calculateDisplayName(member, disambiguate),
|
|
);
|
|
}
|
|
return displaynameMap;
|
|
}),
|
|
),
|
|
);
|
|
};
|
|
|
|
export const createMatrixMemberMetadata$ = (
|
|
scope: ObservableScope,
|
|
memberships$: Behavior<Pick<CallMembership, "userId">[]>,
|
|
roomMembers$: Behavior<RoomMemberMap>,
|
|
): {
|
|
createDisplayNameBehavior$: (
|
|
scope: ObservableScope,
|
|
userId: string,
|
|
) => Behavior<string | undefined>;
|
|
createAvatarUrlBehavior$: (
|
|
scope: ObservableScope,
|
|
userId: string,
|
|
) => Behavior<string | undefined>;
|
|
displaynameMap$: Behavior<Map<string, string>>;
|
|
avatarMap$: Behavior<Map<string, string | undefined>>;
|
|
} => {
|
|
const displaynameMap$ = memberDisplaynames$(
|
|
scope,
|
|
memberships$,
|
|
roomMembers$,
|
|
);
|
|
const avatarMap$ = scope.behavior(
|
|
roomMembers$.pipe(
|
|
map((roomMembers) =>
|
|
Array.from(roomMembers.keys()).reduce((acc, key) => {
|
|
acc.set(key, roomMembers.get(key)?.getMxcAvatarUrl());
|
|
return acc;
|
|
}, new Map<string, string | undefined>()),
|
|
),
|
|
),
|
|
);
|
|
return {
|
|
createDisplayNameBehavior$: (scope: ObservableScope, userId: string) =>
|
|
scope.behavior(
|
|
displaynameMap$.pipe(
|
|
map((displaynameMap) => displaynameMap.get(userId)),
|
|
),
|
|
),
|
|
createAvatarUrlBehavior$: (scope: ObservableScope, userId: string) =>
|
|
scope.behavior(
|
|
roomMembers$.pipe(
|
|
map((roomMembers) => roomMembers.get(userId)?.getMxcAvatarUrl()),
|
|
),
|
|
),
|
|
// mostly for testing purposes
|
|
displaynameMap$,
|
|
avatarMap$,
|
|
};
|
|
};
|