mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-28 06:50:26 +00:00
Tests all pass.
This commit is contained in:
@@ -10,15 +10,15 @@ import { expect, test } from "vitest";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { type ReactNode } from "react";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
import { ReactionToggleButton } from "./ReactionToggleButton";
|
||||
import { ElementCallReactionEventType } from "../reactions";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
import { alice, local, localRtcMember } from "../utils/test-fixtures";
|
||||
import { MockRTCSession } from "../utils/test";
|
||||
import { ReactionsSenderProvider } from "../useReactionsSender";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { type MockRTCSession } from "../utils/test";
|
||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||
|
||||
const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;
|
||||
|
||||
@@ -71,7 +71,7 @@ test("Can raise hand", async () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await act(() => {
|
||||
act(() => {
|
||||
// Mock receiving a reaction.
|
||||
handRaisedSubject.next({
|
||||
[localIdent]: {
|
||||
@@ -94,7 +94,7 @@ test("Can lower hand", async () => {
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.raise_hand"));
|
||||
await act(() => {
|
||||
act(() => {
|
||||
handRaisedSubject.next({
|
||||
[localIdent]: {
|
||||
time: new Date(),
|
||||
@@ -109,7 +109,7 @@ test("Can lower hand", async () => {
|
||||
rtcSession.room.roomId,
|
||||
reactionEventId,
|
||||
);
|
||||
await act(() => {
|
||||
act(() => {
|
||||
// Mock receiving a redacted reaction.
|
||||
handRaisedSubject.next({});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ import classNames from "classnames";
|
||||
import { useObservableState } from "observable-hooks";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { useReactionsSender } from "../useReactionsSender";
|
||||
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||
import styles from "./ReactionToggleButton.module.css";
|
||||
import {
|
||||
type ReactionOption,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
ReactionsRowSize,
|
||||
} from "../reactions";
|
||||
import { Modal } from "../Modal";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
|
||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
raised: boolean;
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { type MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { type ReactionEventContent } from "matrix-js-sdk/src/types";
|
||||
import {
|
||||
RelationType,
|
||||
EventType,
|
||||
RoomEvent as MatrixRoomEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { BehaviorSubject, type Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
ElementCallReactionEventType,
|
||||
ECallReactionEventContent,
|
||||
type ECallReactionEventContent,
|
||||
GenericReaction,
|
||||
ReactionSet,
|
||||
RaisedHandInfo,
|
||||
ReactionInfo,
|
||||
type RaisedHandInfo,
|
||||
type ReactionInfo,
|
||||
} from ".";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||
|
||||
const REACTION_ACTIVE_TIME_MS = 3000;
|
||||
|
||||
@@ -256,6 +264,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
||||
// may still be sending.
|
||||
room.on(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
||||
|
||||
const innerReactionsSubject = reactionsSubject.current;
|
||||
return (): void => {
|
||||
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
|
||||
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
|
||||
@@ -263,7 +272,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
||||
room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
||||
reactionTimeouts.forEach((t) => clearTimeout(t));
|
||||
// If we're clearing timeouts, we also clear all reactions.
|
||||
reactionsSubject.current.next({});
|
||||
innerReactionsSubject.next({});
|
||||
};
|
||||
}, [
|
||||
room,
|
||||
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
} from "react";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
|
||||
import { useClientState } from "./ClientContext";
|
||||
import { ElementCallReactionEventType, type ReactionOption } from "./reactions";
|
||||
import { CallViewModel } from "./state/CallViewModel";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||
import { useClientState } from "../ClientContext";
|
||||
import { ElementCallReactionEventType, type ReactionOption } from ".";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
|
||||
interface ReactionsSenderContextType {
|
||||
supportsReactions: boolean;
|
||||
toggleRaisedHand: () => Promise<void>;
|
||||
@@ -76,7 +76,7 @@ export function CallEventAudioRenderer({
|
||||
});
|
||||
|
||||
const handRaisedSub = vm.newHandRaised.subscribe(() => {
|
||||
audioEngineRef.current?.playSound("raiseHand");
|
||||
void audioEngineRef.current?.playSound("raiseHand");
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { JoinRule, type RoomState } from "matrix-js-sdk/src/matrix";
|
||||
import { Router } from "react-router-dom";
|
||||
import { createBrowserHistory } from "history";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||
|
||||
import { type MuteStates } from "./MuteStates";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
@@ -29,7 +30,6 @@ import { GroupCallView } from "./GroupCallView";
|
||||
import { leaveRTCSession } from "../rtcSessionHelpers";
|
||||
import { type WidgetHelpers } from "../widget";
|
||||
import { LazyEventEmitter } from "../LazyEventEmitter";
|
||||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||
|
||||
vitest.mock("../soundUtils");
|
||||
vitest.mock("../useAudioContext");
|
||||
|
||||
@@ -86,7 +86,7 @@ import { GridTileViewModel, type TileViewModel } from "../state/TileViewModel";
|
||||
import {
|
||||
ReactionsSenderProvider,
|
||||
useReactionsSender,
|
||||
} from "../useReactionsSender";
|
||||
} from "../reactions/useReactionsSender";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { useSwitchCamera } from "./useSwitchCamera";
|
||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import { GenericReaction, ReactionSet } from "../reactions";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
import {
|
||||
alice,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { GenericReaction, ReactionSet } from "../reactions";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
|
||||
const soundMap = Object.fromEntries([
|
||||
...ReactionSet.filter((v) => v.sound !== undefined).map((v) => [
|
||||
@@ -51,10 +51,10 @@ export function ReactionsAudioRenderer({
|
||||
const sub = vm.audibleReactions.subscribe((newReactions) => {
|
||||
for (const reactionName of newReactions) {
|
||||
if (soundMap[reactionName]) {
|
||||
audioEngineRef.current?.playSound(reactionName);
|
||||
void audioEngineRef.current?.playSound(reactionName);
|
||||
} else {
|
||||
// Fallback sounds.
|
||||
audioEngineRef.current?.playSound("generic");
|
||||
void audioEngineRef.current?.playSound("generic");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,13 +34,15 @@ test("defaults to showing no reactions", () => {
|
||||
|
||||
test("shows a reaction when sent", () => {
|
||||
showReactions.setValue(true);
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
const { getByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||
const reaction = ReactionSet[0];
|
||||
act(() => {
|
||||
vm.updateReactions({
|
||||
reactions: { [aliceRtcMember.deviceId]: reaction },
|
||||
raisedHands: {},
|
||||
reactionsSubject.next({
|
||||
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||
});
|
||||
});
|
||||
const span = getByRole("presentation");
|
||||
@@ -51,15 +53,15 @@ test("shows a reaction when sent", () => {
|
||||
test("shows two of the same reaction when sent", () => {
|
||||
showReactions.setValue(true);
|
||||
const reaction = ReactionSet[0];
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||
act(() => {
|
||||
vm.updateReactions({
|
||||
reactions: {
|
||||
[aliceRtcMember.deviceId]: reaction,
|
||||
[bobRtcMember.deviceId]: reaction,
|
||||
},
|
||||
raisedHands: {},
|
||||
reactionsSubject.next({
|
||||
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||
[bobRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||
});
|
||||
});
|
||||
expect(getAllByRole("presentation")).toHaveLength(2);
|
||||
@@ -68,15 +70,15 @@ test("shows two of the same reaction when sent", () => {
|
||||
test("shows two different reactions when sent", () => {
|
||||
showReactions.setValue(true);
|
||||
const [reactionA, reactionB] = ReactionSet;
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||
act(() => {
|
||||
vm.updateReactions({
|
||||
reactions: {
|
||||
[aliceRtcMember.deviceId]: reactionA,
|
||||
[bobRtcMember.deviceId]: reactionB,
|
||||
},
|
||||
raisedHands: {},
|
||||
reactionsSubject.next({
|
||||
[aliceRtcMember.deviceId]: { reactionOption: reactionA, ttl: 0 },
|
||||
[bobRtcMember.deviceId]: { reactionOption: reactionB, ttl: 0 },
|
||||
});
|
||||
});
|
||||
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
|
||||
@@ -87,12 +89,14 @@ test("shows two different reactions when sent", () => {
|
||||
test("hides reactions when reaction animations are disabled", () => {
|
||||
showReactions.setValue(false);
|
||||
const reaction = ReactionSet[0];
|
||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
const { container } = render(<ReactionsOverlay vm={vm} />);
|
||||
act(() => {
|
||||
vm.updateReactions({
|
||||
reactions: { [aliceRtcMember.deviceId]: reaction },
|
||||
raisedHands: {},
|
||||
reactionsSubject.next({
|
||||
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||
});
|
||||
});
|
||||
expect(container.getElementsByTagName("span")).toHaveLength(0);
|
||||
|
||||
@@ -6,10 +6,10 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import { useObservableState } from "observable-hooks";
|
||||
|
||||
import styles from "./ReactionsOverlay.module.css";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { useObservableState } from "observable-hooks";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
|
||||
export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode {
|
||||
const reactionsIcons = useObservableState(vm.visibleReactions);
|
||||
|
||||
@@ -5,8 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, vi, onTestFinished, it, vitest } from "vitest";
|
||||
import { test, vi, onTestFinished, it } from "vitest";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
type Observable,
|
||||
of,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
type ECConnectionState,
|
||||
} from "../livekit/useECConnectionState";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { RaisedHandInfo } from "../reactions";
|
||||
|
||||
vi.mock("@livekit/components-core");
|
||||
|
||||
@@ -190,7 +191,10 @@ function withCallViewModel(
|
||||
rtcMembers: Observable<Partial<CallMembership>[]>,
|
||||
connectionState: Observable<ECConnectionState>,
|
||||
speaking: Map<Participant, Observable<boolean>>,
|
||||
continuation: (vm: CallViewModel) => void,
|
||||
continuation: (
|
||||
vm: CallViewModel,
|
||||
subjects: { raisedHands: BehaviorSubject<Record<string, RaisedHandInfo>> },
|
||||
) => void,
|
||||
): void {
|
||||
const room = mockMatrixRoom({
|
||||
client: {
|
||||
@@ -235,6 +239,8 @@ function withCallViewModel(
|
||||
{ remoteParticipants },
|
||||
);
|
||||
|
||||
const raisedHands = new BehaviorSubject<Record<string, RaisedHandInfo>>({});
|
||||
|
||||
const vm = new CallViewModel(
|
||||
rtcSession as unknown as MatrixRTCSession,
|
||||
liveKitRoom,
|
||||
@@ -242,6 +248,8 @@ function withCallViewModel(
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
connectionState,
|
||||
raisedHands,
|
||||
new BehaviorSubject({}),
|
||||
);
|
||||
|
||||
onTestFinished(() => {
|
||||
@@ -252,7 +260,7 @@ function withCallViewModel(
|
||||
roomEventSelectorSpy!.mockRestore();
|
||||
});
|
||||
|
||||
continuation(vm);
|
||||
continuation(vm, { raisedHands });
|
||||
}
|
||||
|
||||
test("participants are retained during a focus switch", () => {
|
||||
@@ -689,117 +697,63 @@ it("should show at least one tile per MatrixRTCSession", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Should correctly handle a raised hand event", () => {
|
||||
withTestScheduler(({ expectObservable }) => {
|
||||
const date = new Date();
|
||||
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
// There should always be one tile for each MatrixRTCSession
|
||||
const expectedLayoutMarbles = "ab";
|
||||
|
||||
withCallViewModel(
|
||||
of([]),
|
||||
of([]),
|
||||
of([aliceParticipant, bobParticipant]),
|
||||
of([aliceRtcMember, bobRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
vm.addHandRaised(alice.userId, date);
|
||||
expectObservable(vm.handsRaised).toBe("a", {
|
||||
a: {
|
||||
[alice.userId]: date,
|
||||
(vm, { raisedHands }) => {
|
||||
schedule("ab", {
|
||||
a: () => {
|
||||
// We imagine that only three tiles (the first three) will be visible
|
||||
// on screen at a time
|
||||
vm.layout.subscribe((layout) => {
|
||||
if (layout.type === "grid") {
|
||||
for (let i = 0; i < layout.grid.length; i++)
|
||||
layout.grid[i].setVisible(i <= 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
b: () => {
|
||||
raisedHands.next({
|
||||
[`${bobRtcMember.sender}:${bobRtcMember.deviceId}`]: {
|
||||
time: new Date(),
|
||||
reactionEventId: "",
|
||||
membershipEventId: "",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
expectObservable(vm.handRaised).toBe("a", {
|
||||
a: {
|
||||
value: 1,
|
||||
playSounds: true,
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [
|
||||
"local:0",
|
||||
"@alice:example.org:AAAA:0",
|
||||
"@bob:example.org:BBBB:0",
|
||||
],
|
||||
},
|
||||
b: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [
|
||||
"local:0",
|
||||
// Bob shifts up!
|
||||
"@bob:example.org:BBBB:0",
|
||||
"@alice:example.org:AAAA:0",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.only("Should correctly handle a lowered hand event", () => {
|
||||
const date = new Date();
|
||||
withTestScheduler(({ expectObservable }) => {
|
||||
withCallViewModel(
|
||||
of([]),
|
||||
of([]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
vm.addHandRaised(alice.userId, date);
|
||||
vm.addHandRaised(bob.userId, date);
|
||||
expectObservable(vm.handsRaised).toBe("a", {
|
||||
a: {
|
||||
[bob.userId]: date,
|
||||
},
|
||||
b: {
|
||||
[bob.userId]: date,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add presenters and speakers?
|
||||
it.todo(
|
||||
"should rank raised hands above video feeds and below speakers and presenters",
|
||||
() => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
// There should always be one tile for each MatrixRTCSession
|
||||
const expectedLayoutMarbles = "a";
|
||||
|
||||
withCallViewModel(
|
||||
of([aliceParticipant, bobParticipant]),
|
||||
of([aliceRtcMember, bobRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
schedule("ah", {
|
||||
a: () => {
|
||||
// We imagine that only three tiles (the first three) will be visible
|
||||
// on screen at a time
|
||||
vm.layout.subscribe((layout) => {
|
||||
console.log(layout);
|
||||
if (layout.type === "grid") {
|
||||
for (let i = 0; i < layout.grid.length; i++)
|
||||
layout.grid[i].setVisible(i <= 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
h: () => {
|
||||
vm.updateReactions({
|
||||
reactions: {},
|
||||
raisedHands: {
|
||||
[`${bobRtcMember.sender}:${bobRtcMember.deviceId}`]:
|
||||
new Date(),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [
|
||||
"local:0",
|
||||
"@bob:example.org:BBBB:0",
|
||||
"@alice:example.org:AAAA:0",
|
||||
],
|
||||
},
|
||||
h: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [
|
||||
"local:0",
|
||||
"@bob:example.org:BBBB:0",
|
||||
"@alice:example.org:AAAA:0",
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
switchMap,
|
||||
switchScan,
|
||||
take,
|
||||
tap,
|
||||
timer,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
@@ -88,7 +87,11 @@ import { oneOnOneLayout } from "./OneOnOneLayout";
|
||||
import { pipLayout } from "./PipLayout";
|
||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { observeSpeaker } from "./observeSpeaker";
|
||||
import { RaisedHandInfo, ReactionInfo, ReactionOption } from "../reactions";
|
||||
import {
|
||||
type RaisedHandInfo,
|
||||
type ReactionInfo,
|
||||
type ReactionOption,
|
||||
} from "../reactions";
|
||||
|
||||
// How long we wait after a focus switch before showing the real participant
|
||||
// list again
|
||||
@@ -251,8 +254,8 @@ class UserMedia {
|
||||
participant: LocalParticipant | RemoteParticipant | undefined,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
livekitRoom: LivekitRoom,
|
||||
handRaised: Observable<Date | undefined>,
|
||||
reactions: Observable<ReactionOption | undefined>,
|
||||
handRaised: Observable<Date | null>,
|
||||
reactions: Observable<ReactionOption | null>,
|
||||
) {
|
||||
this.participant = new BehaviorSubject(participant);
|
||||
|
||||
@@ -528,7 +531,7 @@ export class CallViewModel extends ViewModel {
|
||||
this.encryptionSystem,
|
||||
this.livekitRoom,
|
||||
this.handsRaised.pipe(
|
||||
map((v) => v[matrixIdentifier].time ?? undefined),
|
||||
map((v) => v[matrixIdentifier]?.time ?? null),
|
||||
),
|
||||
this.reactions.pipe(
|
||||
map((v) => v[matrixIdentifier] ?? undefined),
|
||||
@@ -649,7 +652,6 @@ export class CallViewModel extends ViewModel {
|
||||
m.vm.handRaised,
|
||||
],
|
||||
(speaker, presenter, video, alwaysShow, handRaised) => {
|
||||
console.log(m.vm.id, handRaised);
|
||||
let bin: SortingBin;
|
||||
if (m.vm.local)
|
||||
bin = alwaysShow
|
||||
@@ -672,12 +674,6 @@ export class CallViewModel extends ViewModel {
|
||||
bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm),
|
||||
);
|
||||
}),
|
||||
tap((v) =>
|
||||
console.log(
|
||||
"final grid",
|
||||
v.map((v) => v.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
private readonly spotlight: Observable<MediaViewModel[]> =
|
||||
|
||||
@@ -51,7 +51,7 @@ import { alwaysShowSelf } from "../settings/settings";
|
||||
import { accumulate } from "../utils/observable";
|
||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { ReactionOption } from "../reactions";
|
||||
import { type ReactionOption } from "../reactions";
|
||||
|
||||
// TODO: Move this naming logic into the view model
|
||||
export function useDisplayName(vm: MediaViewModel): string {
|
||||
@@ -372,8 +372,8 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
||||
participant: Observable<LocalParticipant | RemoteParticipant | undefined>,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
livekitRoom: LivekitRoom,
|
||||
public readonly handRaised: Observable<Date | undefined>,
|
||||
public readonly reactions: Observable<ReactionOption | undefined>,
|
||||
public readonly handRaised: Observable<Date | null>,
|
||||
public readonly reactions: Observable<ReactionOption | null>,
|
||||
) {
|
||||
super(
|
||||
id,
|
||||
@@ -440,8 +440,8 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
participant: Observable<LocalParticipant | undefined>,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
livekitRoom: LivekitRoom,
|
||||
handRaised: Observable<Date | undefined>,
|
||||
reactions: Observable<ReactionOption | undefined>,
|
||||
handRaised: Observable<Date | null>,
|
||||
reactions: Observable<ReactionOption | null>,
|
||||
) {
|
||||
super(
|
||||
id,
|
||||
@@ -511,8 +511,8 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
participant: Observable<RemoteParticipant | undefined>,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
livekitRoom: LivekitRoom,
|
||||
handRaised: Observable<Date | undefined>,
|
||||
reactions: Observable<ReactionOption | undefined>,
|
||||
handRaised: Observable<Date | null>,
|
||||
reactions: Observable<ReactionOption | null>,
|
||||
) {
|
||||
super(
|
||||
id,
|
||||
|
||||
@@ -15,7 +15,8 @@ import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSess
|
||||
import { GridTile } from "./GridTile";
|
||||
import { mockRtcMembership, withRemoteMedia } from "../utils/test";
|
||||
import { GridTileViewModel } from "../state/TileViewModel";
|
||||
import { ReactionsProvider } from "../useReactionsSender";
|
||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
|
||||
global.IntersectionObserver = class MockIntersectionObserver {
|
||||
public observe(): void {}
|
||||
@@ -51,8 +52,12 @@ test("GridTile is accessible", async () => {
|
||||
},
|
||||
memberships: [],
|
||||
} as unknown as MatrixRTCSession;
|
||||
const cVm = {
|
||||
reactions: of({}),
|
||||
handsRaised: of({}),
|
||||
} as Partial<CallViewModel> as CallViewModel;
|
||||
const { container } = render(
|
||||
<ReactionsProvider rtcSession={fakeRtcSession}>
|
||||
<ReactionsSenderProvider vm={cVm} rtcSession={fakeRtcSession}>
|
||||
<GridTile
|
||||
vm={new GridTileViewModel(of(vm))}
|
||||
onOpenProfile={() => {}}
|
||||
@@ -60,7 +65,7 @@ test("GridTile is accessible", async () => {
|
||||
targetHeight={200}
|
||||
showSpeakingIndicators
|
||||
/>
|
||||
</ReactionsProvider>,
|
||||
</ReactionsSenderProvider>,
|
||||
);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
// Name should be visible
|
||||
|
||||
@@ -48,7 +48,7 @@ import { MediaView } from "./MediaView";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { type GridTileViewModel } from "../state/TileViewModel";
|
||||
import { useMergedRefs } from "../useMergedRefs";
|
||||
import { useReactionsSender } from "../useReactionsSender";
|
||||
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||
|
||||
interface TileProps {
|
||||
className?: string;
|
||||
@@ -170,8 +170,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
{menu}
|
||||
</Menu>
|
||||
}
|
||||
raisedHandTime={handRaised}
|
||||
currentReaction={reaction}
|
||||
raisedHandTime={handRaised ?? undefined}
|
||||
currentReaction={reaction ?? undefined}
|
||||
raisedHandOnClick={raisedHandOnClick}
|
||||
localParticipant={vm.local}
|
||||
{...props}
|
||||
|
||||
@@ -1,171 +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 { render } from "@testing-library/react";
|
||||
import { act, type FC } from "react";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useReactionsSender } from "./useReactionsSender";
|
||||
import {
|
||||
createHandRaisedReaction,
|
||||
createRedaction,
|
||||
MockRoom,
|
||||
} from "./utils/testReactions";
|
||||
|
||||
const memberUserIdAlice = "@alice:example.org";
|
||||
const memberEventAlice = "$membership-alice:example.org";
|
||||
const memberUserIdBob = "@bob:example.org";
|
||||
const memberEventBob = "$membership-bob:example.org";
|
||||
|
||||
const membership: Record<string, string> = {
|
||||
[memberEventAlice]: memberUserIdAlice,
|
||||
[memberEventBob]: memberUserIdBob,
|
||||
"$membership-charlie:example.org": "@charlie:example.org",
|
||||
};
|
||||
|
||||
/**
|
||||
* Test explanation.
|
||||
* This test suite checks that the useReactions hook appropriately reacts
|
||||
* to new reactions, redactions and membership changesin the room. There is
|
||||
* a large amount of test structure used to construct a mock environment.
|
||||
*/
|
||||
|
||||
const TestComponent: FC = () => {
|
||||
const { raisedHands } = useReactionsSender();
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{Object.entries(raisedHands).map(([userId, date]) => (
|
||||
<li key={userId}>
|
||||
<span>{userId}</span>
|
||||
<time>{date.getTime()}</time>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe("useReactions", () => {
|
||||
test("starts with an empty list", () => {
|
||||
const rtcSession = new ReactionsMockRTCSession(
|
||||
new MockRoom(memberUserIdAlice),
|
||||
membership,
|
||||
);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||
});
|
||||
test("handles incoming raised hand", async () => {
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
await act(() => room.testSendHandRaise(memberEventAlice, membership));
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||
await act(() => room.testSendHandRaise(memberEventBob, membership));
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(2);
|
||||
});
|
||||
test("handles incoming unraised hand", async () => {
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
const reactionEventId = await act(() =>
|
||||
room.testSendHandRaise(memberEventAlice, membership),
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||
await act(() =>
|
||||
room.emit(
|
||||
RoomEvent.Redaction,
|
||||
createRedaction(memberUserIdAlice, reactionEventId),
|
||||
room,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||
});
|
||||
test("handles loading prior raised hand events", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, membership),
|
||||
]);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||
});
|
||||
// If the membership event changes for a user, we want to remove
|
||||
// the raised hand event.
|
||||
test("will remove reaction when a member leaves the call", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, membership),
|
||||
]);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||
act(() => rtcSession.testRemoveMember(memberUserIdAlice));
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||
});
|
||||
test("will remove reaction when a member joins via a new event", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, membership),
|
||||
]);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||
// Simulate leaving and rejoining
|
||||
act(() => {
|
||||
rtcSession.testRemoveMember(memberUserIdAlice);
|
||||
rtcSession.testAddMember(memberUserIdAlice);
|
||||
});
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||
});
|
||||
test("ignores invalid sender for historic event", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, memberUserIdBob),
|
||||
]);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||
});
|
||||
test("ignores invalid sender for new event", async () => {
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new ReactionsMockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
await act(() => room.testSendHandRaise(memberEventAlice, memberUserIdBob));
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,10 @@
|
||||
/*
|
||||
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,
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
/*
|
||||
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 { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { CallMembership, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
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 { E2eeType } from "../e2ee/e2eeType";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { mockLivekitRoom, mockMatrixRoom, MockRTCSession } from "./test";
|
||||
@@ -13,8 +22,7 @@ import {
|
||||
localParticipant,
|
||||
localRtcMember,
|
||||
} from "./test-fixtures";
|
||||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||
import { RaisedHandInfo, ReactionInfo } from "../reactions";
|
||||
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||
|
||||
/**
|
||||
* Construct a basic CallViewModel to test components that make use of it.
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type RemoteTrackPublication,
|
||||
type Room as LivekitRoom,
|
||||
} from "livekit-client";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import {
|
||||
LocalUserMediaViewModel,
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
type ResolvedConfigOptions,
|
||||
} from "../config/ConfigOptions";
|
||||
import { Config } from "../config/Config";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export function withFakeTimers(continuation: () => void): void {
|
||||
vi.useFakeTimers();
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
EventTimelineSet,
|
||||
type Room,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
type ECallReactionEventContent,
|
||||
ElementCallReactionEventType,
|
||||
|
||||
Reference in New Issue
Block a user