Fix avatar reactivity, simplify display names tracking

This commit is contained in:
Robin
2025-11-09 01:16:39 -05:00
parent b4c17ed26d
commit 92ddc4c797
3 changed files with 72 additions and 75 deletions

View File

@@ -13,15 +13,15 @@ import {
type LivekitTransport, type LivekitTransport,
type CallMembership, type CallMembership,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { combineLatest, filter, map } from "rxjs"; import { combineLatest, filter, fromEvent, map, startWith } from "rxjs";
// eslint-disable-next-line rxjs/no-internal // eslint-disable-next-line rxjs/no-internal
import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent"; import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent";
import { type Room as MatrixRoom } from "matrix-js-sdk"; import { RoomStateEvent, type Room as MatrixRoom } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type Behavior } from "../../Behavior"; import { type Behavior } from "../../Behavior";
import { type IConnectionManager } from "./ConnectionManager"; import { type IConnectionManager } from "./ConnectionManager";
import { Epoch, mapEpoch, type ObservableScope } from "../../ObservableScope"; import { Epoch, type ObservableScope } from "../../ObservableScope";
import { memberDisplaynames$ } from "./displayname"; import { memberDisplaynames$ } from "./displayname";
import { type Connection } from "./Connection"; import { type Connection } from "./Connection";
import { generateItemsWithEpoch } from "../../../utils/observable"; import { generateItemsWithEpoch } from "../../../utils/observable";
@@ -82,14 +82,17 @@ export function createMatrixLivekitMembers$({
const displaynameMap$ = memberDisplaynames$( const displaynameMap$ = memberDisplaynames$(
scope, scope,
matrixRoom, matrixRoom,
membershipsWithTransport$.pipe(mapEpoch((v) => v.map((v) => v.membership))), scope.behavior(
membershipsWithTransport$.pipe(
map((ms) => ms.value.map((m) => m.membership)),
),
),
); );
return scope.behavior( return scope.behavior(
combineLatest([ combineLatest([
membershipsWithTransport$, membershipsWithTransport$,
connectionManager.connectionManagerData$, connectionManager.connectionManagerData$,
displaynameMap$,
]).pipe( ]).pipe(
filter((values) => filter((values) =>
values.every((value) => value.epoch === values[0].epoch), values.every((value) => value.epoch === values[0].epoch),
@@ -98,15 +101,11 @@ export function createMatrixLivekitMembers$({
([ ([
{ value: membershipsWithTransports, epoch }, { value: membershipsWithTransports, epoch },
{ value: managerData }, { value: managerData },
{ value: displaynames },
]) => ]) =>
new Epoch( new Epoch([membershipsWithTransports, managerData] as const, epoch),
[membershipsWithTransports, managerData, displaynames] as const,
epoch,
),
), ),
generateItemsWithEpoch( generateItemsWithEpoch(
function* ([membershipsWithTransports, managerData, displaynames]) { function* ([membershipsWithTransports, managerData]) {
for (const { membership, transport } of membershipsWithTransports) { for (const { membership, transport } of membershipsWithTransports) {
// TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to // TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to
const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`; const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`;
@@ -116,35 +115,42 @@ export function createMatrixLivekitMembers$({
: []; : [];
const participant = const participant =
participants.find((p) => p.identity == participantId) ?? null; participants.find((p) => p.identity == participantId) ?? null;
// This makes sense to add to the js-sdk callMembership (we only need the avatar so probably the call memberhsip just should aquire the avatar)
const member = matrixRoom.getMember(membership.userId);
const connection = transport const connection = transport
? managerData.getConnectionForTransport(transport) ? managerData.getConnectionForTransport(transport)
: undefined; : undefined;
let displayName = displaynames.get(membership.userId);
if (displayName === undefined) {
logger.warn(`No display name for user ${membership.userId}`);
displayName = "";
}
yield { yield {
keys: [participantId, membership.userId], keys: [participantId, membership.userId],
data: { data: { membership, participant, connection },
membership,
participant,
connection,
displayName,
mxcAvatarUrl: member?.getMxcAvatarUrl(),
},
}; };
} }
}, },
(scope, data$, participantId, userId) => ({ (scope, data$, participantId, userId) => {
participantId, const member = matrixRoom.getMember(userId);
userId, return {
...scope.splitBehavior(data$), participantId,
}), userId,
...scope.splitBehavior(data$),
displayName$: scope.behavior(
displaynameMap$.pipe(
map((displayNames) => {
const name = displayNames.get(userId);
if (name === undefined) {
logger.warn(`No display name for user ${userId}`);
return "";
}
return name;
}),
),
),
mxcAvatarUrl$: scope.behavior(
fromEvent(matrixRoom, RoomStateEvent.Members).pipe(
startWith(undefined),
map(() => member?.getMxcAvatarUrl()),
),
),
};
},
), ),
), ),
); );

View File

@@ -13,9 +13,8 @@ import {
RoomStateEvent, RoomStateEvent,
} from "matrix-js-sdk"; } from "matrix-js-sdk";
import EventEmitter from "events"; import EventEmitter from "events";
import { map } from "rxjs";
import { ObservableScope, trackEpoch } from "../../ObservableScope.ts"; import { ObservableScope } from "../../ObservableScope.ts";
import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room"; import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room";
import { mockCallMembership, withTestScheduler } from "../../../utils/test.ts"; import { mockCallMembership, withTestScheduler } from "../../../utils/test.ts";
import { memberDisplaynames$ } from "./displayname.ts"; import { memberDisplaynames$ } from "./displayname.ts";
@@ -86,16 +85,16 @@ afterEach(() => {
// TODO this is a regression, now there the own user is not always in the map. Ask Timo if fine // TODO this is a regression, now there the own user is not always in the map. Ask Timo if fine
test.skip("should always have our own user", () => { test.skip("should always have our own user", () => {
withTestScheduler(({ cold, schedule, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
const dn$ = memberDisplaynames$( const dn$ = memberDisplaynames$(
testScope, testScope,
mockMatrixRoom, mockMatrixRoom,
cold("a", { behavior("a", {
a: [], a: [],
}).pipe(trackEpoch()), }),
); );
expectObservable(dn$.pipe(map((e) => e.value))).toBe("a", { expectObservable(dn$).toBe("a", {
a: new Map<string, string>([ a: new Map<string, string>([
["@local:example.com", "@local:example.com"], ["@local:example.com", "@local:example.com"],
]), ]),
@@ -116,19 +115,19 @@ function setUpBasicRoom(): void {
test("should get displayName for users", () => { test("should get displayName for users", () => {
setUpBasicRoom(); setUpBasicRoom();
withTestScheduler(({ cold, schedule, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
const dn$ = memberDisplaynames$( const dn$ = memberDisplaynames$(
testScope, testScope,
mockMatrixRoom, mockMatrixRoom,
cold("a", { behavior("a", {
a: [ a: [
mockCallMembership("@alice:example.com", "DEVICE1"), mockCallMembership("@alice:example.com", "DEVICE1"),
mockCallMembership("@bob:example.com", "DEVICE1"), mockCallMembership("@bob:example.com", "DEVICE1"),
], ],
}).pipe(trackEpoch()), }),
); );
expectObservable(dn$.pipe(map((e) => e.value))).toBe("a", { expectObservable(dn$).toBe("a", {
a: new Map<string, string>([ a: new Map<string, string>([
// ["@local:example.com", "it's a me"], // ["@local:example.com", "it's a me"],
["@alice:example.com", "Alice"], ["@alice:example.com", "Alice"],
@@ -139,18 +138,18 @@ test("should get displayName for users", () => {
}); });
test("should use userId if no display name", () => { test("should use userId if no display name", () => {
withTestScheduler(({ cold, schedule, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
setUpBasicRoom(); setUpBasicRoom();
const dn$ = memberDisplaynames$( const dn$ = memberDisplaynames$(
testScope, testScope,
mockMatrixRoom, mockMatrixRoom,
cold("a", { behavior("a", {
a: [mockCallMembership("@no-name:foo.bar", "D000")], a: [mockCallMembership("@no-name:foo.bar", "D000")],
}).pipe(trackEpoch()), }),
); );
expectObservable(dn$.pipe(map((e) => e.value))).toBe("a", { expectObservable(dn$).toBe("a", {
a: new Map<string, string>([ a: new Map<string, string>([
// ["@local:example.com", "it's a me"], // ["@local:example.com", "it's a me"],
["@no-name:foo.bar", "@no-name:foo.bar"], ["@no-name:foo.bar", "@no-name:foo.bar"],
@@ -160,13 +159,13 @@ test("should use userId if no display name", () => {
}); });
test("should disambiguate users with same display name", () => { test("should disambiguate users with same display name", () => {
withTestScheduler(({ cold, schedule, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
setUpBasicRoom(); setUpBasicRoom();
const dn$ = memberDisplaynames$( const dn$ = memberDisplaynames$(
testScope, testScope,
mockMatrixRoom, mockMatrixRoom,
cold("a", { behavior("a", {
a: [ a: [
mockCallMembership("@bob:example.com", "DEVICE1"), mockCallMembership("@bob:example.com", "DEVICE1"),
mockCallMembership("@bob:example.com", "DEVICE2"), mockCallMembership("@bob:example.com", "DEVICE2"),
@@ -174,10 +173,10 @@ test("should disambiguate users with same display name", () => {
mockCallMembership("@carl:example.com", "C000"), mockCallMembership("@carl:example.com", "C000"),
mockCallMembership("@evil:example.com", "E000"), mockCallMembership("@evil:example.com", "E000"),
], ],
}).pipe(trackEpoch()), }),
); );
expectObservable(dn$.pipe(map((e) => e.value))).toBe("a", { expectObservable(dn$).toBe("a", {
a: new Map<string, string>([ a: new Map<string, string>([
// ["@local:example.com", "it's a me"], // ["@local:example.com", "it's a me"],
["@bob:example.com", "Bob (@bob:example.com)"], ["@bob:example.com", "Bob (@bob:example.com)"],
@@ -191,22 +190,22 @@ test("should disambiguate users with same display name", () => {
}); });
test("should disambiguate when needed", () => { test("should disambiguate when needed", () => {
withTestScheduler(({ cold, schedule, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
setUpBasicRoom(); setUpBasicRoom();
const dn$ = memberDisplaynames$( const dn$ = memberDisplaynames$(
testScope, testScope,
mockMatrixRoom, mockMatrixRoom,
cold("ab", { behavior("ab", {
a: [mockCallMembership("@bob:example.com", "DEVICE1")], a: [mockCallMembership("@bob:example.com", "DEVICE1")],
b: [ b: [
mockCallMembership("@bob:example.com", "DEVICE1"), mockCallMembership("@bob:example.com", "DEVICE1"),
mockCallMembership("@bob:foo.bar", "BOB000"), mockCallMembership("@bob:foo.bar", "BOB000"),
], ],
}).pipe(trackEpoch()), }),
); );
expectObservable(dn$.pipe(map((e) => e.value))).toBe("ab", { expectObservable(dn$).toBe("ab", {
a: new Map<string, string>([ a: new Map<string, string>([
// ["@local:example.com", "it's a me"], // ["@local:example.com", "it's a me"],
["@bob:example.com", "Bob"], ["@bob:example.com", "Bob"],
@@ -221,22 +220,22 @@ test("should disambiguate when needed", () => {
}); });
test.skip("should keep disambiguated name when other leave", () => { test.skip("should keep disambiguated name when other leave", () => {
withTestScheduler(({ cold, schedule, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
setUpBasicRoom(); setUpBasicRoom();
const dn$ = memberDisplaynames$( const dn$ = memberDisplaynames$(
testScope, testScope,
mockMatrixRoom, mockMatrixRoom,
cold("ab", { behavior("ab", {
a: [ a: [
mockCallMembership("@bob:example.com", "DEVICE1"), mockCallMembership("@bob:example.com", "DEVICE1"),
mockCallMembership("@bob:foo.bar", "BOB000"), mockCallMembership("@bob:foo.bar", "BOB000"),
], ],
b: [mockCallMembership("@bob:example.com", "DEVICE1")], b: [mockCallMembership("@bob:example.com", "DEVICE1")],
}).pipe(trackEpoch()), }),
); );
expectObservable(dn$.pipe(map((e) => e.value))).toBe("ab", { expectObservable(dn$).toBe("ab", {
a: new Map<string, string>([ a: new Map<string, string>([
// ["@local:example.com", "it's a me"], // ["@local:example.com", "it's a me"],
["@bob:example.com", "Bob (@bob:example.com)"], ["@bob:example.com", "Bob (@bob:example.com)"],
@@ -251,18 +250,18 @@ test.skip("should keep disambiguated name when other leave", () => {
}); });
test("should disambiguate on name change", () => { test("should disambiguate on name change", () => {
withTestScheduler(({ cold, schedule, expectObservable }) => { withTestScheduler(({ behavior, schedule, expectObservable }) => {
setUpBasicRoom(); setUpBasicRoom();
const dn$ = memberDisplaynames$( const dn$ = memberDisplaynames$(
testScope, testScope,
mockMatrixRoom, mockMatrixRoom,
cold("a", { behavior("a", {
a: [ a: [
mockCallMembership("@bob:example.com", "B000"), mockCallMembership("@bob:example.com", "B000"),
mockCallMembership("@carl:example.com", "C000"), mockCallMembership("@carl:example.com", "C000"),
], ],
}).pipe(trackEpoch()), }),
); );
schedule("-a", { schedule("-a", {
@@ -271,7 +270,7 @@ test("should disambiguate on name change", () => {
}, },
}); });
expectObservable(dn$.pipe(map((e) => e.value))).toBe("ab", { expectObservable(dn$).toBe("ab", {
a: new Map<string, string>([ a: new Map<string, string>([
// ["@local:example.com", "it's a me"], // ["@local:example.com", "it's a me"],
["@bob:example.com", "Bob"], ["@bob:example.com", "Bob"],

View File

@@ -6,20 +6,14 @@ Please see LICENSE in the repository root for full details.
*/ */
import { type RoomMember, RoomStateEvent } from "matrix-js-sdk"; import { type RoomMember, RoomStateEvent } from "matrix-js-sdk";
import { import { combineLatest, fromEvent, map, startWith } from "rxjs";
combineLatest,
fromEvent,
map,
type Observable,
startWith,
} from "rxjs";
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc"; import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type Room as MatrixRoom } from "matrix-js-sdk/lib/matrix"; import { type Room as MatrixRoom } from "matrix-js-sdk/lib/matrix";
// eslint-disable-next-line rxjs/no-internal // eslint-disable-next-line rxjs/no-internal
import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent"; import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent";
import { Epoch, type ObservableScope } from "../../ObservableScope"; import { type ObservableScope } from "../../ObservableScope";
import { import {
calculateDisplayName, calculateDisplayName,
shouldDisambiguate, shouldDisambiguate,
@@ -49,8 +43,8 @@ export const memberDisplaynames$ = (
scope: ObservableScope, scope: ObservableScope,
matrixRoom: Pick<MatrixRoom, "getMember"> & NodeStyleEventEmitter, matrixRoom: Pick<MatrixRoom, "getMember"> & NodeStyleEventEmitter,
// roomMember$: Behavior<Pick<RoomMember, "userId" | "getMxcAvatarUrl">>; // roomMember$: Behavior<Pick<RoomMember, "userId" | "getMxcAvatarUrl">>;
memberships$: Observable<Epoch<CallMembership[]>>, memberships$: Behavior<CallMembership[]>,
): Behavior<Epoch<Map<string, string>>> => ): Behavior<Map<string, string>> =>
scope.behavior( scope.behavior(
combineLatest([ combineLatest([
// Handle call membership changes // Handle call membership changes
@@ -59,8 +53,7 @@ export const memberDisplaynames$ = (
fromEvent(matrixRoom, RoomStateEvent.Members).pipe(startWith(null)), fromEvent(matrixRoom, RoomStateEvent.Members).pipe(startWith(null)),
// TODO: do we need: pauseWhen(this.pretendToBeDisconnected$), // TODO: do we need: pauseWhen(this.pretendToBeDisconnected$),
]).pipe( ]).pipe(
map(([epochMemberships, _displayNames]) => { map(([memberships, _displayNames]) => {
const { epoch, value: memberships } = epochMemberships;
const displaynameMap = new Map<string, string>(); const displaynameMap = new Map<string, string>();
const room = matrixRoom; const room = matrixRoom;
@@ -77,8 +70,7 @@ export const memberDisplaynames$ = (
calculateDisplayName(member, disambiguate), calculateDisplayName(member, disambiguate),
); );
} }
return new Epoch(displaynameMap, epoch); return displaynameMap;
}), }),
), ),
new Epoch(new Map<string, string>()),
); );