From 699b69f01930ab86fc405a53ce443dd193e5ac75 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 11 Nov 2024 12:07:02 +0000 Subject: [PATCH] Move reactions to own component. --- src/room/InCallView.module.css | 59 +------------ src/room/InCallView.tsx | 27 +----- src/room/ReactionsOverlay.module.css | 54 ++++++++++++ src/room/ReactionsOverlay.test.tsx | 126 +++++++++++++++++++++++++++ src/room/ReactionsOverlay.tsx | 50 +++++++++++ 5 files changed, 234 insertions(+), 82 deletions(-) create mode 100644 src/room/ReactionsOverlay.module.css create mode 100644 src/room/ReactionsOverlay.test.tsx create mode 100644 src/room/ReactionsOverlay.tsx diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 6a87b383..08022979 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -94,19 +94,7 @@ 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 { @@ -179,49 +167,4 @@ Please see LICENSE in the repository root for full details. .tile.maximised { 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; - } -} +} \ No newline at end of file diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 505ab0ca..f5520530 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -87,9 +87,9 @@ import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; import { useSwitchCamera } from "./useSwitchCamera"; import { soundEffectVolumeSetting, - showReactions, useSetting, } from "../settings/settings"; +import { ReactionsOverlay } from "./ReactionsOverlay"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); @@ -185,27 +185,14 @@ export const InCallView: FC = ({ 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(() => { @@ -689,15 +676,7 @@ export const InCallView: FC = ({ - {reactionsIcons.map(({ sender, emoji, startX }) => ( - - {emoji} - - ))} + {footer} {layout.type !== "pip" && ( <> diff --git a/src/room/ReactionsOverlay.module.css b/src/room/ReactionsOverlay.module.css new file mode 100644 index 00000000..3738dc09 --- /dev/null +++ b/src/room/ReactionsOverlay.module.css @@ -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; + } +} diff --git a/src/room/ReactionsOverlay.test.tsx b/src/room/ReactionsOverlay.test.tsx new file mode 100644 index 00000000..86027d83 --- /dev/null +++ b/src/room/ReactionsOverlay.test.tsx @@ -0,0 +1,126 @@ +/* +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 { + MockRoom, + MockRTCSession, + TestReactionsWrapper, +} from "../utils/testReactions"; +import { + showReactions, +} from "../settings/settings"; +import { ReactionsOverlay } from "./ReactionsOverlay"; +import { afterEach } from "node:test"; +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 = { + [memberEventAlice]: memberUserIdAlice, + [memberEventBob]: memberUserIdBob, + [memberEventCharlie]: memberUserIdCharlie, +}; + +function TestComponent({ + rtcSession, +}: { + rtcSession: MockRTCSession; +}): ReactNode { + return ( + + + + + + ); +} + + +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(); + 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(); + 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 room = new MockRoom(memberUserIdAlice); + const rtcSession = new MockRTCSession( + room, + membership, + ); + const { getAllByRole } = render(); + act(() => room.testSendReaction(memberEventAlice, ReactionSet[0], membership)); + act(() => room.testSendReaction(memberEventBob, ReactionSet[0], 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(); + 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(); + expect(container.getElementsByTagName("span")).toHaveLength( + 0 + ); +}); \ No newline at end of file diff --git a/src/room/ReactionsOverlay.tsx b/src/room/ReactionsOverlay.tsx new file mode 100644 index 00000000..7cdf7568 --- /dev/null +++ b/src/room/ReactionsOverlay.tsx @@ -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 ( +
+ {reactionsIcons.map(({ sender, emoji, startX }) => ( + + {emoji} + + ))} +
+ ); +}