From 4d0de2fb71e4320ee4919656a1e1b044a3f95c65 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 5 Nov 2025 17:55:36 +0100 Subject: [PATCH] Refactor Matrix/LiveKit session merging - Replace MatrixLivekitItem with MatrixLivekitMember, add displayName$ and participantId, and use explicit LiveKit participant types - Make sessionBehaviors$ accept a props object and return a typed RxRtcSession - Update CallViewModel to use the new session behaviors, rebuild media items from matrixLivekitMembers, handle missing connections and use participantId-based keys - Change localMembership/localTransport to accept Behavior-based options, read options.value for enterRTCSession, and fix advertised transport selection order - Update tests and minor UI adjustments (settings modal livekitRooms stubbed) and fix JSON formatting in locales --- locales/en/app.json | 12 +- src/room/InCallView.tsx | 7 +- src/rtcSessionHelpers.test.ts | 9 +- src/state/CallViewModel.ts | 153 ++++++++++-------- src/state/SessionBehaviors.ts | 23 ++- src/state/localMember/LocalMembership.ts | 17 +- src/state/localMember/LocalTransport.ts | 7 +- src/state/remoteMembers/ConnectionManager.ts | 2 +- .../remoteMembers/MatrixLivekitMerger.test.ts | 32 ++-- .../remoteMembers/matrixLivekitMerger.ts | 40 +++-- 10 files changed, 172 insertions(+), 130 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 104af750..2c6801bc 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -74,16 +74,16 @@ "matrix_id": "Matrix ID: {{id}}", "matrixRTCMode": { "Comptibility": { - "label": "Compatibility: state events & multi SFU" - "description": "Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)", + "label": "Compatibility: state events & multi SFU", + "description": "Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)" }, "Legacy": { - "label": "Legacy: state events & oldest membership SFU" - "description": "Compatible with old versions of EC that do not support multi SFU", + "label": "Legacy: state events & oldest membership SFU", + "description": "Compatible with old versions of EC that do not support multi SFU" }, "Matrix_2_0": { - "label": "Matrix 2.0: sticky events & multi SFU" - "description": "Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later", + "label": "Matrix 2.0: sticky events & multi SFU", + "description": "Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later" } }, "mute_all_audio": "Mute all audio (participants, reactions, join sounds)", diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 6f6bae93..a6a2e897 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -138,7 +138,7 @@ export const ActiveCall: FC = (props) => { }, reactionsReader.raisedHands$, reactionsReader.reactions$, - trackProcessorState$, + scope.behavior(trackProcessorState$), ); setVm(vm); @@ -247,7 +247,7 @@ export const InCallView: FC = ({ () => void toggleRaisedHand(), ); - const allLivekitRooms = useBehavior(vm.allLivekitRooms$); + // const allLivekitRooms = useBehavior(vm.allLivekitRooms$); const audioParticipants = useBehavior(vm.audioParticipants$); const participantCount = useBehavior(vm.participantCount$); const reconnecting = useBehavior(vm.reconnecting$); @@ -841,7 +841,8 @@ export const InCallView: FC = ({ onDismiss={closeSettings} tab={settingsTab} onTabChange={setSettingsTab} - livekitRooms={allLivekitRooms} + // TODO expose correct data to setttings modal + livekitRooms={[]} /> )} diff --git a/src/rtcSessionHelpers.test.ts b/src/rtcSessionHelpers.test.ts index 8aca40f5..a2b49390 100644 --- a/src/rtcSessionHelpers.test.ts +++ b/src/rtcSessionHelpers.test.ts @@ -12,8 +12,9 @@ import EventEmitter from "events"; import { enterRTCSession } from "../src/rtcSessionHelpers"; import { mockConfig } from "./utils/test"; +import { MatrixRTCMode } from "./settings/settings"; -const USE_MUTI_SFU = false; +const MATRIX_RTC_MODE = MatrixRTCMode.Legacy; const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); vi.mock("./UrlParams", () => ({ getUrlParams })); @@ -94,8 +95,7 @@ test("It joins the correct Session", async () => { }, { encryptMedia: true, - useMultiSfu: USE_MUTI_SFU, - preferStickyEvents: false, + matrixRTCMode: MATRIX_RTC_MODE, }, ); @@ -153,8 +153,7 @@ test("It should not fail with configuration error if homeserver config has livek }, { encryptMedia: true, - useMultiSfu: USE_MUTI_SFU, - preferStickyEvents: false, + matrixRTCMode: MATRIX_RTC_MODE, }, ); }); diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 7396a515..c88348d6 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -21,7 +21,6 @@ import { RoomEvent, } from "matrix-js-sdk"; import { - BehaviorSubject, combineLatest, concat, distinctUntilChanged, @@ -38,7 +37,6 @@ import { of, pairwise, race, - repeat, scan, skip, skipWhile, @@ -93,7 +91,6 @@ import { import { shallowEquals } from "../utils/array"; import { type MediaDevices } from "./MediaDevices"; import { type Behavior, constant } from "./Behavior"; -import { enterRTCSession } from "../rtcSessionHelpers"; import { E2eeType } from "../e2ee/e2eeType"; import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { type MuteStates } from "./MuteStates"; @@ -112,12 +109,12 @@ import { type SpotlightPortraitLayoutMedia, } from "./layout-types.ts"; import { type ElementCallError } from "../utils/errors.ts"; -import { ObservableScope } from "./ObservableScope.ts"; +import { type ObservableScope } from "./ObservableScope.ts"; import { ConnectionManager } from "./remoteMembers/ConnectionManager.ts"; import { MatrixLivekitMerger } from "./remoteMembers/matrixLivekitMerger.ts"; import { localMembership$, - LocalMemberState, + type LocalMemberState, } from "./localMember/LocalMembership.ts"; import { localTransport$ as computeLocalTransport$ } from "./localMember/LocalTransport.ts"; import { sessionBehaviors$ } from "./SessionBehaviors.ts"; @@ -195,10 +192,10 @@ export class CallViewModel { } : undefined; - private sessionBehaviors = sessionBehaviors$( - this.scope, - this.matrixRTCSession, - ); + private sessionBehaviors = sessionBehaviors$({ + scope: this.scope, + matrixRTCSession: this.matrixRTCSession, + }); private memberships$ = this.sessionBehaviors.memberships$; private localTransport$ = computeLocalTransport$({ @@ -211,6 +208,8 @@ export class CallViewModel { ), }); + // ------------------------------------------------------------------------ + private connectionFactory = new ECConnectionFactory( this.matrixRoom.client, this.mediaDevices, @@ -219,10 +218,14 @@ export class CallViewModel { getUrlParams().controlledAudioDevices, ); + // Can contain duplicates. The connection manager will take care of this. private allTransports$ = this.scope.behavior( combineLatest( [this.localTransport$, this.sessionBehaviors.transports$], - (l, t) => [...(l ? [l] : []), ...t], + (localTransport, transports) => { + const localTransportAsArray = localTransport ? [localTransport] : []; + return [...localTransportAsArray, ...transports]; + }, ), ); @@ -232,6 +235,8 @@ export class CallViewModel { this.allTransports$, ); + // ------------------------------------------------------------------------ + private matrixLivekitMerger = new MatrixLivekitMerger( this.scope, this.sessionBehaviors.membershipsWithTransport$, @@ -240,7 +245,7 @@ export class CallViewModel { this.userId, this.deviceId, ); - private matrixLivekitItems$ = this.matrixLivekitMerger.matrixLivekitItems$; + private matrixLivekitMembers$ = this.matrixLivekitMerger.matrixLivekitMember$; private localMembership = localMembership$({ scope: this.scope, @@ -297,12 +302,12 @@ export class CallViewModel { // down, for example, and we want to avoid making people worry that the app is // in a split-brained state. // DISCUSSION own membership manager ALSO this probably can be simplifis - private readonly pretendToBeDisconnected$ = - this.localMembership.reconnecting$; + public reconnecting$ = this.localMembership.reconnecting$; + private readonly pretendToBeDisconnected$ = this.reconnecting$; public readonly audioParticipants$ = this.scope.behavior( - this.matrixLivekitItems$.pipe( - map((items) => items.map((item) => item.participant)), + this.matrixLivekitMembers$.pipe( + map((members) => members.map((m) => m.participant)), ), ); @@ -330,72 +335,82 @@ export class CallViewModel { // TODO KEEP THIS!! and adapt it to what our membershipManger returns private readonly mediaItems$ = this.scope.behavior( generateKeyed$< - [typeof this.participantsByRoom$.value, number], + [typeof this.matrixLivekitMembers$.value, number], MediaItem, MediaItem[] >( // Generate a collection of MediaItems from the list of expected (whether // present or missing) LiveKit participants. - combineLatest([this.participantsByRoom$, duplicateTiles.value$]), - ([participantsByRoom, duplicateTiles], createOrGet) => { + combineLatest([this.matrixLivekitMembers$, duplicateTiles.value$]), + ([matrixLivekitMembers, duplicateTiles], createOrGet) => { const items: MediaItem[] = []; - for (const { livekitRoom, participants, url } of participantsByRoom) { - for (const { id, participant, member } of participants) { - for (let i = 0; i < 1 + duplicateTiles; i++) { - const mediaId = `${id}:${i}`; - const item = createOrGet( - mediaId, - (scope) => - // We create UserMedia with or without a participant. - // This will be the initial value of a BehaviourSubject. - // Once a participant appears we will update the BehaviourSubject. (see below) - new UserMedia( - scope, - mediaId, - member, - participant, - this.options.encryptionSystem, - livekitRoom, - url, - this.mediaDevices, - this.pretendToBeDisconnected$, - this.memberDisplaynames$.pipe( - map((m) => m.get(id) ?? "[👻]"), - ), - this.handsRaised$.pipe(map((v) => v[id]?.time ?? null)), - this.reactions$.pipe(map((v) => v[id] ?? undefined)), + for (const { + connection, + participant, + member, + displayName$, + participantId, + } of matrixLivekitMembers) { + if (connection === undefined) { + logger.warn("connection is not yet initialised."); + continue; + } + for (let i = 0; i < 1 + duplicateTiles; i++) { + const mediaId = `${participantId}:${i}`; + const lkRoom = connection?.livekitRoom; + const url = connection?.transport.livekit_service_url; + const dpName$ = displayName$.pipe(map((n) => n ?? "[👻]")); + const item = createOrGet( + mediaId, + (scope) => + // We create UserMedia with or without a participant. + // This will be the initial value of a BehaviourSubject. + // Once a participant appears we will update the BehaviourSubject. (see below) + new UserMedia( + scope, + mediaId, + member, + participant, + this.options.encryptionSystem, + lkRoom, + url, + this.mediaDevices, + this.pretendToBeDisconnected$, + dpName$, + this.handsRaised$.pipe( + map((v) => v[participantId]?.time ?? null), ), - ); - items.push(item); - (item as UserMedia).updateParticipant(participant); + this.reactions$.pipe( + map((v) => v[participantId] ?? undefined), + ), + ), + ); + items.push(item); + (item as UserMedia).updateParticipant(participant); - if (participant?.isScreenShareEnabled) { - const screenShareId = `${mediaId}:screen-share`; - items.push( - createOrGet( - screenShareId, - (scope) => - new ScreenShare( - scope, - screenShareId, - member, - participant, - this.options.encryptionSystem, - livekitRoom, - url, - this.pretendToBeDisconnected$, - this.memberDisplaynames$.pipe( - map((m) => m.get(id) ?? "[👻]"), - ), - ), - ), - ); - } + if (participant?.isScreenShareEnabled) { + const screenShareId = `${mediaId}:screen-share`; + items.push( + createOrGet( + screenShareId, + (scope) => + new ScreenShare( + scope, + screenShareId, + member, + participant, + this.options.encryptionSystem, + lkRoom, + url, + this.pretendToBeDisconnected$, + dpName$, + ), + ), + ); } } } - return items; }, ), diff --git a/src/state/SessionBehaviors.ts b/src/state/SessionBehaviors.ts index 6c16ace4..250ad86a 100644 --- a/src/state/SessionBehaviors.ts +++ b/src/state/SessionBehaviors.ts @@ -17,16 +17,29 @@ import { fromEvent, map } from "rxjs"; import { type ObservableScope } from "./ObservableScope"; import { type Behavior } from "./Behavior"; -export const sessionBehaviors$ = ( - scope: ObservableScope, - matrixRTCSession: MatrixRTCSession, -): { +interface Props { + scope: ObservableScope; + matrixRTCSession: MatrixRTCSession; +} + +/** + * Wraps behaviors that we extract from an matrixRTCSession. + */ +interface RxRtcSession { + /** + * some prop + */ memberships$: Behavior; membershipsWithTransport$: Behavior< { membership: CallMembership; transport?: LivekitTransport }[] >; transports$: Behavior; -} => { +} + +export const sessionBehaviors$ = ({ + scope, + matrixRTCSession, +}: Props): RxRtcSession => { const memberships$ = scope.behavior( fromEvent( matrixRTCSession, diff --git a/src/state/localMember/LocalMembership.ts b/src/state/localMember/LocalMembership.ts index 0bd5fbb1..33a54574 100644 --- a/src/state/localMember/LocalMembership.ts +++ b/src/state/localMember/LocalMembership.ts @@ -40,9 +40,8 @@ import { enterRTCSession, type EnterRTCSessionOptions, } from "../../rtcSessionHelpers"; -import { ElementCallError } from "../../utils/errors"; -import { Widget } from "matrix-widget-api"; -import { ElementWidgetActions, WidgetHelpers } from "../../widget"; +import { type ElementCallError } from "../../utils/errors"; +import { ElementWidgetActions, type WidgetHelpers } from "../../widget"; enum LivekitState { UNINITIALIZED = "uninitialized", @@ -87,6 +86,7 @@ export interface LocalMemberState { * - send join state/sticky event */ interface Props { + options: Behavior; scope: ObservableScope; mediaDevices: MediaDevices; muteStates: MuteStates; @@ -113,6 +113,7 @@ interface Props { */ export const localMembership$ = ({ scope, + options, muteStates, mediaDevices, connectionManager, @@ -124,7 +125,7 @@ export const localMembership$ = ({ widget, }: Props): { // publisher: Publisher - requestConnect: (options: EnterRTCSessionOptions) => LocalMemberState; + requestConnect: () => LocalMemberState; startTracks: () => Behavior; requestDisconnect: () => Observable | null; state: LocalMemberState; // TODO this is probably superseeded by joinState$ @@ -268,9 +269,7 @@ export const localMembership$ = ({ return tracks$; }; - const requestConnect = ( - options: EnterRTCSessionOptions, - ): LocalMemberState => { + const requestConnect = (): LocalMemberState => { if (state.livekit$.value === null) { startTracks(); state.livekit$.next({ state: LivekitState.CONNECTING }); @@ -290,7 +289,7 @@ export const localMembership$ = ({ localTransport$.pipe( tap((transport) => { if (transport !== undefined) { - enterRTCSession(matrixRTCSession, transport, options).catch( + enterRTCSession(matrixRTCSession, transport, options.value).catch( (error) => { logger.error(error); }, @@ -379,7 +378,7 @@ export const localMembership$ = ({ if (advertised !== null && advertised !== undefined) { try { configError$.next(null); - await enterRTCSession(matrixRTCSession, advertised, options); + await enterRTCSession(matrixRTCSession, advertised, options.value); } catch (e) { logger.error("Error entering RTC session", e); } diff --git a/src/state/localMember/LocalTransport.ts b/src/state/localMember/LocalTransport.ts index 7a5202a9..9fada195 100644 --- a/src/state/localMember/LocalTransport.ts +++ b/src/state/localMember/LocalTransport.ts @@ -77,13 +77,12 @@ export const localTransport$ = ({ scope.behavior(from(makeTransport(client, roomId)), undefined); /** - * The transport we should advertise in our MatrixRTC membership (plus whether - * it is a multi-SFU transport and whether we should use sticky events). + * The transport we should advertise in our MatrixRTC membership. */ const advertisedTransport$ = scope.behavior( combineLatest( - [useOldestMember$, preferredTransport$, oldestMemberTransport$], - (useOldestMember, preferredTransport, oldestMemberTransport) => + [useOldestMember$, oldestMemberTransport$, preferredTransport$], + (useOldestMember, oldestMemberTransport, preferredTransport) => useOldestMember ? oldestMemberTransport : preferredTransport, ).pipe(distinctUntilChanged(deepCompare)), undefined, diff --git a/src/state/remoteMembers/ConnectionManager.ts b/src/state/remoteMembers/ConnectionManager.ts index 37c616f8..d0bbfe6f 100644 --- a/src/state/remoteMembers/ConnectionManager.ts +++ b/src/state/remoteMembers/ConnectionManager.ts @@ -107,7 +107,7 @@ export class ConnectionManager { private readonly connectionFactory: ConnectionFactory, private readonly inputTransports$: Behavior, ) { - // TODO logger: only construct one logger from the client and make it compatible via a EC specific singleton. + // TODO logger: only construct one logger from the client and make it compatible via a EC specific sing this.logger = logger.getChild("ConnectionManager"); scope.onEnd(() => this.running$.next(false)); } diff --git a/src/state/remoteMembers/MatrixLivekitMerger.test.ts b/src/state/remoteMembers/MatrixLivekitMerger.test.ts index e3f08405..16fc9c0c 100644 --- a/src/state/remoteMembers/MatrixLivekitMerger.test.ts +++ b/src/state/remoteMembers/MatrixLivekitMerger.test.ts @@ -23,7 +23,7 @@ import { type Room as MatrixRoom } from "matrix-js-sdk"; import { getParticipantId } from "matrix-js-sdk/lib/matrixrtc/utils"; import { - type MatrixLivekitItem, + type MatrixLivekitMember, MatrixLivekitMerger, } from "./matrixLivekitMerger"; import { ObservableScope } from "../ObservableScope"; @@ -79,10 +79,12 @@ afterEach(() => { test("should signal participant not yet connected to livekit", () => { fakeMemberships$.next([aliceRtcMember]); - let items: MatrixLivekitItem[] = []; - matrixLivekitMerger.matrixLivekitItems$.pipe(take(1)).subscribe((emitted) => { - items = emitted; - }); + let items: MatrixLivekitMember[] = []; + matrixLivekitMerger.matrixLivekitMember$ + .pipe(take(1)) + .subscribe((emitted) => { + items = emitted; + }); expect(items).toHaveLength(1); const item = items[0]; @@ -112,10 +114,12 @@ test("should signal participant on a connection that is publishing", () => { ]); fakeManagerData$.next(managerData); - let items: MatrixLivekitItem[] = []; - matrixLivekitMerger.matrixLivekitItems$.pipe(take(1)).subscribe((emitted) => { - items = emitted; - }); + let items: MatrixLivekitMember[] = []; + matrixLivekitMerger.matrixLivekitMember$ + .pipe(take(1)) + .subscribe((emitted) => { + items = emitted; + }); expect(items).toHaveLength(1); const item = items[0]; @@ -136,7 +140,7 @@ test("should signal participant on a connection that is not publishing", () => { managerData.add(fakeConnection, []); fakeManagerData$.next(managerData); - matrixLivekitMerger.matrixLivekitItems$.pipe(take(1)).subscribe((items) => { + matrixLivekitMerger.matrixLivekitMember$.pipe(take(1)).subscribe((items) => { expect(items).toHaveLength(1); const item = items[0]; @@ -177,8 +181,8 @@ describe("Publication edge case", () => { ); test("bob is publishing in several connections", () => { - let lastMatrixLkItems: MatrixLivekitItem[] = []; - matrixLivekitMerger.matrixLivekitItems$.subscribe((items) => { + let lastMatrixLkItems: MatrixLivekitMember[] = []; + matrixLivekitMerger.matrixLivekitMember$.subscribe((items) => { lastMatrixLkItems = items; }); @@ -218,8 +222,8 @@ describe("Publication edge case", () => { }); test("bob is publishing in the wrong connection", () => { - let lastMatrixLkItems: MatrixLivekitItem[] = []; - matrixLivekitMerger.matrixLivekitItems$.subscribe((items) => { + let lastMatrixLkItems: MatrixLivekitMember[] = []; + matrixLivekitMerger.matrixLivekitMember$.subscribe((items) => { lastMatrixLkItems = items; }); diff --git a/src/state/remoteMembers/matrixLivekitMerger.ts b/src/state/remoteMembers/matrixLivekitMerger.ts index 39acc65b..94e0ebd5 100644 --- a/src/state/remoteMembers/matrixLivekitMerger.ts +++ b/src/state/remoteMembers/matrixLivekitMerger.ts @@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type Participant as LivekitParticipant } from "livekit-client"; +import { + type LocalParticipant as LocalLivekitParticipant, + type RemoteParticipant as RemoteLivekitParticipant, +} from "livekit-client"; import { type LivekitTransport, type CallMembership, @@ -27,22 +30,23 @@ import { type Connection } from "./Connection"; * `livekitParticipant` can be undefined if the member is not yet connected to the livekit room * or if it has no livekit transport at all. */ -export interface MatrixLivekitItem { +export interface MatrixLivekitMember { membership: CallMembership; - displayName: string; - participant?: LivekitParticipant; + displayName$: Behavior; + participant?: LocalLivekitParticipant | RemoteLivekitParticipant; connection?: Connection; /** * TODO Try to remove this! Its waaay to much information. * Just get the member's avatar * @deprecated */ - member?: RoomMember; + member: RoomMember; mxcAvatarUrl?: string; + participantId: string; } // Alternative structure idea: -// const livekitMatrixItems$ = (callMemberships$,connectionManager,scope): Observable => { +// const livekitMatrixMember$ = (callMemberships$,connectionManager,scope): Observable => { /** * Combines MatrixRtc and Livekit worlds. @@ -52,13 +56,13 @@ export interface MatrixLivekitItem { * - an observable of CallMembership[] to track the call members (The matrix side) * - a `ConnectionManager` for the lk rooms (The livekit side) * - out (via public Observable): - * - `remoteMatrixLivekitItems` an observable of MatrixLivekitItem[] to track the remote members and associated livekit data. + * - `remoteMatrixLivekitMember` an observable of MatrixLivekitMember[] to track the remote members and associated livekit data. */ export class MatrixLivekitMerger { /** * Stream of all the call members and their associated livekit data (if available). */ - public matrixLivekitItems$: Behavior; + public matrixLivekitMember$: Behavior; // private readonly logger: Logger; @@ -79,7 +83,7 @@ export class MatrixLivekitMerger { ) { // this.logger = parentLogger.getChild("MatrixLivekitMerger"); - this.matrixLivekitItems$ = this.scope.behavior( + this.matrixLivekitMember$ = this.scope.behavior( this.start$().pipe(startWith([])), ); } @@ -87,7 +91,7 @@ export class MatrixLivekitMerger { // ======================================= /// PRIVATES // ======================================= - private start$(): Observable { + private start$(): Observable { const displaynameMap$ = memberDisplaynames$( this.scope, this.matrixRoom, @@ -102,10 +106,9 @@ export class MatrixLivekitMerger { return combineLatest([ membershipsWithTransport$, this.connectionManager.connectionManagerData$, - displaynameMap$, ]).pipe( - map(([memberships, managerData, displayNameMap]) => { - const items: MatrixLivekitItem[] = memberships.map( + map(([memberships, managerData]) => { + const items: MatrixLivekitMember[] = memberships.map( ({ membership, transport }) => { // TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`; @@ -123,14 +126,23 @@ export class MatrixLivekitMerger { const connection = transport ? managerData.getConnectionForTransport(transport) : undefined; + const displayName$ = this.scope.behavior( + displaynameMap$.pipe( + map( + (displayNameMap) => + displayNameMap.get(membership.membershipID) ?? "---", + ), + ), + ); return { participant, membership, connection, // This makes sense to add the the js-sdk callMembership (we only need the avatar so probably the call memberhsip just should aquire the avatar) member, - displayName: displayNameMap.get(membership.membershipID) ?? "---", + displayName$, mxcAvatarUrl: member?.getMxcAvatarUrl(), + participantId, }; }, );