Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor reactions / hand raised to use rxjs and start ordering tiles based on hand raised. #2885

Merged
merged 39 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
865187d
Add support for using CallViewModel for reactions sounds.
Half-Shot Dec 9, 2024
fbfa8c3
Drop setting
Half-Shot Dec 9, 2024
0812217
Convert reaction sounds to call view model / rxjs
Half-Shot Dec 9, 2024
73ee088
Use call view model for hand raised reactions
Half-Shot Dec 9, 2024
b10863d
Support raising reactions for matrix rtc members.
Half-Shot Dec 9, 2024
de19565
Tie up last bits of useReactions
Half-Shot Dec 9, 2024
ed35049
linting
Half-Shot Dec 9, 2024
fe5095f
Update calleventaudiorenderer
Half-Shot Dec 10, 2024
8d82daa
Update reaction audio renderer
Half-Shot Dec 10, 2024
12a412c
more test bits
Half-Shot Dec 10, 2024
19e5c67
All the test bits and pieces
Half-Shot Dec 12, 2024
1e56443
Merge remote-tracking branch 'origin/livekit' into hs/reactions-viewc…
Half-Shot Dec 12, 2024
0122f85
More refactors
Half-Shot Dec 16, 2024
7017f61
Refactor reactions into a sender and receiver.
Half-Shot Dec 16, 2024
a6403d9
Fixup reaction toggle button
Half-Shot Dec 16, 2024
22ea31d
Adapt reactions test
Half-Shot Dec 16, 2024
3924429
Tests all pass.
Half-Shot Dec 16, 2024
5c0b4d4
lint
Half-Shot Dec 16, 2024
5c580be
fix a couple of bugs
Half-Shot Dec 16, 2024
ada7000
Merge remote-tracking branch 'origin/livekit' into hs/reactions-viewc…
Half-Shot Dec 16, 2024
cf858ce
remove unused helper file
Half-Shot Dec 16, 2024
049df91
Merge remote-tracking branch 'origin/livekit' into hs/reactions-viewc…
Half-Shot Dec 17, 2024
475ff92
lint
Half-Shot Dec 17, 2024
4164e0a
finnish notation
Half-Shot Dec 17, 2024
f604004
Add tests for useReactionsReader
Half-Shot Dec 17, 2024
f216554
Merge remote-tracking branch 'origin/livekit' into hs/reactions-viewc…
Half-Shot Dec 17, 2024
a3ed982
remove mistaken vitest file
Half-Shot Dec 17, 2024
8688907
fix
Half-Shot Dec 17, 2024
3a09434
filter
Half-Shot Dec 17, 2024
770cd9d
invert
Half-Shot Dec 17, 2024
7a896f6
fixup tests with fake timers
Half-Shot Dec 17, 2024
afc9cb5
Merge remote-tracking branch 'origin/livekit' into hs/reactions-viewc…
Half-Shot Dec 19, 2024
3aff86c
Port useReactionsReader hook to ReactionsReader class.
Half-Shot Dec 19, 2024
acef18b
lint
Half-Shot Dec 19, 2024
5dcde4d
exclude some files from coverage
Half-Shot Dec 19, 2024
7cbc710
Merge branch 'livekit' into hs/reactions-viewcallmodel
Half-Shot Dec 19, 2024
955c663
Add screen share sound effect.
Half-Shot Dec 19, 2024
20f2ca4
cancel sub on destroy
Half-Shot Dec 19, 2024
15c2dd1
tidy tidy
Half-Shot Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 93 additions & 75 deletions src/button/ReactionToggleButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,150 +5,168 @@ 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, render } from "@testing-library/react";
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 {
MockRoom,
MockRTCSession,
TestReactionsWrapper,
} from "../utils/testReactions";
import { ReactionToggleButton } from "./ReactionToggleButton";
import { ElementCallReactionEventType } from "../reactions";
import { type CallViewModel } from "../state/CallViewModel";
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
import { alice, local, localRtcMember } from "../utils/test-fixtures";
import { type MockRTCSession } from "../utils/test";
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";

const memberUserIdAlice = "@alice:example.org";
const memberEventAlice = "$membership-alice:example.org";

const membership: Record<string, string> = {
[memberEventAlice]: memberUserIdAlice,
};
const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;

function TestComponent({
rtcSession,
vm,
}: {
rtcSession: MockRTCSession;
vm: CallViewModel;
}): ReactNode {
return (
<TooltipProvider>
<TestReactionsWrapper rtcSession={rtcSession}>
<ReactionToggleButton userId={memberUserIdAlice} />
</TestReactionsWrapper>
<ReactionsSenderProvider
vm={vm}
rtcSession={rtcSession as unknown as MatrixRTCSession}
>
<ReactionToggleButton vm={vm} identifier={localIdent} />
</ReactionsSenderProvider>
</TooltipProvider>
);
}

test("Can open menu", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { vm, rtcSession } = getBasicCallViewModelEnvironment([alice]);
const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} />,
<TestComponent vm={vm} rtcSession={rtcSession} />,
);
await user.click(getByLabelText("common.reactions"));
expect(container).toMatchSnapshot();
});

test("Can raise hand", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { vm, rtcSession, handRaisedSubject$ } =
getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} />,
<TestComponent vm={vm} rtcSession={rtcSession} />,
);
await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.raise_hand"));
expect(room.testSentEvents).toEqual([
[
undefined,
"m.reaction",
{
"m.relates_to": {
event_id: memberEventAlice,
key: "🖐️",
rel_type: "m.annotation",
},
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
rtcSession.room.roomId,
"m.reaction",
{
"m.relates_to": {
event_id: localRtcMember.eventId,
key: "🖐️",
rel_type: "m.annotation",
},
},
);
act(() => {
// Mock receiving a reaction.
handRaisedSubject$.next({
[localIdent]: {
time: new Date(),
reactionEventId: "",
membershipEventId: localRtcMember.eventId!,
},
],
]);
});
});
expect(container).toMatchSnapshot();
});

test("Can lower hand", async () => {
const reactionEventId = "$my-reaction-event:example.org";
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { vm, rtcSession, handRaisedSubject$ } =
getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} />,
<TestComponent vm={vm} rtcSession={rtcSession} />,
);
const reactionEvent = room.testSendHandRaise(memberEventAlice, membership);
await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.raise_hand"));
act(() => {
handRaisedSubject$.next({
[localIdent]: {
time: new Date(),
reactionEventId,
membershipEventId: localRtcMember.eventId!,
},
});
});
await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.lower_hand"));
expect(room.testRedactedEvents).toEqual([[undefined, reactionEvent]]);
expect(rtcSession.room.client.redactEvent).toHaveBeenCalledWith(
rtcSession.room.roomId,
reactionEventId,
);
act(() => {
// Mock receiving a redacted reaction.
handRaisedSubject$.next({});
});
expect(container).toMatchSnapshot();
});

test("Can react with emoji", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, getByText } = render(
<TestComponent rtcSession={rtcSession} />,
<TestComponent vm={vm} rtcSession={rtcSession} />,
);
await user.click(getByLabelText("common.reactions"));
await user.click(getByText("🐶"));
expect(room.testSentEvents).toEqual([
[
undefined,
ElementCallReactionEventType,
{
"m.relates_to": {
event_id: memberEventAlice,
rel_type: "m.reference",
},
name: "dog",
emoji: "🐶",
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
rtcSession.room.roomId,
ElementCallReactionEventType,
{
"m.relates_to": {
event_id: localRtcMember.eventId,
rel_type: "m.reference",
},
],
]);
name: "dog",
emoji: "🐶",
},
);
});

test("Can fully expand emoji picker", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByText, container, getByLabelText } = render(
<TestComponent rtcSession={rtcSession} />,
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container, getByText } = render(
<TestComponent vm={vm} rtcSession={rtcSession} />,
);
await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.show_more"));
expect(container).toMatchSnapshot();
await user.click(getByText("🦗"));

expect(room.testSentEvents).toEqual([
[
undefined,
ElementCallReactionEventType,
{
"m.relates_to": {
event_id: memberEventAlice,
rel_type: "m.reference",
},
name: "crickets",
emoji: "🦗",
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
rtcSession.room.roomId,
ElementCallReactionEventType,
{
"m.relates_to": {
event_id: localRtcMember.eventId,
rel_type: "m.reference",
},
],
]);
name: "crickets",
emoji: "🦗",
},
);
});

test("Can close reaction dialog", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} />,
<TestComponent vm={vm} rtcSession={rtcSession} />,
);
await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.show_more"));
Expand Down
28 changes: 18 additions & 10 deletions src/button/ReactionToggleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ import {
import { useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger";
import classNames from "classnames";
import { useObservableState } from "observable-hooks";
import { map } from "rxjs";

import { useReactions } from "../useReactions";
import { useReactionsSender } from "../reactions/useReactionsSender";
import styles from "./ReactionToggleButton.module.css";
import {
type ReactionOption,
ReactionSet,
ReactionsRowSize,
} from "../reactions";
import { Modal } from "../Modal";
import { type CallViewModel } from "../state/CallViewModel";

interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
raised: boolean;
Expand Down Expand Up @@ -162,22 +165,27 @@ export function ReactionPopupMenu({
}

interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> {
userId: string;
identifier: string;
vm: CallViewModel;
}

export function ReactionToggleButton({
userId,
identifier,
vm,
...props
}: ReactionToggleButtonProps): ReactNode {
const { t } = useTranslation();
const { raisedHands, toggleRaisedHand, sendReaction, reactions } =
useReactions();
const { toggleRaisedHand, sendReaction } = useReactionsSender();
const [busy, setBusy] = useState(false);
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
const [errorText, setErrorText] = useState<string>();

const isHandRaised = !!raisedHands[userId];
const canReact = !reactions[userId];
const isHandRaised = useObservableState(
vm.handsRaised$.pipe(map((v) => !!v[identifier])),
);
const canReact = useObservableState(
vm.reactions$.pipe(map((v) => !v[identifier])),
);

useEffect(() => {
// Clear whenever the reactions menu state changes.
Expand Down Expand Up @@ -223,7 +231,7 @@ export function ReactionToggleButton({
<InnerButton
disabled={busy}
onClick={() => setShowReactionsMenu((show) => !show)}
raised={isHandRaised}
raised={!!isHandRaised}
open={showReactionsMenu}
{...props}
/>
Expand All @@ -237,8 +245,8 @@ export function ReactionToggleButton({
>
<ReactionPopupMenu
errorText={errorText}
isHandRaised={isHandRaised}
canReact={!busy && canReact}
isHandRaised={!!isHandRaised}
canReact={!busy && !!canReact}
sendReaction={(reaction) => void sendRelation(reaction)}
toggleRaisedHand={wrappedToggleRaisedHand}
/>
Expand Down
20 changes: 10 additions & 10 deletions src/button/__snapshots__/ReactionToggleButton.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exports[`Can close reaction dialog 1`] = `
aria-disabled="false"
aria-expanded="true"
aria-haspopup="true"
aria-labelledby=":r9l:"
aria-labelledby=":rav:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
Expand Down Expand Up @@ -43,7 +43,7 @@ exports[`Can fully expand emoji picker 1`] = `
aria-disabled="false"
aria-expanded="true"
aria-haspopup="true"
aria-labelledby=":r6c:"
aria-labelledby=":r7m:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
Expand Down Expand Up @@ -75,8 +75,8 @@ exports[`Can lower hand 1`] = `
aria-expanded="false"
aria-haspopup="true"
aria-labelledby=":r36:"
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
data-size="lg"
role="button"
tabindex="0"
Expand All @@ -90,7 +90,9 @@ exports[`Can lower hand 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.944 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 2 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0V3Z"
clip-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
fill-rule="evenodd"
/>
</svg>
</button>
Expand Down Expand Up @@ -138,8 +140,8 @@ exports[`Can raise hand 1`] = `
aria-expanded="false"
aria-haspopup="true"
aria-labelledby=":r1j:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
Expand All @@ -153,9 +155,7 @@ exports[`Can raise hand 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
fill-rule="evenodd"
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.944 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 2 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0V3Z"
/>
</svg>
</button>
Expand Down
Loading
Loading