mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-02 04:05:56 +00:00
Move reactions to own component.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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(() => {
|
||||
@@ -689,15 +676,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" && (
|
||||
<>
|
||||
|
||||
54
src/room/ReactionsOverlay.module.css
Normal file
54
src/room/ReactionsOverlay.module.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
126
src/room/ReactionsOverlay.test.tsx
Normal file
126
src/room/ReactionsOverlay.test.tsx
Normal file
@@ -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<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 room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(
|
||||
room,
|
||||
membership,
|
||||
);
|
||||
const { getAllByRole } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
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(<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
src/room/ReactionsOverlay.tsx
Normal file
50
src/room/ReactionsOverlay.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user