Refactor reactions / hand raised to use rxjs and start ordering tiles based on hand raised. (#2885)
* Add support for using CallViewModel for reactions sounds. * Drop setting * Convert reaction sounds to call view model / rxjs * Use call view model for hand raised reactions * Support raising reactions for matrix rtc members. * Tie up last bits of useReactions * linting * Update calleventaudiorenderer * Update reaction audio renderer * more test bits * All the test bits and pieces * More refactors * Refactor reactions into a sender and receiver. * Fixup reaction toggle button * Adapt reactions test * Tests all pass. * lint * fix a couple of bugs * remove unused helper file * lint * finnish notation * Add tests for useReactionsReader * remove mistaken vitest file * fix * filter * invert * fixup tests with fake timers * Port useReactionsReader hook to ReactionsReader class. * lint * exclude some files from coverage * Add screen share sound effect. * cancel sub on destroy * tidy tidy
This commit is contained in:
24
src/utils/test-fixtures.ts
Normal file
24
src/utils/test-fixtures.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
mockRtcMembership,
|
||||
mockMatrixRoomMember,
|
||||
mockRemoteParticipant,
|
||||
mockLocalParticipant,
|
||||
} from "./test";
|
||||
|
||||
export const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA");
|
||||
export const alice = mockMatrixRoomMember(aliceRtcMember);
|
||||
export const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`;
|
||||
export const aliceParticipant = mockRemoteParticipant({ identity: aliceId });
|
||||
|
||||
export const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
||||
export const local = mockMatrixRoomMember(localRtcMember);
|
||||
export const localParticipant = mockLocalParticipant({ identity: "" });
|
||||
|
||||
export const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB");
|
||||
150
src/utils/test-viewmodel.ts
Normal file
150
src/utils/test-viewmodel.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ConnectionState } from "livekit-client";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
type CallMembership,
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { vitest } from "vitest";
|
||||
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { mockLivekitRoom, mockMatrixRoom, MockRTCSession } from "./test";
|
||||
import {
|
||||
aliceRtcMember,
|
||||
aliceParticipant,
|
||||
localParticipant,
|
||||
localRtcMember,
|
||||
} from "./test-fixtures";
|
||||
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||
|
||||
export function getBasicRTCSession(
|
||||
members: RoomMember[],
|
||||
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember],
|
||||
): {
|
||||
rtcSession: MockRTCSession;
|
||||
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>;
|
||||
} {
|
||||
const matrixRoomId = "!myRoomId:example.com";
|
||||
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
||||
|
||||
const roomEmitter = new EventEmitter();
|
||||
const clientEmitter = new EventEmitter();
|
||||
const matrixRoom = mockMatrixRoom({
|
||||
relations: {
|
||||
getChildEventsForEvent: vitest.fn(),
|
||||
} as Partial<RelationsContainer> as RelationsContainer,
|
||||
client: {
|
||||
getUserId: () => localRtcMember.sender,
|
||||
getDeviceId: () => localRtcMember.deviceId,
|
||||
sendEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
||||
redactEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
||||
decryptEventIfNeeded: vitest.fn().mockResolvedValue(undefined),
|
||||
on: vitest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||
clientEmitter.on(eventName, fn);
|
||||
},
|
||||
),
|
||||
emit: (eventName: string, ...args: unknown[]) =>
|
||||
clientEmitter.emit(eventName, ...args),
|
||||
off: vitest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||
clientEmitter.off(eventName, fn);
|
||||
},
|
||||
),
|
||||
} as Partial<MatrixClient> as MatrixClient,
|
||||
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
||||
roomId: matrixRoomId,
|
||||
on: vitest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||
roomEmitter.on(eventName, fn);
|
||||
},
|
||||
),
|
||||
emit: (eventName: string, ...args: unknown[]) =>
|
||||
roomEmitter.emit(eventName, ...args),
|
||||
off: vitest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||
roomEmitter.off(eventName, fn);
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
|
||||
initialRemoteRtcMemberships,
|
||||
);
|
||||
|
||||
const rtcSession = new MockRTCSession(
|
||||
matrixRoom,
|
||||
localRtcMember,
|
||||
).withMemberships(remoteRtcMemberships$);
|
||||
|
||||
return {
|
||||
rtcSession,
|
||||
remoteRtcMemberships$,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a basic CallViewModel to test components that make use of it.
|
||||
* @param members
|
||||
* @param initialRemoteRtcMemberships
|
||||
* @returns
|
||||
*/
|
||||
export function getBasicCallViewModelEnvironment(
|
||||
members: RoomMember[],
|
||||
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember],
|
||||
): {
|
||||
vm: CallViewModel;
|
||||
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>;
|
||||
rtcSession: MockRTCSession;
|
||||
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
|
||||
reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>;
|
||||
} {
|
||||
const { rtcSession, remoteRtcMemberships$ } = getBasicRTCSession(
|
||||
members,
|
||||
initialRemoteRtcMemberships,
|
||||
);
|
||||
const handRaisedSubject$ = new BehaviorSubject({});
|
||||
const reactionsSubject$ = new BehaviorSubject({});
|
||||
|
||||
const remoteParticipants$ = of([aliceParticipant]);
|
||||
const liveKitRoom = mockLivekitRoom(
|
||||
{ localParticipant },
|
||||
{ remoteParticipants$ },
|
||||
);
|
||||
const vm = new CallViewModel(
|
||||
rtcSession as unknown as MatrixRTCSession,
|
||||
liveKitRoom,
|
||||
{
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
of(ConnectionState.Connected),
|
||||
handRaisedSubject$,
|
||||
reactionsSubject$,
|
||||
);
|
||||
return {
|
||||
vm,
|
||||
remoteRtcMemberships$,
|
||||
rtcSession,
|
||||
handRaisedSubject$: handRaisedSubject$,
|
||||
reactionsSubject$: reactionsSubject$,
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type RemoteTrackPublication,
|
||||
type Room as LivekitRoom,
|
||||
} from "livekit-client";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import {
|
||||
LocalUserMediaViewModel,
|
||||
@@ -132,6 +133,7 @@ export function mockRtcMembership(
|
||||
};
|
||||
const event = new MatrixEvent({
|
||||
sender: typeof user === "string" ? user : user.userId,
|
||||
event_id: `$-ev-${randomUUID()}:example.org`,
|
||||
});
|
||||
return new CallMembership(event, data);
|
||||
}
|
||||
@@ -203,6 +205,8 @@ export async function withLocalMedia(
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
mockLivekitRoom({ localParticipant }),
|
||||
of(null),
|
||||
of(null),
|
||||
);
|
||||
try {
|
||||
await continuation(vm);
|
||||
@@ -239,6 +243,8 @@ export async function withRemoteMedia(
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
mockLivekitRoom({}, { remoteParticipants$: of([remoteParticipant]) }),
|
||||
of(null),
|
||||
of(null),
|
||||
);
|
||||
try {
|
||||
await continuation(vm);
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, type ReactNode } from "react";
|
||||
import { randomUUID } from "crypto";
|
||||
import EventEmitter from "events";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType, RoomEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
MatrixEvent,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
type Room,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import { ReactionsProvider } from "../useReactions";
|
||||
import {
|
||||
type ECallReactionEventContent,
|
||||
ElementCallReactionEventType,
|
||||
type ReactionOption,
|
||||
} from "../reactions";
|
||||
|
||||
export const TestReactionsWrapper = ({
|
||||
rtcSession,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
rtcSession: MockRTCSession | MatrixRTCSession;
|
||||
}>): ReactNode => {
|
||||
return (
|
||||
<ReactionsProvider rtcSession={rtcSession as unknown as MatrixRTCSession}>
|
||||
{children}
|
||||
</ReactionsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export class MockRTCSession extends EventEmitter {
|
||||
public memberships: {
|
||||
sender: string;
|
||||
eventId: string;
|
||||
createdTs: () => Date;
|
||||
}[];
|
||||
|
||||
public constructor(
|
||||
public readonly room: MockRoom,
|
||||
membership: Record<string, string>,
|
||||
) {
|
||||
super();
|
||||
this.memberships = Object.entries(membership).map(([eventId, sender]) => ({
|
||||
sender,
|
||||
eventId,
|
||||
createdTs: (): Date => new Date(),
|
||||
}));
|
||||
}
|
||||
|
||||
public testRemoveMember(userId: string): void {
|
||||
this.memberships = this.memberships.filter((u) => u.sender !== userId);
|
||||
this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||
}
|
||||
|
||||
public testAddMember(sender: string): void {
|
||||
this.memberships.push({
|
||||
sender,
|
||||
eventId: `!fake-${randomUUID()}:event`,
|
||||
createdTs: (): Date => new Date(),
|
||||
});
|
||||
this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
export function createHandRaisedReaction(
|
||||
parentMemberEvent: string,
|
||||
membershipOrOverridenSender: Record<string, string> | string,
|
||||
): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
sender:
|
||||
typeof membershipOrOverridenSender === "string"
|
||||
? membershipOrOverridenSender
|
||||
: membershipOrOverridenSender[parentMemberEvent],
|
||||
type: EventType.Reaction,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
key: "🖐️",
|
||||
event_id: parentMemberEvent,
|
||||
},
|
||||
},
|
||||
event_id: randomUUID(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createRedaction(
|
||||
sender: string,
|
||||
reactionEventId: string,
|
||||
): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
sender,
|
||||
type: EventType.RoomRedaction,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
redacts: reactionEventId,
|
||||
content: {},
|
||||
event_id: randomUUID(),
|
||||
});
|
||||
}
|
||||
|
||||
export class MockRoom extends EventEmitter {
|
||||
public readonly testSentEvents: Parameters<MatrixClient["sendEvent"]>[] = [];
|
||||
public readonly testRedactedEvents: Parameters<
|
||||
MatrixClient["redactEvent"]
|
||||
>[] = [];
|
||||
|
||||
public constructor(
|
||||
private readonly ownUserId: string,
|
||||
private readonly existingRelations: MatrixEvent[] = [],
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get client(): MatrixClient {
|
||||
return {
|
||||
getUserId: (): string => this.ownUserId,
|
||||
sendEvent: async (
|
||||
...props: Parameters<MatrixClient["sendEvent"]>
|
||||
): ReturnType<MatrixClient["sendEvent"]> => {
|
||||
this.testSentEvents.push(props);
|
||||
return Promise.resolve({ event_id: randomUUID() });
|
||||
},
|
||||
redactEvent: async (
|
||||
...props: Parameters<MatrixClient["redactEvent"]>
|
||||
): ReturnType<MatrixClient["redactEvent"]> => {
|
||||
this.testRedactedEvents.push(props);
|
||||
return Promise.resolve({ event_id: randomUUID() });
|
||||
},
|
||||
decryptEventIfNeeded: async () => {},
|
||||
on() {
|
||||
return this;
|
||||
},
|
||||
off() {
|
||||
return this;
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
public get relations(): Room["relations"] {
|
||||
return {
|
||||
getChildEventsForEvent: (membershipEventId: string) => ({
|
||||
getRelations: (): MatrixEvent[] => {
|
||||
return this.existingRelations.filter(
|
||||
(r) =>
|
||||
r.getContent()["m.relates_to"]?.event_id === membershipEventId,
|
||||
);
|
||||
},
|
||||
}),
|
||||
} as unknown as Room["relations"];
|
||||
}
|
||||
|
||||
public testSendHandRaise(
|
||||
parentMemberEvent: string,
|
||||
membershipOrOverridenSender: Record<string, string> | string,
|
||||
): string {
|
||||
const evt = createHandRaisedReaction(
|
||||
parentMemberEvent,
|
||||
membershipOrOverridenSender,
|
||||
);
|
||||
this.emit(RoomEvent.Timeline, evt, this, undefined, false, {
|
||||
timeline: new EventTimeline(new EventTimelineSet(undefined)),
|
||||
});
|
||||
return evt.getId()!;
|
||||
}
|
||||
|
||||
public testSendReaction(
|
||||
parentMemberEvent: string,
|
||||
reaction: ReactionOption,
|
||||
membershipOrOverridenSender: Record<string, string> | string,
|
||||
): string {
|
||||
const evt = new MatrixEvent({
|
||||
sender:
|
||||
typeof membershipOrOverridenSender === "string"
|
||||
? membershipOrOverridenSender
|
||||
: membershipOrOverridenSender[parentMemberEvent],
|
||||
type: ElementCallReactionEventType,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: parentMemberEvent,
|
||||
},
|
||||
emoji: reaction.emoji,
|
||||
name: reaction.name,
|
||||
} satisfies ECallReactionEventContent,
|
||||
event_id: randomUUID(),
|
||||
});
|
||||
|
||||
this.emit(RoomEvent.Timeline, evt, this, undefined, false, {
|
||||
timeline: new EventTimeline(new EventTimelineSet(undefined)),
|
||||
});
|
||||
return evt.getId()!;
|
||||
}
|
||||
|
||||
public getMember(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public testGetAsMatrixRoom(): Room {
|
||||
return this as unknown as Room;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user