Skip to content

Commit 2946950

Browse files
authored
Merge pull request #2749 from element-hq/hs/css-fixes-for-reactions
Small improvements for reaction rendering
2 parents a6efdf0 + 6830744 commit 2946950

File tree

6 files changed

+238
-87
lines changed

6 files changed

+238
-87
lines changed

src/button/ReactionToggleButton.module.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@
4646
border-radius: var(--cpd-radius-pill-effect);
4747
}
4848

49+
@media (max-width: 800px) {
50+
.reactionButton {
51+
padding: 1em;
52+
font-size: 1em;
53+
width: 1em;
54+
height: 1em;
55+
min-block-size: unset;
56+
}
57+
}
58+
4959
.verticalSeperator {
5060
background-color: var(--cpd-color-gray-800);
5161
width: 1px;

src/room/InCallView.module.css

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -94,20 +94,6 @@ 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-
}
101-
102-
.logo {
103-
display: none;
104-
}
105-
106-
.layout {
107-
display: none !important;
108-
}
109-
}
110-
11197
@media (max-width: 370px) {
11298
.raiseHand {
11399
display: none;
@@ -180,48 +166,3 @@ Please see LICENSE in the repository root for full details.
180166
position: relative;
181167
flex-grow: 1;
182168
}
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-
}

src/room/InCallView.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,8 @@ import handSoundOgg from "../sound/raise_hand.ogg?url";
8585
import handSoundMp3 from "../sound/raise_hand.mp3?url";
8686
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
8787
import { useSwitchCamera } from "./useSwitchCamera";
88-
import {
89-
soundEffectVolumeSetting,
90-
showReactions,
91-
useSetting,
92-
} from "../settings/settings";
88+
import { soundEffectVolumeSetting, useSetting } from "../settings/settings";
89+
import { ReactionsOverlay } from "./ReactionsOverlay";
9390

9491
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
9592

@@ -185,27 +182,14 @@ export const InCallView: FC<InCallViewProps> = ({
185182
connState,
186183
onShareClick,
187184
}) => {
188-
const [shouldShowReactions] = useSetting(showReactions);
189185
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
190-
const { supportsReactions, raisedHands, reactions } = useReactions();
186+
const { supportsReactions, raisedHands } = useReactions();
191187
const raisedHandCount = useMemo(
192188
() => Object.keys(raisedHands).length,
193189
[raisedHands],
194190
);
195191
const previousRaisedHandCount = useDeferredValue(raisedHandCount);
196192

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-
209193
useWakeLock();
210194

211195
useEffect(() => {
@@ -689,15 +673,7 @@ export const InCallView: FC<InCallViewProps> = ({
689673
<source src={handSoundMp3} type="audio/mpeg" />
690674
</audio>
691675
<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-
))}
676+
<ReactionsOverlay />
701677
{footer}
702678
{layout.type !== "pip" && (
703679
<>
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: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
import { afterEach } from "node:test";
13+
14+
import {
15+
MockRoom,
16+
MockRTCSession,
17+
TestReactionsWrapper,
18+
} from "../utils/testReactions";
19+
import { showReactions } from "../settings/settings";
20+
import { ReactionsOverlay } from "./ReactionsOverlay";
21+
import { ReactionSet } from "../reactions";
22+
23+
const memberUserIdAlice = "@alice:example.org";
24+
const memberUserIdBob = "@bob:example.org";
25+
const memberUserIdCharlie = "@charlie:example.org";
26+
const memberEventAlice = "$membership-alice:example.org";
27+
const memberEventBob = "$membership-bob:example.org";
28+
const memberEventCharlie = "$membership-charlie:example.org";
29+
30+
const membership: Record<string, string> = {
31+
[memberEventAlice]: memberUserIdAlice,
32+
[memberEventBob]: memberUserIdBob,
33+
[memberEventCharlie]: memberUserIdCharlie,
34+
};
35+
36+
function TestComponent({
37+
rtcSession,
38+
}: {
39+
rtcSession: MockRTCSession;
40+
}): ReactNode {
41+
return (
42+
<TooltipProvider>
43+
<TestReactionsWrapper rtcSession={rtcSession}>
44+
<ReactionsOverlay />
45+
</TestReactionsWrapper>
46+
</TooltipProvider>
47+
);
48+
}
49+
50+
afterEach(() => {
51+
showReactions.setValue(showReactions.defaultValue);
52+
});
53+
54+
test("defaults to showing no reactions", () => {
55+
showReactions.setValue(true);
56+
const rtcSession = new MockRTCSession(
57+
new MockRoom(memberUserIdAlice),
58+
membership,
59+
);
60+
const { container } = render(<TestComponent rtcSession={rtcSession} />);
61+
expect(container.getElementsByTagName("span")).toHaveLength(0);
62+
});
63+
64+
test("shows a reaction when sent", () => {
65+
showReactions.setValue(true);
66+
const reaction = ReactionSet[0];
67+
const room = new MockRoom(memberUserIdAlice);
68+
const rtcSession = new MockRTCSession(room, membership);
69+
const { getByRole } = render(<TestComponent rtcSession={rtcSession} />);
70+
act(() => {
71+
room.testSendReaction(memberEventAlice, reaction, membership);
72+
});
73+
const span = getByRole("presentation");
74+
expect(getByRole("presentation")).toBeTruthy();
75+
expect(span.innerHTML).toEqual(reaction.emoji);
76+
});
77+
78+
test("shows two of the same reaction when sent", () => {
79+
showReactions.setValue(true);
80+
const reaction = ReactionSet[0];
81+
const room = new MockRoom(memberUserIdAlice);
82+
const rtcSession = new MockRTCSession(room, membership);
83+
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
84+
act(() => {
85+
room.testSendReaction(memberEventAlice, reaction, membership);
86+
});
87+
act(() => {
88+
room.testSendReaction(memberEventBob, reaction, membership);
89+
});
90+
expect(getAllByRole("presentation")).toHaveLength(2);
91+
});
92+
93+
test("shows two different reactions when sent", () => {
94+
showReactions.setValue(true);
95+
const room = new MockRoom(memberUserIdAlice);
96+
const rtcSession = new MockRTCSession(room, membership);
97+
const [reactionA, reactionB] = ReactionSet;
98+
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
99+
act(() => {
100+
room.testSendReaction(memberEventAlice, reactionA, membership);
101+
});
102+
act(() => {
103+
room.testSendReaction(memberEventBob, reactionB, membership);
104+
});
105+
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
106+
expect(reactionElementA.innerHTML).toEqual(reactionA.emoji);
107+
expect(reactionElementB.innerHTML).toEqual(reactionB.emoji);
108+
});
109+
110+
test("hides reactions when reaction animations are disabled", () => {
111+
showReactions.setValue(false);
112+
const reaction = ReactionSet[0];
113+
const room = new MockRoom(memberUserIdAlice);
114+
const rtcSession = new MockRTCSession(room, membership);
115+
act(() => {
116+
room.testSendReaction(memberEventAlice, reaction, membership);
117+
});
118+
const { container } = render(<TestComponent rtcSession={rtcSession} />);
119+
expect(container.getElementsByTagName("span")).toHaveLength(0);
120+
});

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)