mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-02 19:49:23 +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 { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { userEvent } from "@testing-library/user-event";
|
import { userEvent } from "@testing-library/user-event";
|
||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import { ReactionToggleButton } from "./ReactionToggleButton";
|
import { ReactionToggleButton } from "./ReactionToggleButton";
|
||||||
import { ElementCallReactionEventType } from "../reactions";
|
import { ElementCallReactionEventType } from "../reactions";
|
||||||
import { CallViewModel } from "../state/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel";
|
||||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||||
import { alice, local, localRtcMember } from "../utils/test-fixtures";
|
import { alice, local, localRtcMember } from "../utils/test-fixtures";
|
||||||
import { MockRTCSession } from "../utils/test";
|
import { type MockRTCSession } from "../utils/test";
|
||||||
import { ReactionsSenderProvider } from "../useReactionsSender";
|
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
|
||||||
|
|
||||||
const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;
|
const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ test("Can raise hand", async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await act(() => {
|
act(() => {
|
||||||
// Mock receiving a reaction.
|
// Mock receiving a reaction.
|
||||||
handRaisedSubject.next({
|
handRaisedSubject.next({
|
||||||
[localIdent]: {
|
[localIdent]: {
|
||||||
@@ -94,7 +94,7 @@ test("Can lower hand", async () => {
|
|||||||
);
|
);
|
||||||
await user.click(getByLabelText("common.reactions"));
|
await user.click(getByLabelText("common.reactions"));
|
||||||
await user.click(getByLabelText("action.raise_hand"));
|
await user.click(getByLabelText("action.raise_hand"));
|
||||||
await act(() => {
|
act(() => {
|
||||||
handRaisedSubject.next({
|
handRaisedSubject.next({
|
||||||
[localIdent]: {
|
[localIdent]: {
|
||||||
time: new Date(),
|
time: new Date(),
|
||||||
@@ -109,7 +109,7 @@ test("Can lower hand", async () => {
|
|||||||
rtcSession.room.roomId,
|
rtcSession.room.roomId,
|
||||||
reactionEventId,
|
reactionEventId,
|
||||||
);
|
);
|
||||||
await act(() => {
|
act(() => {
|
||||||
// Mock receiving a redacted reaction.
|
// Mock receiving a redacted reaction.
|
||||||
handRaisedSubject.next({});
|
handRaisedSubject.next({});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import classNames from "classnames";
|
|||||||
import { useObservableState } from "observable-hooks";
|
import { useObservableState } from "observable-hooks";
|
||||||
import { map } from "rxjs";
|
import { map } from "rxjs";
|
||||||
|
|
||||||
import { useReactionsSender } from "../useReactionsSender";
|
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||||
import styles from "./ReactionToggleButton.module.css";
|
import styles from "./ReactionToggleButton.module.css";
|
||||||
import {
|
import {
|
||||||
type ReactionOption,
|
type ReactionOption,
|
||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
ReactionsRowSize,
|
ReactionsRowSize,
|
||||||
} from "../reactions";
|
} from "../reactions";
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import { CallViewModel } from "../state/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel";
|
||||||
|
|
||||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
raised: boolean;
|
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 { useCallback, useEffect, useRef } from "react";
|
||||||
import { useLatest } from "../useLatest";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
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 { type ReactionEventContent } from "matrix-js-sdk/src/types";
|
||||||
import {
|
import {
|
||||||
RelationType,
|
RelationType,
|
||||||
EventType,
|
EventType,
|
||||||
RoomEvent as MatrixRoomEvent,
|
RoomEvent as MatrixRoomEvent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
import { BehaviorSubject, type Observable } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElementCallReactionEventType,
|
ElementCallReactionEventType,
|
||||||
ECallReactionEventContent,
|
type ECallReactionEventContent,
|
||||||
GenericReaction,
|
GenericReaction,
|
||||||
ReactionSet,
|
ReactionSet,
|
||||||
RaisedHandInfo,
|
type RaisedHandInfo,
|
||||||
ReactionInfo,
|
type ReactionInfo,
|
||||||
} from ".";
|
} from ".";
|
||||||
import { BehaviorSubject, Observable } from "rxjs";
|
import { useLatest } from "../useLatest";
|
||||||
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
|
|
||||||
const REACTION_ACTIVE_TIME_MS = 3000;
|
const REACTION_ACTIVE_TIME_MS = 3000;
|
||||||
|
|
||||||
@@ -256,6 +264,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
|||||||
// may still be sending.
|
// may still be sending.
|
||||||
room.on(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
room.on(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
||||||
|
|
||||||
|
const innerReactionsSubject = reactionsSubject.current;
|
||||||
return (): void => {
|
return (): void => {
|
||||||
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
|
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
|
||||||
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
|
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
|
||||||
@@ -263,7 +272,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
|||||||
room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
||||||
reactionTimeouts.forEach((t) => clearTimeout(t));
|
reactionTimeouts.forEach((t) => clearTimeout(t));
|
||||||
// If we're clearing timeouts, we also clear all reactions.
|
// If we're clearing timeouts, we also clear all reactions.
|
||||||
reactionsSubject.current.next({});
|
innerReactionsSubject.next({});
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
room,
|
room,
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
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 { useObservableEagerState } from "observable-hooks";
|
||||||
import { map } from "rxjs";
|
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 {
|
interface ReactionsSenderContextType {
|
||||||
supportsReactions: boolean;
|
supportsReactions: boolean;
|
||||||
toggleRaisedHand: () => Promise<void>;
|
toggleRaisedHand: () => Promise<void>;
|
||||||
@@ -76,7 +76,7 @@ export function CallEventAudioRenderer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handRaisedSub = vm.newHandRaised.subscribe(() => {
|
const handRaisedSub = vm.newHandRaised.subscribe(() => {
|
||||||
audioEngineRef.current?.playSound("raiseHand");
|
void audioEngineRef.current?.playSound("raiseHand");
|
||||||
});
|
});
|
||||||
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { JoinRule, type RoomState } from "matrix-js-sdk/src/matrix";
|
|||||||
import { Router } from "react-router-dom";
|
import { Router } from "react-router-dom";
|
||||||
import { createBrowserHistory } from "history";
|
import { createBrowserHistory } from "history";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||||
|
|
||||||
import { type MuteStates } from "./MuteStates";
|
import { type MuteStates } from "./MuteStates";
|
||||||
import { prefetchSounds } from "../soundUtils";
|
import { prefetchSounds } from "../soundUtils";
|
||||||
@@ -29,7 +30,6 @@ import { GroupCallView } from "./GroupCallView";
|
|||||||
import { leaveRTCSession } from "../rtcSessionHelpers";
|
import { leaveRTCSession } from "../rtcSessionHelpers";
|
||||||
import { type WidgetHelpers } from "../widget";
|
import { type WidgetHelpers } from "../widget";
|
||||||
import { LazyEventEmitter } from "../LazyEventEmitter";
|
import { LazyEventEmitter } from "../LazyEventEmitter";
|
||||||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
|
||||||
|
|
||||||
vitest.mock("../soundUtils");
|
vitest.mock("../soundUtils");
|
||||||
vitest.mock("../useAudioContext");
|
vitest.mock("../useAudioContext");
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ import { GridTileViewModel, type TileViewModel } from "../state/TileViewModel";
|
|||||||
import {
|
import {
|
||||||
ReactionsSenderProvider,
|
ReactionsSenderProvider,
|
||||||
useReactionsSender,
|
useReactionsSender,
|
||||||
} from "../useReactionsSender";
|
} from "../reactions/useReactionsSender";
|
||||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||||
import { useSwitchCamera } from "./useSwitchCamera";
|
import { useSwitchCamera } from "./useSwitchCamera";
|
||||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
import { useAudioContext } from "../useAudioContext";
|
import { useAudioContext } from "../useAudioContext";
|
||||||
import { GenericReaction, ReactionSet } from "../reactions";
|
import { GenericReaction, ReactionSet } from "../reactions";
|
||||||
import { prefetchSounds } from "../soundUtils";
|
import { prefetchSounds } from "../soundUtils";
|
||||||
import { CallViewModel } from "../state/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel";
|
||||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||||
import {
|
import {
|
||||||
alice,
|
alice,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { GenericReaction, ReactionSet } from "../reactions";
|
|||||||
import { useAudioContext } from "../useAudioContext";
|
import { useAudioContext } from "../useAudioContext";
|
||||||
import { prefetchSounds } from "../soundUtils";
|
import { prefetchSounds } from "../soundUtils";
|
||||||
import { useLatest } from "../useLatest";
|
import { useLatest } from "../useLatest";
|
||||||
import { CallViewModel } from "../state/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel";
|
||||||
|
|
||||||
const soundMap = Object.fromEntries([
|
const soundMap = Object.fromEntries([
|
||||||
...ReactionSet.filter((v) => v.sound !== undefined).map((v) => [
|
...ReactionSet.filter((v) => v.sound !== undefined).map((v) => [
|
||||||
@@ -51,10 +51,10 @@ export function ReactionsAudioRenderer({
|
|||||||
const sub = vm.audibleReactions.subscribe((newReactions) => {
|
const sub = vm.audibleReactions.subscribe((newReactions) => {
|
||||||
for (const reactionName of newReactions) {
|
for (const reactionName of newReactions) {
|
||||||
if (soundMap[reactionName]) {
|
if (soundMap[reactionName]) {
|
||||||
audioEngineRef.current?.playSound(reactionName);
|
void audioEngineRef.current?.playSound(reactionName);
|
||||||
} else {
|
} else {
|
||||||
// Fallback sounds.
|
// 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", () => {
|
test("shows a reaction when sent", () => {
|
||||||
showReactions.setValue(true);
|
showReactions.setValue(true);
|
||||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||||
|
local,
|
||||||
|
alice,
|
||||||
|
]);
|
||||||
const { getByRole } = render(<ReactionsOverlay vm={vm} />);
|
const { getByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||||
const reaction = ReactionSet[0];
|
const reaction = ReactionSet[0];
|
||||||
act(() => {
|
act(() => {
|
||||||
vm.updateReactions({
|
reactionsSubject.next({
|
||||||
reactions: { [aliceRtcMember.deviceId]: reaction },
|
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||||
raisedHands: {},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const span = getByRole("presentation");
|
const span = getByRole("presentation");
|
||||||
@@ -51,15 +53,15 @@ test("shows a reaction when sent", () => {
|
|||||||
test("shows two of the same reaction when sent", () => {
|
test("shows two of the same reaction when sent", () => {
|
||||||
showReactions.setValue(true);
|
showReactions.setValue(true);
|
||||||
const reaction = ReactionSet[0];
|
const reaction = ReactionSet[0];
|
||||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||||
|
local,
|
||||||
|
alice,
|
||||||
|
]);
|
||||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||||
act(() => {
|
act(() => {
|
||||||
vm.updateReactions({
|
reactionsSubject.next({
|
||||||
reactions: {
|
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||||
[aliceRtcMember.deviceId]: reaction,
|
[bobRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||||
[bobRtcMember.deviceId]: reaction,
|
|
||||||
},
|
|
||||||
raisedHands: {},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(getAllByRole("presentation")).toHaveLength(2);
|
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", () => {
|
test("shows two different reactions when sent", () => {
|
||||||
showReactions.setValue(true);
|
showReactions.setValue(true);
|
||||||
const [reactionA, reactionB] = ReactionSet;
|
const [reactionA, reactionB] = ReactionSet;
|
||||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||||
|
local,
|
||||||
|
alice,
|
||||||
|
]);
|
||||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||||
act(() => {
|
act(() => {
|
||||||
vm.updateReactions({
|
reactionsSubject.next({
|
||||||
reactions: {
|
[aliceRtcMember.deviceId]: { reactionOption: reactionA, ttl: 0 },
|
||||||
[aliceRtcMember.deviceId]: reactionA,
|
[bobRtcMember.deviceId]: { reactionOption: reactionB, ttl: 0 },
|
||||||
[bobRtcMember.deviceId]: reactionB,
|
|
||||||
},
|
|
||||||
raisedHands: {},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
|
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
|
||||||
@@ -87,12 +89,14 @@ test("shows two different reactions when sent", () => {
|
|||||||
test("hides reactions when reaction animations are disabled", () => {
|
test("hides reactions when reaction animations are disabled", () => {
|
||||||
showReactions.setValue(false);
|
showReactions.setValue(false);
|
||||||
const reaction = ReactionSet[0];
|
const reaction = ReactionSet[0];
|
||||||
const { vm } = getBasicCallViewModelEnvironment([local, alice]);
|
const { vm, reactionsSubject } = getBasicCallViewModelEnvironment([
|
||||||
|
local,
|
||||||
|
alice,
|
||||||
|
]);
|
||||||
const { container } = render(<ReactionsOverlay vm={vm} />);
|
const { container } = render(<ReactionsOverlay vm={vm} />);
|
||||||
act(() => {
|
act(() => {
|
||||||
vm.updateReactions({
|
reactionsSubject.next({
|
||||||
reactions: { [aliceRtcMember.deviceId]: reaction },
|
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
||||||
raisedHands: {},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(container.getElementsByTagName("span")).toHaveLength(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 { type ReactNode } from "react";
|
||||||
|
import { useObservableState } from "observable-hooks";
|
||||||
|
|
||||||
import styles from "./ReactionsOverlay.module.css";
|
import styles from "./ReactionsOverlay.module.css";
|
||||||
import { CallViewModel } from "../state/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel";
|
||||||
import { useObservableState } from "observable-hooks";
|
|
||||||
|
|
||||||
export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode {
|
export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode {
|
||||||
const reactionsIcons = useObservableState(vm.visibleReactions);
|
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.
|
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 {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
type Observable,
|
type Observable,
|
||||||
of,
|
of,
|
||||||
switchMap,
|
switchMap,
|
||||||
tap,
|
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
type ECConnectionState,
|
type ECConnectionState,
|
||||||
} from "../livekit/useECConnectionState";
|
} from "../livekit/useECConnectionState";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
|
import { RaisedHandInfo } from "../reactions";
|
||||||
|
|
||||||
vi.mock("@livekit/components-core");
|
vi.mock("@livekit/components-core");
|
||||||
|
|
||||||
@@ -190,7 +191,10 @@ function withCallViewModel(
|
|||||||
rtcMembers: Observable<Partial<CallMembership>[]>,
|
rtcMembers: Observable<Partial<CallMembership>[]>,
|
||||||
connectionState: Observable<ECConnectionState>,
|
connectionState: Observable<ECConnectionState>,
|
||||||
speaking: Map<Participant, Observable<boolean>>,
|
speaking: Map<Participant, Observable<boolean>>,
|
||||||
continuation: (vm: CallViewModel) => void,
|
continuation: (
|
||||||
|
vm: CallViewModel,
|
||||||
|
subjects: { raisedHands: BehaviorSubject<Record<string, RaisedHandInfo>> },
|
||||||
|
) => void,
|
||||||
): void {
|
): void {
|
||||||
const room = mockMatrixRoom({
|
const room = mockMatrixRoom({
|
||||||
client: {
|
client: {
|
||||||
@@ -235,6 +239,8 @@ function withCallViewModel(
|
|||||||
{ remoteParticipants },
|
{ remoteParticipants },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const raisedHands = new BehaviorSubject<Record<string, RaisedHandInfo>>({});
|
||||||
|
|
||||||
const vm = new CallViewModel(
|
const vm = new CallViewModel(
|
||||||
rtcSession as unknown as MatrixRTCSession,
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
liveKitRoom,
|
liveKitRoom,
|
||||||
@@ -242,6 +248,8 @@ function withCallViewModel(
|
|||||||
kind: E2eeType.PER_PARTICIPANT,
|
kind: E2eeType.PER_PARTICIPANT,
|
||||||
},
|
},
|
||||||
connectionState,
|
connectionState,
|
||||||
|
raisedHands,
|
||||||
|
new BehaviorSubject({}),
|
||||||
);
|
);
|
||||||
|
|
||||||
onTestFinished(() => {
|
onTestFinished(() => {
|
||||||
@@ -252,7 +260,7 @@ function withCallViewModel(
|
|||||||
roomEventSelectorSpy!.mockRestore();
|
roomEventSelectorSpy!.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
continuation(vm);
|
continuation(vm, { raisedHands });
|
||||||
}
|
}
|
||||||
|
|
||||||
test("participants are retained during a focus switch", () => {
|
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", () => {
|
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
||||||
withTestScheduler(({ expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
const date = new Date();
|
// There should always be one tile for each MatrixRTCSession
|
||||||
|
const expectedLayoutMarbles = "ab";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
of([]),
|
of([aliceParticipant, bobParticipant]),
|
||||||
of([]),
|
of([aliceRtcMember, bobRtcMember]),
|
||||||
of(ConnectionState.Connected),
|
of(ConnectionState.Connected),
|
||||||
new Map(),
|
new Map(),
|
||||||
(vm) => {
|
(vm, { raisedHands }) => {
|
||||||
vm.addHandRaised(alice.userId, date);
|
schedule("ab", {
|
||||||
expectObservable(vm.handsRaised).toBe("a", {
|
a: () => {
|
||||||
a: {
|
// We imagine that only three tiles (the first three) will be visible
|
||||||
[alice.userId]: date,
|
// 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", {
|
expectObservable(summarizeLayout(vm.layout)).toBe(
|
||||||
a: {
|
expectedLayoutMarbles,
|
||||||
value: 1,
|
{
|
||||||
playSounds: true,
|
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,
|
switchMap,
|
||||||
switchScan,
|
switchScan,
|
||||||
take,
|
take,
|
||||||
tap,
|
|
||||||
timer,
|
timer,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
@@ -88,7 +87,11 @@ import { oneOnOneLayout } from "./OneOnOneLayout";
|
|||||||
import { pipLayout } from "./PipLayout";
|
import { pipLayout } from "./PipLayout";
|
||||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import { observeSpeaker } from "./observeSpeaker";
|
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
|
// How long we wait after a focus switch before showing the real participant
|
||||||
// list again
|
// list again
|
||||||
@@ -251,8 +254,8 @@ class UserMedia {
|
|||||||
participant: LocalParticipant | RemoteParticipant | undefined,
|
participant: LocalParticipant | RemoteParticipant | undefined,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
handRaised: Observable<Date | undefined>,
|
handRaised: Observable<Date | null>,
|
||||||
reactions: Observable<ReactionOption | undefined>,
|
reactions: Observable<ReactionOption | null>,
|
||||||
) {
|
) {
|
||||||
this.participant = new BehaviorSubject(participant);
|
this.participant = new BehaviorSubject(participant);
|
||||||
|
|
||||||
@@ -528,7 +531,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.encryptionSystem,
|
this.encryptionSystem,
|
||||||
this.livekitRoom,
|
this.livekitRoom,
|
||||||
this.handsRaised.pipe(
|
this.handsRaised.pipe(
|
||||||
map((v) => v[matrixIdentifier].time ?? undefined),
|
map((v) => v[matrixIdentifier]?.time ?? null),
|
||||||
),
|
),
|
||||||
this.reactions.pipe(
|
this.reactions.pipe(
|
||||||
map((v) => v[matrixIdentifier] ?? undefined),
|
map((v) => v[matrixIdentifier] ?? undefined),
|
||||||
@@ -649,7 +652,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
m.vm.handRaised,
|
m.vm.handRaised,
|
||||||
],
|
],
|
||||||
(speaker, presenter, video, alwaysShow, handRaised) => {
|
(speaker, presenter, video, alwaysShow, handRaised) => {
|
||||||
console.log(m.vm.id, handRaised);
|
|
||||||
let bin: SortingBin;
|
let bin: SortingBin;
|
||||||
if (m.vm.local)
|
if (m.vm.local)
|
||||||
bin = alwaysShow
|
bin = alwaysShow
|
||||||
@@ -672,12 +674,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm),
|
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[]> =
|
private readonly spotlight: Observable<MediaViewModel[]> =
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import { alwaysShowSelf } from "../settings/settings";
|
|||||||
import { accumulate } from "../utils/observable";
|
import { accumulate } from "../utils/observable";
|
||||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { ReactionOption } from "../reactions";
|
import { type ReactionOption } from "../reactions";
|
||||||
|
|
||||||
// TODO: Move this naming logic into the view model
|
// TODO: Move this naming logic into the view model
|
||||||
export function useDisplayName(vm: MediaViewModel): string {
|
export function useDisplayName(vm: MediaViewModel): string {
|
||||||
@@ -372,8 +372,8 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
|||||||
participant: Observable<LocalParticipant | RemoteParticipant | undefined>,
|
participant: Observable<LocalParticipant | RemoteParticipant | undefined>,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
public readonly handRaised: Observable<Date | undefined>,
|
public readonly handRaised: Observable<Date | null>,
|
||||||
public readonly reactions: Observable<ReactionOption | undefined>,
|
public readonly reactions: Observable<ReactionOption | null>,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
@@ -440,8 +440,8 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
participant: Observable<LocalParticipant | undefined>,
|
participant: Observable<LocalParticipant | undefined>,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
handRaised: Observable<Date | undefined>,
|
handRaised: Observable<Date | null>,
|
||||||
reactions: Observable<ReactionOption | undefined>,
|
reactions: Observable<ReactionOption | null>,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
@@ -511,8 +511,8 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
participant: Observable<RemoteParticipant | undefined>,
|
participant: Observable<RemoteParticipant | undefined>,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
handRaised: Observable<Date | undefined>,
|
handRaised: Observable<Date | null>,
|
||||||
reactions: Observable<ReactionOption | undefined>,
|
reactions: Observable<ReactionOption | null>,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSess
|
|||||||
import { GridTile } from "./GridTile";
|
import { GridTile } from "./GridTile";
|
||||||
import { mockRtcMembership, withRemoteMedia } from "../utils/test";
|
import { mockRtcMembership, withRemoteMedia } from "../utils/test";
|
||||||
import { GridTileViewModel } from "../state/TileViewModel";
|
import { GridTileViewModel } from "../state/TileViewModel";
|
||||||
import { ReactionsProvider } from "../useReactionsSender";
|
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||||
|
import { CallViewModel } from "../state/CallViewModel";
|
||||||
|
|
||||||
global.IntersectionObserver = class MockIntersectionObserver {
|
global.IntersectionObserver = class MockIntersectionObserver {
|
||||||
public observe(): void {}
|
public observe(): void {}
|
||||||
@@ -51,8 +52,12 @@ test("GridTile is accessible", async () => {
|
|||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
} as unknown as MatrixRTCSession;
|
} as unknown as MatrixRTCSession;
|
||||||
|
const cVm = {
|
||||||
|
reactions: of({}),
|
||||||
|
handsRaised: of({}),
|
||||||
|
} as Partial<CallViewModel> as CallViewModel;
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ReactionsProvider rtcSession={fakeRtcSession}>
|
<ReactionsSenderProvider vm={cVm} rtcSession={fakeRtcSession}>
|
||||||
<GridTile
|
<GridTile
|
||||||
vm={new GridTileViewModel(of(vm))}
|
vm={new GridTileViewModel(of(vm))}
|
||||||
onOpenProfile={() => {}}
|
onOpenProfile={() => {}}
|
||||||
@@ -60,7 +65,7 @@ test("GridTile is accessible", async () => {
|
|||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
showSpeakingIndicators
|
showSpeakingIndicators
|
||||||
/>
|
/>
|
||||||
</ReactionsProvider>,
|
</ReactionsSenderProvider>,
|
||||||
);
|
);
|
||||||
expect(await axe(container)).toHaveNoViolations();
|
expect(await axe(container)).toHaveNoViolations();
|
||||||
// Name should be visible
|
// Name should be visible
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import { MediaView } from "./MediaView";
|
|||||||
import { useLatest } from "../useLatest";
|
import { useLatest } from "../useLatest";
|
||||||
import { type GridTileViewModel } from "../state/TileViewModel";
|
import { type GridTileViewModel } from "../state/TileViewModel";
|
||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
import { useReactionsSender } from "../useReactionsSender";
|
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||||
|
|
||||||
interface TileProps {
|
interface TileProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -170,8 +170,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
{menu}
|
{menu}
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
raisedHandTime={handRaised}
|
raisedHandTime={handRaised ?? undefined}
|
||||||
currentReaction={reaction}
|
currentReaction={reaction ?? undefined}
|
||||||
raisedHandOnClick={raisedHandOnClick}
|
raisedHandOnClick={raisedHandOnClick}
|
||||||
localParticipant={vm.local}
|
localParticipant={vm.local}
|
||||||
{...props}
|
{...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 {
|
import {
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
mockMatrixRoomMember,
|
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 { ConnectionState } from "livekit-client";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
import { type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { CallMembership, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
import { type CallMembership, type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
import { vitest } from "vitest";
|
import { vitest } from "vitest";
|
||||||
|
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||||
|
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { CallViewModel } from "../state/CallViewModel";
|
import { CallViewModel } from "../state/CallViewModel";
|
||||||
import { mockLivekitRoom, mockMatrixRoom, MockRTCSession } from "./test";
|
import { mockLivekitRoom, mockMatrixRoom, MockRTCSession } from "./test";
|
||||||
@@ -13,8 +22,7 @@ import {
|
|||||||
localParticipant,
|
localParticipant,
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
} from "./test-fixtures";
|
} from "./test-fixtures";
|
||||||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||||
import { RaisedHandInfo, ReactionInfo } from "../reactions";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a basic CallViewModel to test components that make use of it.
|
* Construct a basic CallViewModel to test components that make use of it.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
type RemoteTrackPublication,
|
type RemoteTrackPublication,
|
||||||
type Room as LivekitRoom,
|
type Room as LivekitRoom,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LocalUserMediaViewModel,
|
LocalUserMediaViewModel,
|
||||||
@@ -39,7 +40,6 @@ import {
|
|||||||
type ResolvedConfigOptions,
|
type ResolvedConfigOptions,
|
||||||
} from "../config/ConfigOptions";
|
} from "../config/ConfigOptions";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
|
|
||||||
export function withFakeTimers(continuation: () => void): void {
|
export function withFakeTimers(continuation: () => void): void {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
type Room,
|
type Room,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type ECallReactionEventContent,
|
type ECallReactionEventContent,
|
||||||
ElementCallReactionEventType,
|
ElementCallReactionEventType,
|
||||||
|
|||||||
Reference in New Issue
Block a user