Skip to content

Commit

Permalink
Merge pull request #2749 from element-hq/hs/css-fixes-for-reactions
Browse files Browse the repository at this point in the history
Small improvements for reaction rendering
  • Loading branch information
robintown authored Nov 11, 2024
2 parents a6efdf0 + 6830744 commit 2946950
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 87 deletions.
10 changes: 10 additions & 0 deletions src/button/ReactionToggleButton.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
border-radius: var(--cpd-radius-pill-effect);
}

@media (max-width: 800px) {
.reactionButton {
padding: 1em;
font-size: 1em;
width: 1em;
height: 1em;
min-block-size: unset;
}
}

.verticalSeperator {
background-color: var(--cpd-color-gray-800);
width: 1px;
Expand Down
59 changes: 0 additions & 59 deletions src/room/InCallView.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,6 @@ Please see LICENSE in the repository root for full details.
justify-self: end;
}

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

.logo {
display: none;
}

.layout {
display: none !important;
}
}

@media (max-width: 370px) {
.raiseHand {
display: none;
Expand Down Expand Up @@ -180,48 +166,3 @@ Please see LICENSE in the repository root for full details.
position: relative;
flex-grow: 1;
}

.floatingReaction {
position: relative;
display: inline;
z-index: 2;
font-size: 32pt;
/* Reactions are "active" for 3 seconds (as per REACTION_ACTIVE_TIME_MS), give a bit more time for it to fade out. */
animation-duration: 4s;
animation-name: reaction-up;
width: fit-content;
pointer-events: none;
}

@keyframes reaction-up {
from {
opacity: 1;
translate: 100vw 0;
scale: 200%;
}

to {
opacity: 0;
translate: 100vw -100vh;
scale: 100%;
}
}

@media (prefers-reduced-motion) {
@keyframes reaction-up-reduced {
from {
opacity: 1;
}

to {
opacity: 0;
}
}

.floatingReaction {
font-size: 48pt;
animation-name: reaction-up-reduced;
top: calc(-50vh + (48pt / 2));
left: calc(50vw - (48pt / 2)) !important;
}
}
32 changes: 4 additions & 28 deletions src/room/InCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,8 @@ import handSoundOgg from "../sound/raise_hand.ogg?url";
import handSoundMp3 from "../sound/raise_hand.mp3?url";
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
import { useSwitchCamera } from "./useSwitchCamera";
import {
soundEffectVolumeSetting,
showReactions,
useSetting,
} from "../settings/settings";
import { soundEffectVolumeSetting, useSetting } from "../settings/settings";
import { ReactionsOverlay } from "./ReactionsOverlay";

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

Expand Down Expand Up @@ -185,27 +182,14 @@ export const InCallView: FC<InCallViewProps> = ({
connState,
onShareClick,
}) => {
const [shouldShowReactions] = useSetting(showReactions);
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
const { supportsReactions, raisedHands, reactions } = useReactions();
const { supportsReactions, raisedHands } = useReactions();
const raisedHandCount = useMemo(
() => Object.keys(raisedHands).length,
[raisedHands],
);
const previousRaisedHandCount = useDeferredValue(raisedHandCount);

const reactionsIcons = useMemo(
() =>
shouldShowReactions
? Object.entries(reactions).map(([sender, { emoji }]) => ({
sender,
emoji,
startX: -Math.ceil(Math.random() * 50) - 25,
}))
: [],
[shouldShowReactions, reactions],
);

useWakeLock();

useEffect(() => {
Expand Down Expand Up @@ -689,15 +673,7 @@ export const InCallView: FC<InCallViewProps> = ({
<source src={handSoundMp3} type="audio/mpeg" />
</audio>
<ReactionsAudioRenderer />
{reactionsIcons.map(({ sender, emoji, startX }) => (
<span
style={{ left: `${startX}vw` }}
className={styles.floatingReaction}
key={sender}
>
{emoji}
</span>
))}
<ReactionsOverlay />
{footer}
{layout.type !== "pip" && (
<>
Expand Down
54 changes: 54 additions & 0 deletions src/room/ReactionsOverlay.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.container {
position: absolute;
display: inline;
z-index: 2;
pointer-events: none;
width: 100vw;
height: 100vh;
left: 0;
top: 0;
}

.reaction {
font-size: 32pt;
/* Reactions are "active" for 3 seconds (as per REACTION_ACTIVE_TIME_MS), give a bit more time for it to fade out. */
animation-duration: 4s;
animation-name: reaction-up;
width: fit-content;
position: relative;
top: 80vh;
}

@keyframes reaction-up {
from {
opacity: 1;
translate: 0 0;
scale: 200%;
top: 80vh;
}

to {
top: 0;
opacity: 0;
scale: 100%;
}
}

@media (prefers-reduced-motion) {
@keyframes reaction-up-reduced {
from {
opacity: 1;
}

to {
opacity: 0;
}
}

.reaction {
font-size: 48pt;
animation-name: reaction-up-reduced;
top: calc(-50vh + (48pt / 2));
left: calc(50vw - (48pt / 2)) !important;
}
}
120 changes: 120 additions & 0 deletions src/room/ReactionsOverlay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
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 { expect, test } from "vitest";
import { TooltipProvider } from "@vector-im/compound-web";
import { act, ReactNode } from "react";
import { afterEach } from "node:test";

import {
MockRoom,
MockRTCSession,
TestReactionsWrapper,
} from "../utils/testReactions";
import { showReactions } from "../settings/settings";
import { ReactionsOverlay } from "./ReactionsOverlay";
import { ReactionSet } from "../reactions";

const memberUserIdAlice = "@alice:example.org";
const memberUserIdBob = "@bob:example.org";
const memberUserIdCharlie = "@charlie:example.org";
const memberEventAlice = "$membership-alice:example.org";
const memberEventBob = "$membership-bob:example.org";
const memberEventCharlie = "$membership-charlie:example.org";

const membership: Record<string, string> = {
[memberEventAlice]: memberUserIdAlice,
[memberEventBob]: memberUserIdBob,
[memberEventCharlie]: memberUserIdCharlie,
};

function TestComponent({
rtcSession,
}: {
rtcSession: MockRTCSession;
}): ReactNode {
return (
<TooltipProvider>
<TestReactionsWrapper rtcSession={rtcSession}>
<ReactionsOverlay />
</TestReactionsWrapper>
</TooltipProvider>
);
}

afterEach(() => {
showReactions.setValue(showReactions.defaultValue);
});

test("defaults to showing no reactions", () => {
showReactions.setValue(true);
const rtcSession = new MockRTCSession(
new MockRoom(memberUserIdAlice),
membership,
);
const { container } = render(<TestComponent rtcSession={rtcSession} />);
expect(container.getElementsByTagName("span")).toHaveLength(0);
});

test("shows a reaction when sent", () => {
showReactions.setValue(true);
const reaction = ReactionSet[0];
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByRole } = render(<TestComponent rtcSession={rtcSession} />);
act(() => {
room.testSendReaction(memberEventAlice, reaction, membership);
});
const span = getByRole("presentation");
expect(getByRole("presentation")).toBeTruthy();
expect(span.innerHTML).toEqual(reaction.emoji);
});

test("shows two of the same reaction when sent", () => {
showReactions.setValue(true);
const reaction = ReactionSet[0];
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
act(() => {
room.testSendReaction(memberEventAlice, reaction, membership);
});
act(() => {
room.testSendReaction(memberEventBob, reaction, membership);
});
expect(getAllByRole("presentation")).toHaveLength(2);
});

test("shows two different reactions when sent", () => {
showReactions.setValue(true);
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const [reactionA, reactionB] = ReactionSet;
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
act(() => {
room.testSendReaction(memberEventAlice, reactionA, membership);
});
act(() => {
room.testSendReaction(memberEventBob, reactionB, membership);
});
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
expect(reactionElementA.innerHTML).toEqual(reactionA.emoji);
expect(reactionElementB.innerHTML).toEqual(reactionB.emoji);
});

test("hides reactions when reaction animations are disabled", () => {
showReactions.setValue(false);
const reaction = ReactionSet[0];
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
act(() => {
room.testSendReaction(memberEventAlice, reaction, membership);
});
const { container } = render(<TestComponent rtcSession={rtcSession} />);
expect(container.getElementsByTagName("span")).toHaveLength(0);
});
50 changes: 50 additions & 0 deletions src/room/ReactionsOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { ReactNode, useMemo } from "react";

import { useReactions } from "../useReactions";
import {
showReactions as showReactionsSetting,
useSetting,
} from "../settings/settings";
import styles from "./ReactionsOverlay.module.css";

export function ReactionsOverlay(): ReactNode {
const { reactions } = useReactions();
const [showReactions] = useSetting(showReactionsSetting);
const reactionsIcons = useMemo(
() =>
showReactions
? Object.entries(reactions).map(([sender, { emoji }]) => ({
sender,
emoji,
startX: Math.ceil(Math.random() * 80) + 10,
}))
: [],
[showReactions, reactions],
);

return (
<div className={styles.container}>
{reactionsIcons.map(({ sender, emoji, startX }) => (
<span
// Reactions effects are considered presentation elements. The reaction
// is also present on the sender's tile, which assistive technology can
// read from instead.
role="presentation"
style={{ left: `${startX}vw` }}
className={styles.reaction}
// A sender can only send one emoji at a time.
key={sender}
>
{emoji}
</span>
))}
</div>
);
}

0 comments on commit 2946950

Please sign in to comment.