finnish notation

This commit is contained in:
Half-Shot
2024-12-17 10:07:42 +00:00
parent 475ff920b7
commit 4164e0a61f
17 changed files with 151 additions and 129 deletions
+7 -13
View File
@@ -53,11 +53,8 @@ test("Can open menu", async () => {
test("Can raise hand", async () => { test("Can raise hand", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const { const { vm, rtcSession, handRaisedSubject$ } =
vm, getBasicCallViewModelEnvironment([local, alice]);
rtcSession,
handRaisedSubject$: handRaisedSubject,
} = getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent vm={vm} rtcSession={rtcSession} />, <TestComponent vm={vm} rtcSession={rtcSession} />,
); );
@@ -76,7 +73,7 @@ test("Can raise hand", async () => {
); );
act(() => { act(() => {
// Mock receiving a reaction. // Mock receiving a reaction.
handRaisedSubject.next({ handRaisedSubject$.next({
[localIdent]: { [localIdent]: {
time: new Date(), time: new Date(),
reactionEventId: "", reactionEventId: "",
@@ -90,18 +87,15 @@ test("Can raise hand", async () => {
test("Can lower hand", async () => { test("Can lower hand", async () => {
const reactionEventId = "$my-reaction-event:example.org"; const reactionEventId = "$my-reaction-event:example.org";
const user = userEvent.setup(); const user = userEvent.setup();
const { const { vm, rtcSession, handRaisedSubject$ } =
vm, getBasicCallViewModelEnvironment([local, alice]);
rtcSession,
handRaisedSubject$: handRaisedSubject,
} = getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent vm={vm} rtcSession={rtcSession} />, <TestComponent vm={vm} rtcSession={rtcSession} />,
); );
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"));
act(() => { act(() => {
handRaisedSubject.next({ handRaisedSubject$.next({
[localIdent]: { [localIdent]: {
time: new Date(), time: new Date(),
reactionEventId, reactionEventId,
@@ -117,7 +111,7 @@ test("Can lower hand", async () => {
); );
act(() => { act(() => {
// Mock receiving a redacted reaction. // Mock receiving a redacted reaction.
handRaisedSubject.next({}); handRaisedSubject$.next({});
}); });
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
+2 -2
View File
@@ -181,10 +181,10 @@ export function ReactionToggleButton({
const [errorText, setErrorText] = useState<string>(); const [errorText, setErrorText] = useState<string>();
const isHandRaised = useObservableState( const isHandRaised = useObservableState(
vm.handsRaised.pipe(map((v) => !!v[identifier])), vm.handsRaised$.pipe(map((v) => !!v[identifier])),
); );
const canReact = useObservableState( const canReact = useObservableState(
vm.reactions.pipe(map((v) => !v[identifier])), vm.reactions$.pipe(map((v) => !v[identifier])),
); );
useEffect(() => { useEffect(() => {
+20 -20
View File
@@ -36,32 +36,32 @@ const REACTION_ACTIVE_TIME_MS = 3000;
* @param rtcSession * @param rtcSession
*/ */
export default function useReactionsReader(rtcSession: MatrixRTCSession): { export default function useReactionsReader(rtcSession: MatrixRTCSession): {
raisedHands: Observable<Record<string, RaisedHandInfo>>; raisedHands$: Observable<Record<string, RaisedHandInfo>>;
reactions: Observable<Record<string, ReactionInfo>>; reactions$: Observable<Record<string, ReactionInfo>>;
} { } {
const raisedHandsSubject = useRef( const raisedHandsSubject$ = useRef(
new BehaviorSubject<Record<string, RaisedHandInfo>>({}), new BehaviorSubject<Record<string, RaisedHandInfo>>({}),
); );
const reactionsSubject = useRef( const reactionsSubject$ = useRef(
new BehaviorSubject<Record<string, ReactionInfo>>({}), new BehaviorSubject<Record<string, ReactionInfo>>({}),
); );
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const latestMemberships = useLatest(memberships); const latestMemberships = useLatest(memberships);
const latestRaisedHands = useLatest(raisedHandsSubject.current); const latestRaisedHands = useLatest(raisedHandsSubject$.current);
const room = rtcSession.room; const room = rtcSession.room;
const addRaisedHand = useCallback((userId: string, info: RaisedHandInfo) => { const addRaisedHand = useCallback((userId: string, info: RaisedHandInfo) => {
raisedHandsSubject.current.next({ raisedHandsSubject$.current.next({
...raisedHandsSubject.current.value, ...raisedHandsSubject$.current.value,
[userId]: info, [userId]: info,
}); });
}, []); }, []);
const removeRaisedHand = useCallback((userId: string) => { const removeRaisedHand = useCallback((userId: string) => {
raisedHandsSubject.current.next( raisedHandsSubject$.current.next(
Object.fromEntries( Object.fromEntries(
Object.entries(raisedHandsSubject.current.value).filter( Object.entries(raisedHandsSubject$.current.value).filter(
([uId]) => uId !== userId, ([uId]) => uId !== userId,
), ),
), ),
@@ -90,7 +90,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
}; };
// Remove any raised hands for users no longer joined to the call. // Remove any raised hands for users no longer joined to the call.
for (const identifier of Object.keys(raisedHandsSubject).filter( for (const identifier of Object.keys(raisedHandsSubject$).filter(
(rhId) => !memberships.find((u) => u.sender == rhId), (rhId) => !memberships.find((u) => u.sender == rhId),
)) { )) {
removeRaisedHand(identifier); removeRaisedHand(identifier);
@@ -104,8 +104,8 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
} }
const identifier = `${m.sender}:${m.deviceId}`; const identifier = `${m.sender}:${m.deviceId}`;
if ( if (
raisedHandsSubject.current.value[identifier] && raisedHandsSubject$.current.value[identifier] &&
raisedHandsSubject.current.value[identifier].membershipEventId !== raisedHandsSubject$.current.value[identifier].membershipEventId !==
m.eventId m.eventId
) { ) {
// Membership event for sender has changed since the hand // Membership event for sender has changed since the hand
@@ -193,16 +193,16 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
...ReactionSet.find((r) => r.name === content.name), ...ReactionSet.find((r) => r.name === content.name),
}; };
const currentReactions = reactionsSubject.current.value; const currentReactions = reactionsSubject$.current.value;
if (currentReactions[identifier]) { if (currentReactions[identifier]) {
// We've still got a reaction from this user, ignore it to prevent spamming // We've still got a reaction from this user, ignore it to prevent spamming
return; return;
} }
const timeout = globalThis.setTimeout(() => { const timeout = globalThis.setTimeout(() => {
// Clear the reaction after some time. // Clear the reaction after some time.
reactionsSubject.current.next( reactionsSubject$.current.next(
Object.fromEntries( Object.fromEntries(
Object.entries(reactionsSubject.current.value).filter( Object.entries(reactionsSubject$.current.value).filter(
([id]) => id !== identifier, ([id]) => id !== identifier,
), ),
), ),
@@ -210,7 +210,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
reactionTimeouts.delete(timeout); reactionTimeouts.delete(timeout);
}, REACTION_ACTIVE_TIME_MS); }, REACTION_ACTIVE_TIME_MS);
reactionTimeouts.add(timeout); reactionTimeouts.add(timeout);
reactionsSubject.current.next({ reactionsSubject$.current.next({
...currentReactions, ...currentReactions,
[identifier]: { [identifier]: {
reactionOption: reaction, reactionOption: reaction,
@@ -264,7 +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; 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);
@@ -272,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.
innerReactionsSubject.next({}); innerReactionsSubject$.next({});
}; };
}, [ }, [
room, room,
@@ -283,7 +283,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
]); ]);
return { return {
reactions: reactionsSubject.current.asObservable(), reactions$: reactionsSubject$.current.asObservable(),
raisedHands: raisedHandsSubject.current.asObservable(), raisedHands$: raisedHandsSubject$.current.asObservable(),
}; };
} }
+2 -2
View File
@@ -76,7 +76,7 @@ export const ReactionsSenderProvider = ({
}, [memberships, myUserId]); }, [memberships, myUserId]);
const myReaction = useObservableEagerState( const myReaction = useObservableEagerState(
vm.reactions.pipe( vm.reactions$.pipe(
map((v) => map((v) =>
myMembershipIdentifier !== undefined myMembershipIdentifier !== undefined
? v[myMembershipIdentifier] ? v[myMembershipIdentifier]
@@ -86,7 +86,7 @@ export const ReactionsSenderProvider = ({
); );
const myRaisedHand = useObservableEagerState( const myRaisedHand = useObservableEagerState(
vm.handsRaised.pipe( vm.handsRaised$.pipe(
map((v) => map((v) =>
myMembershipIdentifier !== undefined myMembershipIdentifier !== undefined
? v[myMembershipIdentifier] ? v[myMembershipIdentifier]
+31 -19
View File
@@ -66,36 +66,42 @@ beforeEach(() => {
* a noise every time. * a noise every time.
*/ */
test("plays one sound when entering a call", () => { test("plays one sound when entering a call", () => {
const { vm, remoteRtcMemberships$: remoteRtcMemberships } = const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
// Joining a call usually means remote participants are added later. // Joining a call usually means remote participants are added later.
act(() => { act(() => {
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]); remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
}); });
expect(playSound).toHaveBeenCalledOnce(); expect(playSound).toHaveBeenCalledOnce();
}); });
test("plays a sound when a user joins", () => { test("plays a sound when a user joins", () => {
const { vm, remoteRtcMemberships$: remoteRtcMemberships } = const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
act(() => { act(() => {
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]); remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
}); });
// Play a sound when joining a call. // Play a sound when joining a call.
expect(playSound).toBeCalledWith("join"); expect(playSound).toBeCalledWith("join");
}); });
test("plays a sound when a user leaves", () => { test("plays a sound when a user leaves", () => {
const { vm, remoteRtcMemberships$: remoteRtcMemberships } = const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
act(() => { act(() => {
remoteRtcMemberships.next([]); remoteRtcMemberships$.next([]);
}); });
expect(playSound).toBeCalledWith("left"); expect(playSound).toBeCalledWith("left");
}); });
@@ -108,13 +114,15 @@ test("plays no sound when the participant list is more than the maximum size", (
); );
} }
const { vm, remoteRtcMemberships$: remoteRtcMemberships } = const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment(
getBasicCallViewModelEnvironment([local, alice], mockRtcMemberships); [local, alice],
mockRtcMemberships,
);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
expect(playSound).not.toBeCalled(); expect(playSound).not.toBeCalled();
act(() => { act(() => {
remoteRtcMemberships.next( remoteRtcMemberships$.next(
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1), mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1),
); );
}); });
@@ -122,12 +130,14 @@ test("plays no sound when the participant list is more than the maximum size", (
}); });
test("plays one sound when a hand is raised", () => { test("plays one sound when a hand is raised", () => {
const { vm, handRaisedSubject$: handRaisedSubject } = const { vm, handRaisedSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
act(() => { act(() => {
handRaisedSubject.next({ handRaisedSubject$.next({
[bobRtcMember.callId]: { [bobRtcMember.callId]: {
time: new Date(), time: new Date(),
membershipEventId: "", membershipEventId: "",
@@ -139,12 +149,14 @@ test("plays one sound when a hand is raised", () => {
}); });
test("should not play a sound when a hand raise is retracted", () => { test("should not play a sound when a hand raise is retracted", () => {
const { vm, handRaisedSubject$: handRaisedSubject } = const { vm, handRaisedSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
act(() => { act(() => {
handRaisedSubject.next({ handRaisedSubject$.next({
["foo"]: { ["foo"]: {
time: new Date(), time: new Date(),
membershipEventId: "", membershipEventId: "",
@@ -159,7 +171,7 @@ test("should not play a sound when a hand raise is retracted", () => {
}); });
expect(playSound).toHaveBeenCalledTimes(2); expect(playSound).toHaveBeenCalledTimes(2);
act(() => { act(() => {
handRaisedSubject.next({ handRaisedSubject$.next({
["foo"]: { ["foo"]: {
time: new Date(), time: new Date(),
membershipEventId: "", membershipEventId: "",
+1 -1
View File
@@ -75,7 +75,7 @@ export function CallEventAudioRenderer({
void audioEngineRef.current?.playSound("left"); void audioEngineRef.current?.playSound("left");
}); });
const handRaisedSub = vm.newHandRaised.subscribe(() => { const handRaisedSub = vm.newHandRaised$.subscribe(() => {
void audioEngineRef.current?.playSound("raiseHand"); void audioEngineRef.current?.playSound("raiseHand");
}); });
+2 -2
View File
@@ -139,8 +139,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
livekitRoom, livekitRoom,
props.e2eeSystem, props.e2eeSystem,
connStateObservable$, connStateObservable$,
reader.current.raisedHands, reader.current.raisedHands$,
reader.current.reactions, reader.current.reactions$,
); );
setVm(vm); setVm(vm);
return (): void => vm.destroy(); return (): void => vm.destroy();
+15 -9
View File
@@ -80,8 +80,10 @@ test("preloads all audio elements", () => {
}); });
test("will play an audio sound when there is a reaction", () => { test("will play an audio sound when there is a reaction", () => {
const { vm, reactionsSubject$: reactionsSubject } = const { vm, reactionsSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
playReactionsSound.setValue(true); playReactionsSound.setValue(true);
render(<TestComponent vm={vm} />); render(<TestComponent vm={vm} />);
@@ -93,7 +95,7 @@ test("will play an audio sound when there is a reaction", () => {
); );
} }
act(() => { act(() => {
reactionsSubject.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 },
}); });
}); });
@@ -101,8 +103,10 @@ test("will play an audio sound when there is a reaction", () => {
}); });
test("will play the generic audio sound when there is soundless reaction", () => { test("will play the generic audio sound when there is soundless reaction", () => {
const { vm, reactionsSubject$: reactionsSubject } = const { vm, reactionsSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
playReactionsSound.setValue(true); playReactionsSound.setValue(true);
render(<TestComponent vm={vm} />); render(<TestComponent vm={vm} />);
@@ -114,7 +118,7 @@ test("will play the generic audio sound when there is soundless reaction", () =>
); );
} }
act(() => { act(() => {
reactionsSubject.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 },
}); });
}); });
@@ -122,8 +126,10 @@ test("will play the generic audio sound when there is soundless reaction", () =>
}); });
test("will play multiple audio sounds when there are multiple different reactions", () => { test("will play multiple audio sounds when there are multiple different reactions", () => {
const { vm, reactionsSubject$: reactionsSubject } = const { vm, reactionsSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
playReactionsSound.setValue(true); playReactionsSound.setValue(true);
render(<TestComponent vm={vm} />); render(<TestComponent vm={vm} />);
@@ -135,7 +141,7 @@ test("will play multiple audio sounds when there are multiple different reaction
); );
} }
act(() => { act(() => {
reactionsSubject.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 },
[bobRtcMember.deviceId]: { reactionOption: reaction2, ttl: 0 }, [bobRtcMember.deviceId]: { reactionOption: reaction2, ttl: 0 },
[localRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 }, [localRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 },
+1 -1
View File
@@ -48,7 +48,7 @@ export function ReactionsAudioRenderer({
}, [soundCache, shouldPlay]); }, [soundCache, shouldPlay]);
useEffect(() => { useEffect(() => {
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]) {
void audioEngineRef.current?.playSound(reactionName); void audioEngineRef.current?.playSound(reactionName);
+20 -12
View File
@@ -34,12 +34,14 @@ 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, reactionsSubject$: reactionsSubject } = const { vm, reactionsSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
const { getByRole } = render(<ReactionsOverlay vm={vm} />); const { getByRole } = render(<ReactionsOverlay vm={vm} />);
const reaction = ReactionSet[0]; const reaction = ReactionSet[0];
act(() => { act(() => {
reactionsSubject.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
}); });
}); });
@@ -51,11 +53,13 @@ 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, reactionsSubject$: reactionsSubject } = const { vm, reactionsSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />); const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
act(() => { act(() => {
reactionsSubject.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
[bobRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [bobRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
}); });
@@ -66,11 +70,13 @@ 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, reactionsSubject$: reactionsSubject } = const { vm, reactionsSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />); const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
act(() => { act(() => {
reactionsSubject.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reactionA, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reactionA, ttl: 0 },
[bobRtcMember.deviceId]: { reactionOption: reactionB, ttl: 0 }, [bobRtcMember.deviceId]: { reactionOption: reactionB, ttl: 0 },
}); });
@@ -83,11 +89,13 @@ 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, reactionsSubject$: reactionsSubject } = const { vm, reactionsSubject$ } = getBasicCallViewModelEnvironment([
getBasicCallViewModelEnvironment([local, alice]); local,
alice,
]);
const { container } = render(<ReactionsOverlay vm={vm} />); const { container } = render(<ReactionsOverlay vm={vm} />);
act(() => { act(() => {
reactionsSubject.next({ reactionsSubject$.next({
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 }, [aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
}); });
}); });
+1 -1
View File
@@ -12,7 +12,7 @@ import styles from "./ReactionsOverlay.module.css";
import { type CallViewModel } from "../state/CallViewModel"; import { type CallViewModel } from "../state/CallViewModel";
export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode { export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode {
const reactionsIcons = useObservableState(vm.visibleReactions); const reactionsIcons = useObservableState(vm.visibleReactions$);
return ( return (
<div className={styles.container}> <div className={styles.container}>
{reactionsIcons?.map(({ sender, emoji, startX }) => ( {reactionsIcons?.map(({ sender, emoji, startX }) => (
+6 -6
View File
@@ -194,7 +194,7 @@ function withCallViewModel(
speaking: Map<Participant, Observable<boolean>>, speaking: Map<Participant, Observable<boolean>>,
continuation: ( continuation: (
vm: CallViewModel, vm: CallViewModel,
subjects: { raisedHands: BehaviorSubject<Record<string, RaisedHandInfo>> }, subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> },
) => void, ) => void,
): void { ): void {
const room = mockMatrixRoom({ const room = mockMatrixRoom({
@@ -240,7 +240,7 @@ function withCallViewModel(
{ remoteParticipants$ }, { remoteParticipants$ },
); );
const raisedHands = new BehaviorSubject<Record<string, RaisedHandInfo>>({}); 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,
@@ -249,7 +249,7 @@ function withCallViewModel(
kind: E2eeType.PER_PARTICIPANT, kind: E2eeType.PER_PARTICIPANT,
}, },
connectionState$, connectionState$,
raisedHands, raisedHands$,
new BehaviorSubject({}), new BehaviorSubject({}),
); );
@@ -261,7 +261,7 @@ function withCallViewModel(
roomEventSelectorSpy!.mockRestore(); roomEventSelectorSpy!.mockRestore();
}); });
continuation(vm, { raisedHands }); continuation(vm, { raisedHands$: raisedHands$ });
} }
test("participants are retained during a focus switch", () => { test("participants are retained during a focus switch", () => {
@@ -802,7 +802,7 @@ it("should rank raised hands above video feeds and below speakers and presenters
of([aliceRtcMember, bobRtcMember]), of([aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
(vm, { raisedHands }) => { (vm, { raisedHands$ }) => {
schedule("ab", { schedule("ab", {
a: () => { a: () => {
// We imagine that only three tiles (the first three) will be visible // We imagine that only three tiles (the first three) will be visible
@@ -814,7 +814,7 @@ it("should rank raised hands above video feeds and below speakers and presenters
}); });
}, },
b: () => { b: () => {
raisedHands.next({ raisedHands$.next({
[`${bobRtcMember.sender}:${bobRtcMember.deviceId}`]: { [`${bobRtcMember.sender}:${bobRtcMember.deviceId}`]: {
time: new Date(), time: new Date(),
reactionEventId: "", reactionEventId: "",
+20 -18
View File
@@ -258,8 +258,8 @@ class UserMedia {
participant: LocalParticipant | RemoteParticipant | undefined, participant: LocalParticipant | RemoteParticipant | undefined,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
livekitRoom: LivekitRoom, livekitRoom: LivekitRoom,
handRaised: Observable<Date | null>, handRaised$: Observable<Date | null>,
reaction: Observable<ReactionOption | null>, reaction$: Observable<ReactionOption | null>,
) { ) {
this.participant$ = new BehaviorSubject(participant); this.participant$ = new BehaviorSubject(participant);
@@ -270,8 +270,8 @@ class UserMedia {
this.participant$.asObservable() as Observable<LocalParticipant>, this.participant$.asObservable() as Observable<LocalParticipant>,
encryptionSystem, encryptionSystem,
livekitRoom, livekitRoom,
handRaised, handRaised$,
reaction, reaction$,
); );
} else { } else {
this.vm = new RemoteUserMediaViewModel( this.vm = new RemoteUserMediaViewModel(
@@ -282,8 +282,8 @@ class UserMedia {
>, >,
encryptionSystem, encryptionSystem,
livekitRoom, livekitRoom,
handRaised, handRaised$,
reaction, reaction$,
); );
} }
@@ -544,10 +544,10 @@ export class CallViewModel extends ViewModel {
participant, participant,
this.encryptionSystem, this.encryptionSystem,
this.livekitRoom, this.livekitRoom,
this.handsRaised.pipe( this.handsRaised$.pipe(
map((v) => v[matrixIdentifier]?.time ?? null), map((v) => v[matrixIdentifier]?.time ?? null),
), ),
this.reactions.pipe( this.reactions$.pipe(
map((v) => v[matrixIdentifier] ?? undefined), map((v) => v[matrixIdentifier] ?? undefined),
), ),
), ),
@@ -711,7 +711,7 @@ export class CallViewModel extends ViewModel {
m.speaker$, m.speaker$,
m.presenter$, m.presenter$,
m.vm.videoEnabled$, m.vm.videoEnabled$,
m.vm.handRaised, m.vm.handRaised$,
m.vm instanceof LocalUserMediaViewModel m.vm instanceof LocalUserMediaViewModel
? m.vm.alwaysShow$ ? m.vm.alwaysShow$
: of(false), : of(false),
@@ -1203,7 +1203,7 @@ export class CallViewModel extends ViewModel {
this.scope.state(), this.scope.state(),
); );
public readonly reactions = this.reactionsSubject.pipe( public readonly reactions$ = this.reactionsSubject$.pipe(
map((v) => map((v) =>
Object.fromEntries( Object.fromEntries(
Object.entries(v).map(([a, { reactionOption }]) => [a, reactionOption]), Object.entries(v).map(([a, { reactionOption }]) => [a, reactionOption]),
@@ -1211,13 +1211,13 @@ export class CallViewModel extends ViewModel {
), ),
); );
public readonly handsRaised = this.handsRaisedSubject.pipe(); public readonly handsRaised$ = this.handsRaisedSubject$.pipe();
/** /**
* Emits an array of reactions that should be visible on the screen. * Emits an array of reactions that should be visible on the screen.
*/ */
public readonly visibleReactions = showReactions.value$ public readonly visibleReactions$ = showReactions.value$
.pipe(switchMap((show) => (show ? this.reactions : of({})))) .pipe(switchMap((show) => (show ? this.reactions$ : of({}))))
.pipe( .pipe(
scan< scan<
Record<string, ReactionOption>, Record<string, ReactionOption>,
@@ -1238,10 +1238,10 @@ export class CallViewModel extends ViewModel {
/** /**
* Emits an array of reactions that should be played. * Emits an array of reactions that should be played.
*/ */
public readonly audibleReactions = playReactionsSound.value$ public readonly audibleReactions$ = playReactionsSound.value$
.pipe( .pipe(
switchMap((show) => switchMap((show) =>
show ? this.reactions : of<Record<string, ReactionOption>>({}), show ? this.reactions$ : of<Record<string, ReactionOption>>({}),
), ),
) )
.pipe( .pipe(
@@ -1267,7 +1267,7 @@ export class CallViewModel extends ViewModel {
* Emits an event every time a new hand is raised in * Emits an event every time a new hand is raised in
* the call. * the call.
*/ */
public readonly newHandRaised = this.handsRaised.pipe( public readonly newHandRaised$ = this.handsRaised$.pipe(
map((v) => Object.keys(v).length), map((v) => Object.keys(v).length),
scan( scan(
(acc, newValue) => ({ (acc, newValue) => ({
@@ -1285,10 +1285,12 @@ export class CallViewModel extends ViewModel {
private readonly livekitRoom: LivekitRoom, private readonly livekitRoom: LivekitRoom,
private readonly encryptionSystem: EncryptionSystem, private readonly encryptionSystem: EncryptionSystem,
private readonly connectionState$: Observable<ECConnectionState>, private readonly connectionState$: Observable<ECConnectionState>,
private readonly handsRaisedSubject: Observable< private readonly handsRaisedSubject$: Observable<
Record<string, RaisedHandInfo> Record<string, RaisedHandInfo>
>, >,
private readonly reactionsSubject: Observable<Record<string, ReactionInfo>>, private readonly reactionsSubject$: Observable<
Record<string, ReactionInfo>
>,
) { ) {
super(); super();
} }
+10 -10
View File
@@ -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 | null>, public readonly handRaised$: Observable<Date | null>,
public readonly reaction: Observable<ReactionOption | null>, public readonly reaction$: 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 | null>, handRaised$: Observable<Date | null>,
reaction: Observable<ReactionOption | null>, reaction$: Observable<ReactionOption | null>,
) { ) {
super( super(
id, id,
@@ -449,8 +449,8 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
participant$, participant$,
encryptionSystem, encryptionSystem,
livekitRoom, livekitRoom,
handRaised, handRaised$,
reaction, reaction$,
); );
} }
} }
@@ -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 | null>, handRaised$: Observable<Date | null>,
reaction: Observable<ReactionOption | null>, reaction$: Observable<ReactionOption | null>,
) { ) {
super( super(
id, id,
@@ -520,8 +520,8 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
participant$, participant$,
encryptionSystem, encryptionSystem,
livekitRoom, livekitRoom,
handRaised, handRaised$,
reaction, reaction$,
); );
// Sync the local volume with LiveKit // Sync the local volume with LiveKit
+2 -2
View File
@@ -53,8 +53,8 @@ test("GridTile is accessible", async () => {
memberships: [], memberships: [],
} as unknown as MatrixRTCSession; } as unknown as MatrixRTCSession;
const cVm = { const cVm = {
reactions: of({}), reactions$: of({}),
handsRaised: of({}), handsRaised$: of({}),
} as Partial<CallViewModel> as CallViewModel; } as Partial<CallViewModel> as CallViewModel;
const { container } = render( const { container } = render(
<ReactionsSenderProvider vm={cVm} rtcSession={fakeRtcSession}> <ReactionsSenderProvider vm={cVm} rtcSession={fakeRtcSession}>
+2 -2
View File
@@ -97,8 +97,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
}, },
[vm], [vm],
); );
const handRaised = useObservableState(vm.handRaised); const handRaised = useObservableState(vm.handRaised$);
const reaction = useObservableState(vm.reaction); const reaction = useObservableState(vm.reaction$);
const AudioIcon = locallyMuted const AudioIcon = locallyMuted
? VolumeOffSolidIcon ? VolumeOffSolidIcon
+9 -9
View File
@@ -66,17 +66,17 @@ export function getBasicCallViewModelEnvironment(
roomId: matrixRoomId, roomId: matrixRoomId,
}); });
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>( const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
initialRemoteRtcMemberships, initialRemoteRtcMemberships,
); );
const handRaisedSubject = new BehaviorSubject({}); const handRaisedSubject$ = new BehaviorSubject({});
const reactionsSubject = new BehaviorSubject({}); const reactionsSubject$ = new BehaviorSubject({});
const rtcSession = new MockRTCSession( const rtcSession = new MockRTCSession(
matrixRoom, matrixRoom,
localRtcMember, localRtcMember,
).withMemberships(remoteRtcMemberships); ).withMemberships(remoteRtcMemberships$);
const vm = new CallViewModel( const vm = new CallViewModel(
rtcSession as unknown as MatrixRTCSession, rtcSession as unknown as MatrixRTCSession,
@@ -85,14 +85,14 @@ export function getBasicCallViewModelEnvironment(
kind: E2eeType.PER_PARTICIPANT, kind: E2eeType.PER_PARTICIPANT,
}, },
of(ConnectionState.Connected), of(ConnectionState.Connected),
handRaisedSubject, handRaisedSubject$,
reactionsSubject, reactionsSubject$,
); );
return { return {
vm, vm,
remoteRtcMemberships$: remoteRtcMemberships, remoteRtcMemberships$: remoteRtcMemberships$,
rtcSession, rtcSession,
handRaisedSubject$: handRaisedSubject, handRaisedSubject$: handRaisedSubject$,
reactionsSubject$: reactionsSubject, reactionsSubject$: reactionsSubject$,
}; };
} }