Skip to content

Commit 699b69f

Browse files
committed
Move reactions to own component.
1 parent 67e5abc commit 699b69f

File tree

5 files changed

+234
-82
lines changed

5 files changed

+234
-82
lines changed

src/room/InCallView.module.css

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,7 @@ Please see LICENSE in the repository root for full details.
9494
justify-self: end;
9595
}
9696

97-
@media (max-width: 660px) {
98-
.footer {
99-
grid-template-areas: ". buttons buttons buttons .";
100-
}
10197

102-
.logo {
103-
display: none;
104-
}
105-
106-
.layout {
107-
display: none !important;
108-
}
109-
}
11098

11199
@media (max-width: 370px) {
112100
.raiseHand {
@@ -179,49 +167,4 @@ Please see LICENSE in the repository root for full details.
179167
.tile.maximised {
180168
position: relative;
181169
flex-grow: 1;
182-
}
183-
184-
.floatingReaction {
185-
position: relative;
186-
display: inline;
187-
z-index: 2;
188-
font-size: 32pt;
189-
/* Reactions are "active" for 3 seconds (as per REACTION_ACTIVE_TIME_MS), give a bit more time for it to fade out. */
190-
animation-duration: 4s;
191-
animation-name: reaction-up;
192-
width: fit-content;
193-
pointer-events: none;
194-
}
195-
196-
@keyframes reaction-up {
197-
from {
198-
opacity: 1;
199-
translate: 100vw 0;
200-
scale: 200%;
201-
}
202-
203-
to {
204-
opacity: 0;
205-
translate: 100vw -100vh;
206-
scale: 100%;
207-
}
208-
}
209-
210-
@media (prefers-reduced-motion) {
211-
@keyframes reaction-up-reduced {
212-
from {
213-
opacity: 1;
214-
}
215-
216-
to {
217-
opacity: 0;
218-
}
219-
}
220-
221-
.floatingReaction {
222-
font-size: 48pt;
223-
animation-name: reaction-up-reduced;
224-
top: calc(-50vh + (48pt / 2));
225-
left: calc(50vw - (48pt / 2)) !important;
226-
}
227-
}
170+
}

src/room/InCallView.tsx

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
8787
import { useSwitchCamera } from "./useSwitchCamera";
8888
import {
8989
soundEffectVolumeSetting,
90-
showReactions,
9190
useSetting,
9291
} from "../settings/settings";
92+
import { ReactionsOverlay } from "./ReactionsOverlay";
9393

9494
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
9595

@@ -185,27 +185,14 @@ export const InCallView: FC<InCallViewProps> = ({
185185
connState,
186186
onShareClick,
187187
}) => {
188-
const [shouldShowReactions] = useSetting(showReactions);
189188
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
190-
const { supportsReactions, raisedHands, reactions } = useReactions();
189+
const { supportsReactions, raisedHands } = useReactions();
191190
const raisedHandCount = useMemo(
192191
() => Object.keys(raisedHands).length,
193192
[raisedHands],
194193
);
195194
const previousRaisedHandCount = useDeferredValue(raisedHandCount);
196195

197-
const reactionsIcons = useMemo(
198-
() =>
199-
shouldShowReactions
200-
? Object.entries(reactions).map(([sender, { emoji }]) => ({
201-
sender,
202-
emoji,
203-
startX: -Math.ceil(Math.random() * 50) - 25,
204-
}))
205-
: [],
206-
[shouldShowReactions, reactions],
207-
);
208-
209196
useWakeLock();
210197

211198
useEffect(() => {
@@ -689,15 +676,7 @@ export const InCallView: FC<InCallViewProps> = ({
689676
<source src={handSoundMp3} type="audio/mpeg" />
690677
</audio>
691678
<ReactionsAudioRenderer />
692-
{reactionsIcons.map(({ sender, emoji, startX }) => (
693-
<span
694-
style={{ left: `${startX}vw` }}
695-
className={styles.floatingReaction}
696-
key={sender}
697-
>
698-
{emoji}
699-
</span>
700-
))}
679+
<ReactionsOverlay />
701680
{footer}
702681
{layout.type !== "pip" && (
703682
<>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.container {
2+
position: absolute;
3+
display: inline;
4+
z-index: 2;
5+
pointer-events: none;
6+
width: 100vw;
7+
height: 100vh;
8+
left: 0;
9+
top: 0;
10+
}
11+
12+
.reaction {
13+
font-size: 32pt;
14+
/* Reactions are "active" for 3 seconds (as per REACTION_ACTIVE_TIME_MS), give a bit more time for it to fade out. */
15+
animation-duration: 4s;
16+
animation-name: reaction-up;
17+
width: fit-content;
18+
position: relative;
19+
top: 80vh;
20+
}
21+
22+
@keyframes reaction-up {
23+
from {
24+
opacity: 1;
25+
translate: 0 0;
26+
scale: 200%;
27+
top: 80vh;
28+
}
29+
30+
to {
31+
top: 0;
32+
opacity: 0;
33+
scale: 100%;
34+
}
35+
}
36+
37+
@media (prefers-reduced-motion) {
38+
@keyframes reaction-up-reduced {
39+
from {
40+
opacity: 1;
41+
}
42+
43+
to {
44+
opacity: 0;
45+
}
46+
}
47+
48+
.reaction {
49+
font-size: 48pt;
50+
animation-name: reaction-up-reduced;
51+
top: calc(-50vh + (48pt / 2));
52+
left: calc(50vw - (48pt / 2)) !important;
53+
}
54+
}

src/room/ReactionsOverlay.test.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
Copyright 2024 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only
5+
Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
import { render } from "@testing-library/react";
9+
import { expect, test } from "vitest";
10+
import { TooltipProvider } from "@vector-im/compound-web";
11+
import { act, ReactNode } from "react";
12+
13+
import {
14+
MockRoom,
15+
MockRTCSession,
16+
TestReactionsWrapper,
17+
} from "../utils/testReactions";
18+
import {
19+
showReactions,
20+
} from "../settings/settings";
21+
import { ReactionsOverlay } from "./ReactionsOverlay";
22+
import { afterEach } from "node:test";
23+
import { ReactionSet } from "../reactions";
24+
25+
const memberUserIdAlice = "@alice:example.org";
26+
const memberUserIdBob = "@bob:example.org";
27+
const memberUserIdCharlie = "@charlie:example.org";
28+
const memberEventAlice = "$membership-alice:example.org";
29+
const memberEventBob = "$membership-bob:example.org";
30+
const memberEventCharlie = "$membership-charlie:example.org";
31+
32+
const membership: Record<string, string> = {
33+
[memberEventAlice]: memberUserIdAlice,
34+
[memberEventBob]: memberUserIdBob,
35+
[memberEventCharlie]: memberUserIdCharlie,
36+
};
37+
38+
function TestComponent({
39+
rtcSession,
40+
}: {
41+
rtcSession: MockRTCSession;
42+
}): ReactNode {
43+
return (
44+
<TooltipProvider>
45+
<TestReactionsWrapper rtcSession={rtcSession}>
46+
<ReactionsOverlay />
47+
</TestReactionsWrapper>
48+
</TooltipProvider>
49+
);
50+
}
51+
52+
53+
afterEach(() => {
54+
showReactions.setValue(showReactions.defaultValue);
55+
});
56+
57+
test("defaults to showing no reactions", () => {
58+
showReactions.setValue(true);
59+
const rtcSession = new MockRTCSession(
60+
new MockRoom(memberUserIdAlice),
61+
membership,
62+
);
63+
const { container } = render(<TestComponent rtcSession={rtcSession} />);
64+
expect(container.getElementsByTagName("span")).toHaveLength(
65+
0
66+
);
67+
});
68+
69+
test("shows a reaction when sent", () => {
70+
showReactions.setValue(true);
71+
const reaction = ReactionSet[0];
72+
const room = new MockRoom(memberUserIdAlice);
73+
const rtcSession = new MockRTCSession(
74+
room,
75+
membership,
76+
);
77+
const { getByRole } = render(<TestComponent rtcSession={rtcSession} />);
78+
act(() => room.testSendReaction(memberEventAlice, reaction, membership));
79+
const span = getByRole('presentation');
80+
expect(getByRole('presentation')).toBeTruthy();
81+
expect(span.innerHTML).toEqual(reaction.emoji);
82+
});
83+
84+
test("shows two of the same reaction when sent", () => {
85+
showReactions.setValue(true);
86+
const room = new MockRoom(memberUserIdAlice);
87+
const rtcSession = new MockRTCSession(
88+
room,
89+
membership,
90+
);
91+
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
92+
act(() => room.testSendReaction(memberEventAlice, ReactionSet[0], membership));
93+
act(() => room.testSendReaction(memberEventBob, ReactionSet[0], membership));
94+
expect(getAllByRole('presentation')).toHaveLength(2);
95+
});
96+
97+
test("shows two different reactions when sent", () => {
98+
showReactions.setValue(true);
99+
const room = new MockRoom(memberUserIdAlice);
100+
const rtcSession = new MockRTCSession(
101+
room,
102+
membership,
103+
);
104+
const [reactionA, reactionB] = ReactionSet;
105+
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
106+
act(() => room.testSendReaction(memberEventAlice, reactionA, membership));
107+
act(() => room.testSendReaction(memberEventBob, reactionB, membership));
108+
const [reactionElementA, reactionElementB] = getAllByRole('presentation');
109+
expect(reactionElementA.innerHTML).toEqual(reactionA.emoji);
110+
expect(reactionElementB.innerHTML).toEqual(reactionB.emoji);
111+
});
112+
113+
test("hides reactions when reaction animations are disabled", () => {
114+
showReactions.setValue(false);
115+
const reaction = ReactionSet[0];
116+
const room = new MockRoom(memberUserIdAlice);
117+
const rtcSession = new MockRTCSession(
118+
room,
119+
membership,
120+
);
121+
act(() => room.testSendReaction(memberEventAlice, reaction, membership));
122+
const { container } = render(<TestComponent rtcSession={rtcSession} />);
123+
expect(container.getElementsByTagName("span")).toHaveLength(
124+
0
125+
);
126+
});

src/room/ReactionsOverlay.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright 2024 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only
5+
Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
import { ReactNode, useMemo } from "react";
9+
10+
import { useReactions } from "../useReactions";
11+
import {
12+
showReactions as showReactionsSetting,
13+
useSetting,
14+
} from "../settings/settings";
15+
import styles from "./ReactionsOverlay.module.css";
16+
17+
export function ReactionsOverlay(): ReactNode {
18+
const { reactions } = useReactions();
19+
const [showReactions] = useSetting(showReactionsSetting);
20+
const reactionsIcons = useMemo(
21+
() =>
22+
showReactions
23+
? Object.entries(reactions).map(([sender, { emoji }]) => ({
24+
sender,
25+
emoji,
26+
startX: Math.ceil(Math.random() * 80) + 10,
27+
}))
28+
: [],
29+
[showReactions, reactions],
30+
);
31+
32+
return (
33+
<div className={styles.container}>
34+
{reactionsIcons.map(({ sender, emoji, startX }) => (
35+
<span
36+
// Reactions effects are considered presentation elements. The reaction
37+
// is also present on the sender's tile, which assistive technology can
38+
// read from instead.
39+
role="presentation"
40+
style={{ left: `${startX}vw` }}
41+
className={styles.reaction}
42+
// A sender can only send one emoji at a time.
43+
key={sender}
44+
>
45+
{emoji}
46+
</span>
47+
))}
48+
</div>
49+
);
50+
}

0 commit comments

Comments
 (0)